Compare commits
103 commits
a3c5993797
...
907063c97a
Author | SHA1 | Date | |
---|---|---|---|
907063c97a | |||
775f6b1d2b | |||
04c10bb680 | |||
9b40c46d59 | |||
5b6599f98d | |||
a52dad864b | |||
c87d0a16a1 | |||
b7a7ea4027 | |||
907a7177de | |||
fa38a39dfe | |||
0dd58b714a | |||
c618e1f619 | |||
47c190cd16 | |||
ddf464844d | |||
2dee9f858a | |||
978e2cccce | |||
343a3450a7 | |||
fe39a749e4 | |||
b35fde5e3a | |||
390fe46d23 | |||
67243dca8f | |||
fb7c3ca4fa | |||
5e1a39ddc9 | |||
d61401dd6d | |||
eed9613982 | |||
9a0de1e4bb | |||
9dbdff28df | |||
9c4115c217 | |||
36f9a6df21 | |||
e745c8557b | |||
ea94f12cdf | |||
d01c3a1eea | |||
a16fd2f9d4 | |||
3284a1e2db | |||
b0d8f22195 | |||
cdba672a34 | |||
b52a57fc1c | |||
5e6f1f00d8 | |||
c87a14d72d | |||
874f5998fa | |||
becacba86a | |||
864c999373 | |||
312931876e | |||
a69a539330 | |||
14505931b5 | |||
dc4b9dd1b3 | |||
777b493aab | |||
cdbfbf95ae | |||
6d74d7c573 | |||
9ee5472bf2 | |||
2d45d3598a | |||
e6bff18ec1 | |||
7ca39764f1 | |||
c5e4bd9f93 | |||
b5906e919b | |||
0def7e62d5 | |||
ca262bcbb1 | |||
00ea9e3cdf | |||
ded94fda08 | |||
1d406a0ef0 | |||
67c10cd4d2 | |||
75dc7c704f | |||
583ed1215c | |||
68292e72b7 | |||
8fff46e0e3 | |||
9a073e6872 | |||
bece55207a | |||
705b4a29bc | |||
1455e65c1c | |||
c166b7a5fb | |||
f8c54ab387 | |||
f24f99426b | |||
43057034eb | |||
4b8d7a2378 | |||
02e299907a | |||
e15979c9ab | |||
aed4d5a578 | |||
48becac6be | |||
bcdd41f1d7 | |||
6bd55a99fb | |||
00712185e8 | |||
f4368c8d8d | |||
17472cc54c | |||
60be98dad0 | |||
1ed4918138 | |||
3d9fb46ccc | |||
62e3ef06f8 | |||
e61ef6dcbe | |||
51a3f051f3 | |||
74f903d62c | |||
34dfe7f22a | |||
7e66b66a95 | |||
e7b1eff152 | |||
2be52bc207 | |||
4c9201e8b4 | |||
b820781eff | |||
1bcd2b93f1 | |||
36ffe2fa51 | |||
7d9eee7a90 | |||
57bc0a9144 | |||
48b39abc0c | |||
0a2b07bea0 | |||
a530d3dfb2 |
118 changed files with 913 additions and 822 deletions
|
@ -9,13 +9,38 @@ import { coerceObject, datetimeSchema, filteredArray } from './utils';
|
|||
const filterBadges = (tags?: string[]) =>
|
||||
tags?.filter(tag => tag.startsWith('badge:')).map(tag => v.parse(roleSchema, { id: tag, name: tag.replace(/^badge:/, '') }));
|
||||
|
||||
const getDomainFromURL = (account: any): string => {
|
||||
try {
|
||||
const url = account.url;
|
||||
return new URL(url).host;
|
||||
} catch {
|
||||
return '';
|
||||
}
|
||||
};
|
||||
|
||||
const guessFqn = (account: any): string => {
|
||||
const acct = account.acct;
|
||||
const [user, domain] = acct.split('@');
|
||||
|
||||
if (domain) {
|
||||
return acct;
|
||||
} else {
|
||||
return [user, getDomainFromURL(account)].join('@');
|
||||
}
|
||||
};
|
||||
|
||||
const preprocessAccount = v.transform((account: any) => {
|
||||
if (!account?.acct) return null;
|
||||
|
||||
const username = account.username || account.acct.split('@')[0];
|
||||
|
||||
const fqn = account.fqn || guessFqn(account);
|
||||
const domain = fqn.split('@')[1] || '';
|
||||
|
||||
return {
|
||||
username,
|
||||
fqn,
|
||||
domain,
|
||||
avatar_static: account.avatar_static || account.avatar,
|
||||
header_static: account.header_static || account.header,
|
||||
local: typeof account.pleroma?.is_local === 'boolean' ? account.pleroma.is_local : account.acct.split('@')[1] === undefined,
|
||||
|
@ -73,7 +98,7 @@ const baseAccountSchema = v.object({
|
|||
acct: v.fallback(v.string(), ''),
|
||||
url: v.pipe(v.string(), v.url()),
|
||||
display_name: v.fallback(v.string(), ''),
|
||||
note: v.fallback(v.string(), ''),
|
||||
note: v.fallback(v.pipe(v.string(), v.transform(note => note === '<p></p>' ? '' : note)), ''),
|
||||
avatar: v.fallback(v.string(), ''),
|
||||
avatar_static: v.fallback(v.pipe(v.string(), v.url()), ''),
|
||||
header: v.fallback(v.pipe(v.string(), v.url()), ''),
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"name": "pl-fe",
|
||||
"name": "BigBuffet",
|
||||
"description": "Web-based federated social media client, a fork of Soapbox.",
|
||||
"keywords": ["fediverse"],
|
||||
"website": "https://github.com/mkljczk/pl-fe",
|
||||
"website": "https://forge.pch.net/BigBuffet/frontend-rw",
|
||||
"stack": "container"
|
||||
}
|
|
@ -1,13 +1,13 @@
|
|||
{
|
||||
"name": "pl-fe",
|
||||
"displayName": "pl-fe",
|
||||
"displayName": "BigBuffet",
|
||||
"version": "0.0.1",
|
||||
"type": "module",
|
||||
"description": "Web-based federated social media client, a fork of Soapbox",
|
||||
"homepage": "https://github.com/mkljczk/pl-fe",
|
||||
"homepage": "https://bigbuff.et",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/mkljczk/pl-fe"
|
||||
"url": "https://forge.pch.net/BigBuffet/frontend-rw"
|
||||
},
|
||||
"keywords": [
|
||||
"fediverse",
|
||||
|
|
|
@ -95,9 +95,7 @@
|
|||
"compose_form.sensitive.marked": "Media is marked as sensitive",
|
||||
"compose_form.sensitive.unmarked": "Media is not marked as sensitive",
|
||||
"compose_form.spoiler.marked": "Text is hidden behind warning",
|
||||
"compose_form.spoiler.unmarked": "Text is not hidden",
|
||||
"compose_form.spoiler_placeholder": "Write your warning here",
|
||||
"confirmation_modal.cancel": "Cancel",
|
||||
"confirmation_modal.cancel": "Cancel",
|
||||
"confirmations.block.block_and_report": "Block & Report",
|
||||
"confirmations.block.confirm": "Block",
|
||||
"confirmations.block.message": "Are you sure you want to block {name}?",
|
||||
|
@ -574,8 +572,6 @@
|
|||
"compose_form.sensitive.marked": "Media is marked as sensitive",
|
||||
"compose_form.sensitive.unmarked": "Media is not marked as sensitive",
|
||||
"compose_form.spoiler.marked": "Text is hidden behind warning",
|
||||
"compose_form.spoiler.unmarked": "Text is not hidden",
|
||||
"compose_form.spoiler_placeholder": "Write your warning here",
|
||||
"confirmation_modal.cancel": "Cancel",
|
||||
"confirmations.block.block_and_report": "Block & Report",
|
||||
"confirmations.block.confirm": "Block",
|
||||
|
|
|
@ -5,6 +5,7 @@ import { getClient } from 'pl-fe/api';
|
|||
import { isNativeEmoji } from 'pl-fe/features/emoji';
|
||||
import emojiSearch from 'pl-fe/features/emoji/search';
|
||||
import { Language } from 'pl-fe/features/preferences';
|
||||
import { userTouching } from 'pl-fe/is-mobile';
|
||||
import { selectAccount, selectOwnAccount, makeGetAccount } from 'pl-fe/selectors';
|
||||
import { tagHistory } from 'pl-fe/settings';
|
||||
import { useModalsStore } from 'pl-fe/stores/modals';
|
||||
|
@ -176,6 +177,9 @@ const replyCompose = (
|
|||
rebloggedBy?: ComposeReplyAction['rebloggedBy'],
|
||||
) =>
|
||||
(dispatch: AppDispatch, getState: () => RootState) => {
|
||||
if (!userTouching.matches) {
|
||||
return window.open(`/compose?in_reply_to=${status.id}`, 'targetWindow', 'height=500,width=700');
|
||||
}
|
||||
const state = getState();
|
||||
const client = getClient(state);
|
||||
const { createStatusExplicitAddressing: explicitAddressing } = client.features;
|
||||
|
@ -213,6 +217,9 @@ interface ComposeQuoteAction {
|
|||
|
||||
const quoteCompose = (status: ComposeQuoteAction['status']) =>
|
||||
(dispatch: AppDispatch, getState: () => RootState) => {
|
||||
if (!userTouching.matches) {
|
||||
return window.open(`/compose?quote=${status.id}`, 'targetWindow', 'height=500,width=700');
|
||||
}
|
||||
const state = getState();
|
||||
const { createStatusExplicitAddressing: explicitAddressing } = state.auth.client.features;
|
||||
|
||||
|
@ -299,7 +306,7 @@ const directComposeById = (accountId: string) =>
|
|||
useModalsStore.getState().openModal('COMPOSE');
|
||||
};
|
||||
|
||||
const handleComposeSubmit = (dispatch: AppDispatch, getState: () => RootState, composeId: string, data: BaseStatus | ScheduledStatus, status: string, edit?: boolean) => {
|
||||
const handleComposeSubmit = (dispatch: AppDispatch, getState: () => RootState, composeId: string, data: BaseStatus | ScheduledStatus, status: string, edit?: boolean, propagate?: boolean) => {
|
||||
if (!dispatch || !getState) return;
|
||||
|
||||
const state = getState();
|
||||
|
@ -308,18 +315,24 @@ const handleComposeSubmit = (dispatch: AppDispatch, getState: () => RootState, c
|
|||
const draftId = getState().compose.get(composeId)!.draft_id;
|
||||
|
||||
dispatch(submitComposeSuccess(composeId, data, accountUrl, draftId));
|
||||
let toastMessage;
|
||||
let toastOpts;
|
||||
if (data.scheduled_at === null) {
|
||||
dispatch(insertIntoTagHistory(composeId, data.tags || [], status));
|
||||
toast.success(edit ? messages.editSuccess : messages.success, {
|
||||
toastMessage = edit ? messages.editSuccess : messages.success;
|
||||
toastOpts = {
|
||||
actionLabel: messages.view,
|
||||
actionLink: `/@${data.account.acct}/posts/${data.id}`,
|
||||
});
|
||||
};
|
||||
} else {
|
||||
toast.success(messages.scheduledSuccess, {
|
||||
toastMessage = messages.scheduledSuccess;
|
||||
toastOpts = {
|
||||
actionLabel: messages.view,
|
||||
actionLink: '/scheduled_statuses',
|
||||
});
|
||||
};
|
||||
}
|
||||
if (propagate) toast.propagate('success', toastMessage, toastOpts);
|
||||
else toast.success(toastMessage, toastOpts);
|
||||
};
|
||||
|
||||
const needsDescriptions = (state: RootState, composeId: string) => {
|
||||
|
@ -343,11 +356,13 @@ const validateSchedule = (state: RootState, composeId: string) => {
|
|||
interface SubmitComposeOpts {
|
||||
history?: History;
|
||||
force?: boolean;
|
||||
onSubmit?: () => void;
|
||||
propagate?: boolean;
|
||||
}
|
||||
|
||||
const submitCompose = (composeId: string, opts: SubmitComposeOpts = {}) =>
|
||||
async (dispatch: AppDispatch, getState: () => RootState) => {
|
||||
const { history, force = false } = opts;
|
||||
const { history, force = false, onSubmit, propagate } = opts;
|
||||
|
||||
if (!isLoggedIn(getState)) return;
|
||||
const state = getState();
|
||||
|
@ -359,6 +374,7 @@ const submitCompose = (composeId: string, opts: SubmitComposeOpts = {}) =>
|
|||
const statusId = compose.id;
|
||||
let to = compose.to;
|
||||
|
||||
|
||||
if (!validateSchedule(state, composeId)) {
|
||||
toast.error(messages.scheduleError);
|
||||
return;
|
||||
|
@ -443,7 +459,8 @@ const submitCompose = (composeId: string, opts: SubmitComposeOpts = {}) =>
|
|||
if (!statusId && data.scheduled_at === null && data.visibility === 'direct' && getState().conversations.mounted <= 0 && history) {
|
||||
history.push('/conversations');
|
||||
}
|
||||
handleComposeSubmit(dispatch, getState, composeId, data, status, !!statusId);
|
||||
handleComposeSubmit(dispatch, getState, composeId, data, status, !!statusId, propagate);
|
||||
if (onSubmit) onSubmit();
|
||||
}).catch((error) => {
|
||||
dispatch(submitComposeFail(composeId, error));
|
||||
});
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
import { serialize } from 'object-to-formdata';
|
||||
import { defineMessages } from 'react-intl';
|
||||
|
||||
import toast from 'pl-fe/toast';
|
||||
|
@ -18,6 +19,10 @@ const IMPORT_MUTES_REQUEST = 'IMPORT_MUTES_REQUEST' as const;
|
|||
const IMPORT_MUTES_SUCCESS = 'IMPORT_MUTES_SUCCESS' as const;
|
||||
const IMPORT_MUTES_FAIL = 'IMPORT_MUTES_FAIL' as const;
|
||||
|
||||
const IMPORT_ARCHIVE_REQUEST = 'IMPORT_ARCHIVE_REQUEST';
|
||||
const IMPORT_ARCHIVE_SUCCESS = 'IMPORT_ARCHIVE_SUCCESS';
|
||||
const IMPORT_ARCHIVE_FAIL = 'IMPORT_ARCHIVE_FAIL';
|
||||
|
||||
type ImportDataActions = {
|
||||
type: typeof IMPORT_FOLLOWS_REQUEST
|
||||
| typeof IMPORT_FOLLOWS_SUCCESS
|
||||
|
@ -27,7 +32,10 @@ type ImportDataActions = {
|
|||
| typeof IMPORT_BLOCKS_FAIL
|
||||
| typeof IMPORT_MUTES_REQUEST
|
||||
| typeof IMPORT_MUTES_SUCCESS
|
||||
| typeof IMPORT_MUTES_FAIL;
|
||||
| typeof IMPORT_MUTES_FAIL
|
||||
| typeof IMPORT_ARCHIVE_REQUEST
|
||||
| typeof IMPORT_ARCHIVE_SUCCESS
|
||||
| typeof IMPORT_ARCHIVE_FAIL;
|
||||
error?: any;
|
||||
response?: string;
|
||||
}
|
||||
|
@ -36,6 +44,7 @@ const messages = defineMessages({
|
|||
blocksSuccess: { id: 'import_data.success.blocks', defaultMessage: 'Blocks imported successfully' },
|
||||
followersSuccess: { id: 'import_data.success.followers', defaultMessage: 'Followers imported successfully' },
|
||||
mutesSuccess: { id: 'import_data.success.mutes', defaultMessage: 'Mutes imported successfully' },
|
||||
archiveSuccess: { id: 'import_data.success.archive', defaultMessage: 'Archive imported successfully' },
|
||||
});
|
||||
|
||||
const importFollows = (list: File | string, overwrite?: boolean) =>
|
||||
|
@ -71,6 +80,23 @@ const importMutes = (list: File | string) =>
|
|||
});
|
||||
};
|
||||
|
||||
const importArchive = (file: File) =>
|
||||
(dispatch: React.Dispatch<ImportDataActions>, getState: () => RootState) => {
|
||||
dispatch({ type: IMPORT_ARCHIVE_REQUEST });
|
||||
const form = serialize({ file, keep_unlisted: true }, { indices: true });
|
||||
return getClient(getState).request('/api/pleroma/archive_import', {
|
||||
method: 'POST',
|
||||
body: form,
|
||||
contentType: '',
|
||||
})
|
||||
.then(response => {
|
||||
toast.success(messages.archiveSuccess);
|
||||
dispatch({ type: IMPORT_ARCHIVE_SUCCESS, response: response.json });
|
||||
}).catch(error => {
|
||||
dispatch({ type: IMPORT_ARCHIVE_FAIL, error });
|
||||
});
|
||||
};
|
||||
|
||||
export {
|
||||
IMPORT_FOLLOWS_REQUEST,
|
||||
IMPORT_FOLLOWS_SUCCESS,
|
||||
|
@ -81,7 +107,11 @@ export {
|
|||
IMPORT_MUTES_REQUEST,
|
||||
IMPORT_MUTES_SUCCESS,
|
||||
IMPORT_MUTES_FAIL,
|
||||
IMPORT_ARCHIVE_REQUEST,
|
||||
IMPORT_ARCHIVE_SUCCESS,
|
||||
IMPORT_ARCHIVE_FAIL,
|
||||
importFollows,
|
||||
importBlocks,
|
||||
importMutes,
|
||||
importArchive,
|
||||
};
|
||||
|
|
|
@ -3,6 +3,7 @@ import * as v from 'valibot';
|
|||
import { Entities } from 'pl-fe/entity-store/entities';
|
||||
import { useEntity } from 'pl-fe/entity-store/hooks/useEntity';
|
||||
import { useClient } from 'pl-fe/hooks/useClient';
|
||||
import { useLoggedIn } from 'pl-fe/hooks/useLoggedIn';
|
||||
|
||||
import type { Relationship } from 'pl-api';
|
||||
|
||||
|
@ -12,13 +13,14 @@ interface UseRelationshipOpts {
|
|||
|
||||
const useRelationship = (accountId: string | undefined, opts: UseRelationshipOpts = {}) => {
|
||||
const client = useClient();
|
||||
const { isLoggedIn } = useLoggedIn();
|
||||
const { enabled = false } = opts;
|
||||
|
||||
const { entity: relationship, ...result } = useEntity<Relationship>(
|
||||
[Entities.RELATIONSHIPS, accountId!],
|
||||
() => client.accounts.getRelationships([accountId!]),
|
||||
{
|
||||
enabled: enabled && !!accountId,
|
||||
enabled: enabled && isLoggedIn && !!accountId,
|
||||
schema: v.pipe(v.any(), v.transform(arr => arr[0])),
|
||||
},
|
||||
);
|
||||
|
|
|
@ -1,14 +1,13 @@
|
|||
import { useMutation, useQuery } from '@tanstack/react-query';
|
||||
import {
|
||||
adminAnnouncementSchema,
|
||||
type AdminAnnouncement as BaseAdminAnnouncement,
|
||||
type AdminAnnouncement,
|
||||
type AdminCreateAnnouncementParams,
|
||||
type AdminUpdateAnnouncementParams,
|
||||
} from 'pl-api';
|
||||
import * as v from 'valibot';
|
||||
|
||||
import { useClient } from 'pl-fe/hooks/useClient';
|
||||
import { normalizeAnnouncement, AdminAnnouncement } from 'pl-fe/normalizers/announcement';
|
||||
import { queryClient } from 'pl-fe/queries/client';
|
||||
|
||||
import { useAnnouncements as useUserAnnouncements } from '../announcements/useAnnouncements';
|
||||
|
@ -20,7 +19,7 @@ const useAnnouncements = () => {
|
|||
const getAnnouncements = async () => {
|
||||
const data = await client.admin.announcements.getAnnouncements();
|
||||
|
||||
return data.items.map(normalizeAnnouncement<BaseAdminAnnouncement>);
|
||||
return data.items;
|
||||
};
|
||||
|
||||
const result = useQuery<ReadonlyArray<AdminAnnouncement>>({
|
||||
|
|
|
@ -1,9 +1,8 @@
|
|||
import { useMutation, useQuery } from '@tanstack/react-query';
|
||||
import { announcementReactionSchema, type AnnouncementReaction } from 'pl-api';
|
||||
import { announcementReactionSchema, type AnnouncementReaction, type Announcement } from 'pl-api';
|
||||
import * as v from 'valibot';
|
||||
|
||||
import { useClient } from 'pl-fe/hooks/useClient';
|
||||
import { type Announcement, normalizeAnnouncement } from 'pl-fe/normalizers/announcement';
|
||||
import { queryClient } from 'pl-fe/queries/client';
|
||||
|
||||
const updateReaction = (reaction: AnnouncementReaction, count: number, me?: boolean, overwrite?: boolean) => v.parse(announcementReactionSchema, {
|
||||
|
@ -25,14 +24,9 @@ const updateReactions = (reactions: AnnouncementReaction[], name: string, count:
|
|||
const useAnnouncements = () => {
|
||||
const client = useClient();
|
||||
|
||||
const getAnnouncements = async () => {
|
||||
const data = await client.announcements.getAnnouncements();
|
||||
return data.map(normalizeAnnouncement);
|
||||
};
|
||||
|
||||
const { data, ...result } = useQuery<ReadonlyArray<Announcement>>({
|
||||
queryKey: ['announcements'],
|
||||
queryFn: getAnnouncements,
|
||||
queryFn: () => client.announcements.getAnnouncements(),
|
||||
placeholderData: [],
|
||||
});
|
||||
|
||||
|
|
|
@ -160,7 +160,7 @@ const AccountHoverCard: React.FC<IAccountHoverCard> = ({ visible = true }) => {
|
|||
size='sm'
|
||||
className='mr-2 rtl:ml-2 rtl:mr-0 [&_br]:hidden [&_p:first-child]:inline [&_p:first-child]:truncate [&_p]:hidden'
|
||||
>
|
||||
<ParsedContent html={account.note_emojified} />
|
||||
<ParsedContent html={account.note} emojis={account.emojis} />
|
||||
</Text>
|
||||
)}
|
||||
</Stack>
|
||||
|
|
|
@ -11,6 +11,7 @@ import IconButton from 'pl-fe/components/ui/icon-button';
|
|||
import Stack from 'pl-fe/components/ui/stack';
|
||||
import Text from 'pl-fe/components/ui/text';
|
||||
import VerificationBadge from 'pl-fe/components/verification-badge';
|
||||
import Emojify from 'pl-fe/features/emoji/emojify';
|
||||
import ActionButton from 'pl-fe/features/ui/components/action-button';
|
||||
import { useAppSelector } from 'pl-fe/hooks/useAppSelector';
|
||||
import { getAcct } from 'pl-fe/utils/accounts';
|
||||
|
@ -219,8 +220,9 @@ const Account = ({
|
|||
size='sm'
|
||||
weight='semibold'
|
||||
truncate
|
||||
dangerouslySetInnerHTML={{ __html: account.display_name_html }}
|
||||
/>
|
||||
>
|
||||
<Emojify text={account.display_name} emojis={account.emojis} />
|
||||
</Text>
|
||||
|
||||
{account.verified && <VerificationBadge />}
|
||||
|
||||
|
@ -281,8 +283,9 @@ const Account = ({
|
|||
size='sm'
|
||||
weight='semibold'
|
||||
truncate
|
||||
dangerouslySetInnerHTML={{ __html: account.display_name_html }}
|
||||
/>
|
||||
>
|
||||
<Emojify text={account.display_name} emojis={account.emojis} />
|
||||
</Text>
|
||||
|
||||
{account.verified && <VerificationBadge />}
|
||||
|
||||
|
@ -356,7 +359,7 @@ const Account = ({
|
|||
truncate
|
||||
size='sm'
|
||||
>
|
||||
<ParsedContent html={account.note_emojified} />
|
||||
<ParsedContent html={account.note} emojis={account.emojis} />
|
||||
</Text>
|
||||
)}
|
||||
</Stack>
|
||||
|
|
|
@ -3,8 +3,9 @@ import { useHistory } from 'react-router-dom';
|
|||
|
||||
import { getTextDirection } from 'pl-fe/utils/rtl';
|
||||
|
||||
import type { Mention as MentionEntity } from 'pl-api';
|
||||
import type { Announcement } from 'pl-fe/normalizers/announcement';
|
||||
import { ParsedContent } from '../parsed-content';
|
||||
|
||||
import type { Announcement, Mention as MentionEntity } from 'pl-api';
|
||||
|
||||
interface IAnnouncementContent {
|
||||
announcement: Announcement;
|
||||
|
@ -83,8 +84,9 @@ const AnnouncementContent: React.FC<IAnnouncementContent> = ({ announcement }) =
|
|||
dir={direction}
|
||||
className='text-sm ltr:ml-0 rtl:mr-0'
|
||||
ref={node}
|
||||
dangerouslySetInnerHTML={{ __html: announcement.contentHtml }}
|
||||
/>
|
||||
>
|
||||
<ParsedContent html={announcement.content} emojis={announcement.emojis} />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
|
|
|
@ -10,8 +10,7 @@ import AnnouncementContent from './announcement-content';
|
|||
import ReactionsBar from './reactions-bar';
|
||||
|
||||
import type { Map as ImmutableMap } from 'immutable';
|
||||
import type { CustomEmoji } from 'pl-api';
|
||||
import type { Announcement as AnnouncementEntity } from 'pl-fe/normalizers/announcement';
|
||||
import type { Announcement as AnnouncementEntity, CustomEmoji } from 'pl-api';
|
||||
|
||||
interface IAnnouncement {
|
||||
announcement: AnnouncementEntity;
|
||||
|
|
|
@ -8,6 +8,7 @@ import HStack from 'pl-fe/components/ui/hstack';
|
|||
import Stack from 'pl-fe/components/ui/stack';
|
||||
import Text from 'pl-fe/components/ui/text';
|
||||
import VerificationBadge from 'pl-fe/components/verification-badge';
|
||||
import Emojify from 'pl-fe/features/emoji/emojify';
|
||||
import EventActionButton from 'pl-fe/features/event/components/event-action-button';
|
||||
import EventDate from 'pl-fe/features/event/components/event-date';
|
||||
import { useAppSelector } from 'pl-fe/hooks/useAppSelector';
|
||||
|
@ -71,7 +72,9 @@ const EventPreview: React.FC<IEventPreview> = ({ status, className, hideAction,
|
|||
<HStack alignItems='center' space={2}>
|
||||
<Icon src={require('@tabler/icons/outline/user.svg')} />
|
||||
<HStack space={1} alignItems='center' grow>
|
||||
<span dangerouslySetInnerHTML={{ __html: account.display_name_html }} />
|
||||
<span>
|
||||
<Emojify text={account.display_name} emojis={account.emojis} />
|
||||
</span>
|
||||
{account.verified && <VerificationBadge />}
|
||||
</HStack>
|
||||
</HStack>
|
||||
|
|
|
@ -3,6 +3,7 @@ import React from 'react';
|
|||
import HStack from 'pl-fe/components/ui/hstack';
|
||||
import Stack from 'pl-fe/components/ui/stack';
|
||||
import Text from 'pl-fe/components/ui/text';
|
||||
import Emojify from 'pl-fe/features/emoji/emojify';
|
||||
import GroupHeaderImage from 'pl-fe/features/group/components/group-header-image';
|
||||
import GroupMemberCount from 'pl-fe/features/group/components/group-member-count';
|
||||
import GroupPrivacy from 'pl-fe/features/group/components/group-privacy';
|
||||
|
@ -37,7 +38,9 @@ const GroupCard: React.FC<IGroupCard> = ({ group }) => (
|
|||
{/* Group Info */}
|
||||
<Stack alignItems='center' justifyContent='end' grow className='basis-1/2 py-4' space={0.5}>
|
||||
<HStack alignItems='center' space={1.5}>
|
||||
<Text size='lg' weight='bold' dangerouslySetInnerHTML={{ __html: group.display_name_html }} />
|
||||
<Text size='lg' weight='bold'>
|
||||
<Emojify text={group.display_name} emojis={group.emojis} />
|
||||
</Text>
|
||||
</HStack>
|
||||
|
||||
<HStack className='text-gray-700 dark:text-gray-600' space={2} wrap>
|
||||
|
|
|
@ -8,6 +8,7 @@ import HStack from 'pl-fe/components/ui/hstack';
|
|||
import Popover from 'pl-fe/components/ui/popover';
|
||||
import Stack from 'pl-fe/components/ui/stack';
|
||||
import Text from 'pl-fe/components/ui/text';
|
||||
import Emojify from 'pl-fe/features/emoji/emojify';
|
||||
import GroupMemberCount from 'pl-fe/features/group/components/group-member-count';
|
||||
import GroupPrivacy from 'pl-fe/features/group/components/group-privacy';
|
||||
|
||||
|
@ -71,7 +72,9 @@ const GroupPopover = (props: IGroupPopoverContainer) => {
|
|||
|
||||
{/* Group Info */}
|
||||
<Stack alignItems='center' justifyContent='end' grow className='basis-1/2 py-4' space={0.5}>
|
||||
<Text size='lg' weight='bold' dangerouslySetInnerHTML={{ __html: group.display_name_html }} />
|
||||
<Text size='lg' weight='bold'>
|
||||
<Emojify text={group.display_name} emojis={group.emojis} />
|
||||
</Text>
|
||||
|
||||
<HStack className='text-gray-700 dark:text-gray-600' space={2} wrap>
|
||||
<GroupPrivacy group={group} />
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
import React from 'react';
|
||||
import { Helmet as ReactHelmet } from 'react-helmet-async';
|
||||
|
||||
import { useStatContext } from 'pl-fe/contexts/stat-context';
|
||||
import { useAppSelector } from 'pl-fe/hooks/useAppSelector';
|
||||
import { useInstance } from 'pl-fe/hooks/useInstance';
|
||||
import { useSettings } from 'pl-fe/hooks/useSettings';
|
||||
|
@ -23,8 +22,7 @@ interface IHelmet {
|
|||
|
||||
const Helmet: React.FC<IHelmet> = ({ children }) => {
|
||||
const instance = useInstance();
|
||||
const { unreadChatsCount } = useStatContext();
|
||||
const unreadCount = useAppSelector((state) => getNotifTotals(state) + unreadChatsCount);
|
||||
const unreadCount = useAppSelector((state) => getNotifTotals(state));
|
||||
const { demetricator } = useSettings();
|
||||
|
||||
const hasUnreadNotifications = React.useMemo(() => !(unreadCount < 1 || demetricator), [unreadCount, demetricator]);
|
||||
|
|
134
packages/pl-fe/src/components/markup.css
Normal file
134
packages/pl-fe/src/components/markup.css
Normal file
|
@ -0,0 +1,134 @@
|
|||
[data-markup] {
|
||||
@apply whitespace-pre-wrap;
|
||||
}
|
||||
|
||||
[data-markup] h1 {
|
||||
@apply text-3xl font-semibold;
|
||||
}
|
||||
|
||||
[data-markup] h2 {
|
||||
@apply text-2xl font-semibold;
|
||||
}
|
||||
|
||||
[data-markup] h3 {
|
||||
@apply text-xl font-black;
|
||||
}
|
||||
|
||||
[data-markup] hr {
|
||||
@apply mb-4;
|
||||
}
|
||||
|
||||
[data-markup] p {
|
||||
@apply mb-4 whitespace-pre-wrap;
|
||||
}
|
||||
|
||||
[data-markup] p:last-child,
|
||||
[data-markup] hr:last-child {
|
||||
@apply mb-0;
|
||||
}
|
||||
|
||||
[data-markup] a {
|
||||
@apply text-primary-600 dark:text-accent-blue hover:underline;
|
||||
}
|
||||
|
||||
[data-markup] strong {
|
||||
@apply font-bold;
|
||||
}
|
||||
|
||||
[data-markup] em {
|
||||
@apply italic;
|
||||
}
|
||||
|
||||
[data-markup] ul,
|
||||
[data-markup] ol {
|
||||
@apply pl-10 mb-4;
|
||||
}
|
||||
|
||||
[data-markup] ul {
|
||||
@apply list-disc list-outside;
|
||||
}
|
||||
|
||||
[data-markup] ol {
|
||||
@apply list-decimal list-outside;
|
||||
}
|
||||
|
||||
[data-markup] blockquote {
|
||||
@apply py-1 pl-4 mb-4 border-l-4 border-solid border-gray-400 text-gray-500 dark:text-gray-400;
|
||||
}
|
||||
|
||||
[data-markup] table {
|
||||
@apply table-auto w-full bg-gray-200 dark:bg-gray-900 my-4 rounded-md;
|
||||
}
|
||||
|
||||
[data-markup] table th, table td {
|
||||
@apply text-center px-2;
|
||||
}
|
||||
|
||||
[data-markup] table th {
|
||||
@apply border-b-2 border-gray-600;
|
||||
}
|
||||
|
||||
[data-markup] code,
|
||||
[data-markup] pre {
|
||||
@apply cursor-text font-mono;
|
||||
}
|
||||
|
||||
[data-markup] p > code,
|
||||
[data-markup] pre {
|
||||
@apply bg-gray-100 dark:bg-primary-800;
|
||||
}
|
||||
|
||||
/* Inline code */
|
||||
[data-markup] p > code {
|
||||
@apply py-0.5 px-1 rounded-sm;
|
||||
}
|
||||
|
||||
/* Code block */
|
||||
[data-markup] pre {
|
||||
@apply py-2 px-3 mb-4 leading-6 overflow-x-auto rounded-md break-all;
|
||||
}
|
||||
|
||||
[data-markup] pre:last-child {
|
||||
@apply mb-0;
|
||||
}
|
||||
|
||||
/* Emojis */
|
||||
[data-markup] img.emojione {
|
||||
@apply w-5 h-5 m-0;
|
||||
}
|
||||
|
||||
/* Markdown inline images (Pleroma) */
|
||||
[data-markup] img:not(.emojione) {
|
||||
@apply max-h-[500px] mx-auto rounded-sm;
|
||||
}
|
||||
|
||||
/* User setting to underline links */
|
||||
body.underline-links [data-markup] a {
|
||||
@apply underline;
|
||||
}
|
||||
|
||||
[data-markup].big-emoji img.emojione {
|
||||
@apply inline w-9 h-9 p-1;
|
||||
}
|
||||
|
||||
[data-markup] .status-link {
|
||||
@apply hover:underline text-primary-600 dark:text-accent-blue hover:text-primary-800 dark:hover:text-accent-blue;
|
||||
}
|
||||
|
||||
[data-markup] .invisible {
|
||||
font-size: 0 !important;
|
||||
line-height: 0 !important;
|
||||
display: inline-block;
|
||||
width: 0;
|
||||
height: 0;
|
||||
position: absolute;
|
||||
}
|
||||
|
||||
[data-markup] .invisible img,
|
||||
[data-markup] .invisible svg {
|
||||
margin: 0 !important;
|
||||
border: 0 !important;
|
||||
padding: 0 !important;
|
||||
width: 0 !important;
|
||||
height: 0 !important;
|
||||
}
|
|
@ -289,6 +289,7 @@ const MediaGallery: React.FC<IMediaGallery> = (props) => {
|
|||
height,
|
||||
visible,
|
||||
} = props;
|
||||
|
||||
const [width, setWidth] = useState<number>(defaultWidth);
|
||||
|
||||
const node = useRef<HTMLDivElement>(null);
|
||||
|
|
|
@ -3,11 +3,14 @@ import DOMPurify from 'isomorphic-dompurify';
|
|||
import React, { useMemo } from 'react';
|
||||
import { Link } from 'react-router-dom';
|
||||
|
||||
import Emojify from 'pl-fe/features/emoji/emojify';
|
||||
import { makeEmojiMap } from 'pl-fe/utils/normalizers';
|
||||
|
||||
import HashtagLink from './hashtag-link';
|
||||
import HoverAccountWrapper from './hover-account-wrapper';
|
||||
import StatusMention from './status-mention';
|
||||
|
||||
import type { Mention } from 'pl-api';
|
||||
import type { CustomEmoji, Mention } from 'pl-api';
|
||||
|
||||
const nodesToText = (nodes: Array<DOMNode>): string =>
|
||||
nodes.map(node => node.type === 'text' ? node.data : node.type === 'tag' ? nodesToText(node.children as Array<DOMNode>) : '').join('');
|
||||
|
@ -19,14 +22,18 @@ interface IParsedContent {
|
|||
mentions?: Array<Mention>;
|
||||
/** Whether it's a status which has a quote. */
|
||||
hasQuote?: boolean;
|
||||
/** Related custom emojis. */
|
||||
emojis?: Array<CustomEmoji>;
|
||||
}
|
||||
|
||||
const ParsedContent: React.FC<IParsedContent> = (({ html, mentions, hasQuote }) => {
|
||||
const ParsedContent: React.FC<IParsedContent> = (({ html, mentions, hasQuote, emojis }) => {
|
||||
return useMemo(() => {
|
||||
if (html.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const emojiMap = emojis ? makeEmojiMap(emojis) : undefined;
|
||||
|
||||
const selectors: Array<string> = [];
|
||||
|
||||
// Explicit mentions
|
||||
|
@ -99,6 +106,14 @@ const ParsedContent: React.FC<IParsedContent> = (({ html, mentions, hasQuote })
|
|||
return fallback;
|
||||
}
|
||||
},
|
||||
|
||||
transform(reactNode) {
|
||||
if (typeof reactNode === 'string') {
|
||||
return <Emojify text={reactNode} emojis={emojiMap} />;
|
||||
}
|
||||
|
||||
return reactNode as JSX.Element;
|
||||
},
|
||||
};
|
||||
|
||||
return parse(DOMPurify.sanitize(html, { ADD_ATTR: ['target'], USE_PROFILES: { html: true } }), options);
|
||||
|
|
|
@ -12,7 +12,7 @@ import { useAppDispatch } from 'pl-fe/hooks/useAppDispatch';
|
|||
import RelativeTimestamp from '../relative-timestamp';
|
||||
|
||||
import type { Selected } from './poll';
|
||||
import type { Poll } from 'pl-fe/normalizers/poll';
|
||||
import type { Poll } from 'pl-api';
|
||||
|
||||
const messages = defineMessages({
|
||||
closed: { id: 'poll.closed', defaultMessage: 'Closed' },
|
||||
|
|
|
@ -7,7 +7,9 @@ import HStack from 'pl-fe/components/ui/hstack';
|
|||
import Icon from 'pl-fe/components/ui/icon';
|
||||
import Text from 'pl-fe/components/ui/text';
|
||||
|
||||
import type { Poll } from 'pl-fe/normalizers/poll';
|
||||
import { ParsedContent } from '../parsed-content';
|
||||
|
||||
import type { Poll } from 'pl-api';
|
||||
|
||||
const messages = defineMessages({
|
||||
voted: { id: 'poll.voted', defaultMessage: 'You voted for this answer' },
|
||||
|
@ -65,8 +67,9 @@ const PollOptionText: React.FC<IPollOptionText> = ({ poll, option, index, active
|
|||
theme='inherit'
|
||||
weight='medium'
|
||||
align='center'
|
||||
dangerouslySetInnerHTML={{ __html: option.title_emojified }}
|
||||
/>
|
||||
>
|
||||
<ParsedContent html={option.title} emojis={poll.emojis} />
|
||||
</Text>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
@ -133,9 +136,10 @@ const PollOption: React.FC<IPollOption> = (props): JSX.Element | null => {
|
|||
<Text
|
||||
theme='inherit'
|
||||
weight='medium'
|
||||
dangerouslySetInnerHTML={{ __html: (language && option.title_map_emojified) && option.title_map_emojified[language] || option.title_emojified }}
|
||||
className='relative'
|
||||
/>
|
||||
>
|
||||
<ParsedContent html={(language && option.title_map) && option.title_map[language] || option.title} emojis={poll.emojis} />
|
||||
</Text>
|
||||
</div>
|
||||
|
||||
<HStack space={2} alignItems='center' className='relative'>
|
||||
|
|
|
@ -3,8 +3,6 @@ import { defineMessages, FormattedMessage, useIntl } from 'react-intl';
|
|||
|
||||
import Icon from 'pl-fe/components/ui/icon';
|
||||
import Stack from 'pl-fe/components/ui/stack';
|
||||
import { useStatContext } from 'pl-fe/contexts/stat-context';
|
||||
import Search from 'pl-fe/features/search/components/search';
|
||||
import ComposeButton from 'pl-fe/features/ui/components/compose-button';
|
||||
import ProfileDropdown from 'pl-fe/features/ui/components/profile-dropdown';
|
||||
import { useAppSelector } from 'pl-fe/hooks/useAppSelector';
|
||||
|
@ -36,7 +34,6 @@ const messages = defineMessages({
|
|||
/** Desktop sidebar with links to different views in the app. */
|
||||
const SidebarNavigation = () => {
|
||||
const intl = useIntl();
|
||||
const { unreadChatsCount } = useStatContext();
|
||||
|
||||
const instance = useInstance();
|
||||
const features = useFeatures();
|
||||
|
@ -163,9 +160,6 @@ const SidebarNavigation = () => {
|
|||
/>
|
||||
</ProfileDropdown>
|
||||
</div>
|
||||
<div className='block w-full max-w-xs'>
|
||||
<Search openInRoute autosuggest />
|
||||
</div>
|
||||
</Stack>
|
||||
)}
|
||||
|
||||
|
@ -193,16 +187,6 @@ const SidebarNavigation = () => {
|
|||
text={<FormattedMessage id='tabs_bar.notifications' defaultMessage='Notifications' />}
|
||||
/>
|
||||
|
||||
{features.chats && (
|
||||
<SidebarNavigationLink
|
||||
to='/chats'
|
||||
icon={require('@tabler/icons/outline/messages.svg')}
|
||||
count={unreadChatsCount}
|
||||
countMax={9}
|
||||
text={<FormattedMessage id='navigation.chats' defaultMessage='Chats' />}
|
||||
/>
|
||||
)}
|
||||
|
||||
{!features.chats && features.conversations && (
|
||||
<SidebarNavigationLink
|
||||
to='/conversations'
|
||||
|
|
|
@ -5,7 +5,7 @@ import { useHistory, useRouteMatch } from 'react-router-dom';
|
|||
|
||||
import { blockAccount } from 'pl-fe/actions/accounts';
|
||||
import { directCompose, mentionCompose, quoteCompose, replyCompose } from 'pl-fe/actions/compose';
|
||||
import { emojiReact } from 'pl-fe/actions/emoji-reacts';
|
||||
import { emojiReact, unEmojiReact } from 'pl-fe/actions/emoji-reacts';
|
||||
import { editEvent } from 'pl-fe/actions/events';
|
||||
import { toggleBookmark, toggleDislike, toggleFavourite, togglePin, toggleReblog } from 'pl-fe/actions/interactions';
|
||||
import { deleteStatusModal, toggleStatusSensitivityModal } from 'pl-fe/actions/moderation';
|
||||
|
@ -104,6 +104,7 @@ const messages = defineMessages({
|
|||
unmuteConversation: { id: 'status.unmute_conversation', defaultMessage: 'Unmute conversation' },
|
||||
unpin: { id: 'status.unpin', defaultMessage: 'Unpin from profile' },
|
||||
viewReactions: { id: 'status.view_reactions', defaultMessage: 'View reactions' },
|
||||
wrench: { id: 'status.wrench', defaultMessage: 'Wrench reaction' },
|
||||
addKnownLanguage: { id: 'status.add_known_language', defaultMessage: 'Do not auto-translate posts in {language}.' },
|
||||
translate: { id: 'status.translate', defaultMessage: 'Translate' },
|
||||
hideTranslation: { id: 'status.hide_translation', defaultMessage: 'Hide translation' },
|
||||
|
@ -143,7 +144,9 @@ const StatusActionBar: React.FC<IStatusActionBar> = ({
|
|||
const { groupRelationship } = useGroupRelationship(status.group_id || undefined);
|
||||
const features = useFeatures();
|
||||
const instance = useInstance();
|
||||
const { autoTranslate, boostModal, deleteModal, knownLanguages } = useSettings();
|
||||
const { autoTranslate, boostModal, deleteModal, knownLanguages, showWrenchButton } = useSettings();
|
||||
|
||||
const wrenches = showWrenchButton && status.emoji_reactions.find(emoji => emoji.name === '🔧') || undefined;
|
||||
|
||||
const { translationLanguages } = useTranslationLanguages();
|
||||
|
||||
|
@ -153,7 +156,7 @@ const StatusActionBar: React.FC<IStatusActionBar> = ({
|
|||
allow_unauthenticated: allowUnauthenticated,
|
||||
} = instance.pleroma.metadata.translation;
|
||||
|
||||
const renderTranslate = (me || allowUnauthenticated) && (allowRemote || status.account.local) && ['public', 'unlisted'].includes(status.visibility) && status.contentHtml.length > 0 && status.language !== null && !knownLanguages.includes(status.language);
|
||||
const renderTranslate = (me || allowUnauthenticated) && (allowRemote || status.account.local) && ['public', 'unlisted'].includes(status.visibility) && status.content.length > 0 && status.language !== null && !knownLanguages.includes(status.language);
|
||||
const supportsLanguages = (translationLanguages[status.language!]?.includes(intl.locale));
|
||||
|
||||
return autoTranslate && features.translations && renderTranslate && supportsLanguages;
|
||||
|
@ -211,10 +214,24 @@ const StatusActionBar: React.FC<IStatusActionBar> = ({
|
|||
}
|
||||
};
|
||||
|
||||
const handleWrenchClick: React.EventHandler<React.MouseEvent> = (e) => {
|
||||
if (!me) {
|
||||
onOpenUnauthorizedModal('DISLIKE');
|
||||
} else if (wrenches?.me) {
|
||||
dispatch(unEmojiReact(status, '🔧'));
|
||||
} else {
|
||||
dispatch(emojiReact(status, '🔧'));
|
||||
}
|
||||
};
|
||||
|
||||
const handleDislikeLongPress = status.dislikes_count ? () => {
|
||||
openModal('DISLIKES', { statusId: status.id });
|
||||
} : undefined;
|
||||
|
||||
const handleWrenchLongPress = wrenches?.count ? () => {
|
||||
openModal('REACTIONS', { statusId: status.id, reaction: wrenches.name });
|
||||
} : undefined;
|
||||
|
||||
const handlePickEmoji = (emoji: EmojiType) => {
|
||||
dispatch(emojiReact(status, emoji.custom ? emoji.id : emoji.native, emoji.custom ? emoji.imageUrl : undefined));
|
||||
};
|
||||
|
@ -784,6 +801,20 @@ const StatusActionBar: React.FC<IStatusActionBar> = ({
|
|||
/>
|
||||
)}
|
||||
|
||||
{me && !withLabels && features.emojiReacts && showWrenchButton && (
|
||||
<StatusActionButton
|
||||
title={intl.formatMessage(messages.wrench)}
|
||||
icon={require('@tabler/icons/outline/tool.svg')}
|
||||
color='accent'
|
||||
filled
|
||||
onClick={handleWrenchClick}
|
||||
onLongPress={handleWrenchLongPress}
|
||||
active={wrenches?.me}
|
||||
count={wrenches?.count || undefined}
|
||||
theme={statusActionButtonTheme}
|
||||
/>
|
||||
)}
|
||||
|
||||
{me && !withLabels && features.emojiReacts && (
|
||||
<EmojiPickerDropdown
|
||||
onPickEmoji={handlePickEmoji}
|
||||
|
|
|
@ -1,14 +1,11 @@
|
|||
import clsx from 'clsx';
|
||||
import React, { useState, useRef, useLayoutEffect, useMemo, useEffect } from 'react';
|
||||
import React, { useState, useRef, useLayoutEffect } from 'react';
|
||||
import { FormattedMessage } from 'react-intl';
|
||||
|
||||
import { collapseStatusSpoiler, expandStatusSpoiler } from 'pl-fe/actions/statuses';
|
||||
import Icon from 'pl-fe/components/icon';
|
||||
import Button from 'pl-fe/components/ui/button';
|
||||
import Stack from 'pl-fe/components/ui/stack';
|
||||
import Text from 'pl-fe/components/ui/text';
|
||||
import { useAppDispatch } from 'pl-fe/hooks/useAppDispatch';
|
||||
import { useSettings } from 'pl-fe/hooks/useSettings';
|
||||
import Emojify from 'pl-fe/features/emoji/emojify';
|
||||
import { onlyEmoji as isOnlyEmoji } from 'pl-fe/utils/rich-content';
|
||||
|
||||
import { getTextDirection } from '../utils/rtl';
|
||||
|
@ -63,15 +60,12 @@ const StatusContent: React.FC<IStatusContent> = React.memo(({
|
|||
textSize = 'md',
|
||||
quote = false,
|
||||
}) => {
|
||||
const dispatch = useAppDispatch();
|
||||
const { displaySpoilers } = useSettings();
|
||||
|
||||
const [collapsed, setCollapsed] = useState(false);
|
||||
const [onlyEmoji, setOnlyEmoji] = useState(false);
|
||||
const [lineClamp, setLineClamp] = useState(true);
|
||||
const [isTranslationEqual, setIsTranslationEqual] = useState(false);
|
||||
|
||||
const node = useRef<HTMLDivElement>(null);
|
||||
const spoilerNode = useRef<HTMLSpanElement>(null);
|
||||
const translationNode = useRef<HTMLDivElement>(null);
|
||||
|
||||
const maybeSetCollapsed = (): void => {
|
||||
if (!node.current) return;
|
||||
|
@ -93,37 +87,26 @@ const StatusContent: React.FC<IStatusContent> = React.memo(({
|
|||
}
|
||||
};
|
||||
|
||||
const toggleExpanded: React.MouseEventHandler<HTMLButtonElement> = (e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
|
||||
if (expanded) dispatch(collapseStatusSpoiler(status.id));
|
||||
else dispatch(expandStatusSpoiler(status.id));
|
||||
};
|
||||
|
||||
useLayoutEffect(() => {
|
||||
maybeSetCollapsed();
|
||||
maybeSetOnlyEmoji();
|
||||
});
|
||||
|
||||
const parsedHtml = useMemo(
|
||||
(): string => translatable && status.translation
|
||||
? status.translation.content!
|
||||
: (status.contentMapHtml && status.currentLanguage)
|
||||
? (status.contentMapHtml[status.currentLanguage] || status.contentHtml)
|
||||
: status.contentHtml,
|
||||
[status.contentHtml, status.translation, status.currentLanguage],
|
||||
);
|
||||
useLayoutEffect(() => {
|
||||
setIsTranslationEqual(node.current?.innerText.trim().replace(/\s+/g, ' ') === translationNode.current?.innerText.trim().replace(/\s+/g, ' '));
|
||||
}, [status.translation]);
|
||||
|
||||
useEffect(() => {
|
||||
setLineClamp(!spoilerNode.current || spoilerNode.current.clientHeight >= 96);
|
||||
}, [spoilerNode.current]);
|
||||
const content = (status.content_map && status.currentLanguage)
|
||||
? status.content_map[status.currentLanguage] || status.content
|
||||
: status.content;
|
||||
|
||||
const withSpoiler = status.spoiler_text.length > 0;
|
||||
const translationContent = translatable && status.translation && !isTranslationEqual ? status.translation.content : null;
|
||||
|
||||
const spoilerText = status.spoilerMapHtml && status.currentLanguage
|
||||
? status.spoilerMapHtml[status.currentLanguage] || status.spoilerHtml
|
||||
: status.spoilerHtml;
|
||||
const baseClassName = 'text-gray-900 dark:text-gray-100 break-words text-ellipsis overflow-hidden relative focus:outline-none';
|
||||
|
||||
const spoilerText = status.spoiler_text_map && status.currentLanguage
|
||||
? status.spoiler_text_map[status.currentLanguage] || status.spoiler_text
|
||||
: status.spoiler_text;
|
||||
|
||||
const direction = getTextDirection(status.search_index);
|
||||
const className = clsx('relative text-ellipsis break-words text-gray-900 focus:outline-none dark:text-gray-100', {
|
||||
|
@ -134,42 +117,21 @@ const StatusContent: React.FC<IStatusContent> = React.memo(({
|
|||
'leading-normal big-emoji': onlyEmoji,
|
||||
});
|
||||
|
||||
const expandable = !displaySpoilers;
|
||||
const expanded = !withSpoiler || status.expanded || false;
|
||||
|
||||
const output = [];
|
||||
|
||||
if (spoilerText) {
|
||||
output.push(
|
||||
<Text key='spoiler' size='2xl' weight='medium'>
|
||||
<span
|
||||
className={clsx({ 'line-clamp-3': !expanded && lineClamp })}
|
||||
dangerouslySetInnerHTML={{ __html: spoilerText }}
|
||||
ref={spoilerNode}
|
||||
/>
|
||||
{status.content && expandable && (
|
||||
<Button
|
||||
className='ml-2 align-middle'
|
||||
type='button'
|
||||
theme='muted'
|
||||
size='xs'
|
||||
onClick={toggleExpanded}
|
||||
icon={expanded ? require('@tabler/icons/outline/chevron-up.svg') : require('@tabler/icons/outline/chevron-down.svg')}
|
||||
>
|
||||
{expanded
|
||||
? <FormattedMessage id='status.spoiler.collapse' defaultMessage='Collapse' />
|
||||
: <FormattedMessage id='status.spoiler.expand' defaultMessage='Expand' />}
|
||||
</Button>
|
||||
)}
|
||||
<span>
|
||||
<Emojify text={spoilerText} emojis={status.emojis} />
|
||||
</span>
|
||||
</Text>,
|
||||
);
|
||||
}
|
||||
|
||||
if (expandable && !expanded) return <>{output}</>;
|
||||
|
||||
if (onClick) {
|
||||
if (status.content) {
|
||||
output.push(
|
||||
if (content) {
|
||||
const body = (
|
||||
<Markup
|
||||
ref={node}
|
||||
tabIndex={0}
|
||||
|
@ -179,14 +141,39 @@ const StatusContent: React.FC<IStatusContent> = React.memo(({
|
|||
lang={status.language || undefined}
|
||||
size={textSize}
|
||||
>
|
||||
<ParsedContent html={parsedHtml} mentions={status.mentions} hasQuote={!!status.quote_id} />
|
||||
</Markup>,
|
||||
<ParsedContent html={content} mentions={status.mentions} hasQuote={!!status.quote_id} emojis={status.emojis} />
|
||||
</Markup>
|
||||
);
|
||||
|
||||
if (translationContent && !isTranslationEqual) {
|
||||
output.push(
|
||||
<div className='flex flex-col gap-2 sm:flex-row'>
|
||||
<div className='break-word-nested w-full grow'>
|
||||
{body}
|
||||
</div>
|
||||
<hr className='sm:hidden' />
|
||||
<div className='break-word-nested w-full grow'>
|
||||
<Markup
|
||||
ref={translationNode}
|
||||
tabIndex={0}
|
||||
className={className}
|
||||
direction={direction}
|
||||
lang={status.language || undefined}
|
||||
size={textSize}
|
||||
>
|
||||
<ParsedContent html={translationContent} mentions={status.mentions} hasQuote={!!status.quote_id} />
|
||||
</Markup>
|
||||
</div>
|
||||
</div>,
|
||||
);
|
||||
} else {
|
||||
output.push(body);
|
||||
}
|
||||
}
|
||||
|
||||
const hasPoll = !!status.poll_id;
|
||||
|
||||
if (collapsed) {
|
||||
if (content && collapsed) {
|
||||
output.push(<ReadMoreButton onClick={onClick} key='read-more' quote={quote} poll={hasPoll} />);
|
||||
}
|
||||
|
||||
|
@ -196,8 +183,8 @@ const StatusContent: React.FC<IStatusContent> = React.memo(({
|
|||
|
||||
return <Stack space={4} className={clsx({ 'bg-gray-100 dark:bg-primary-800 rounded-md p-4': hasPoll })}>{output}</Stack>;
|
||||
} else {
|
||||
if (status.content) {
|
||||
output.push(
|
||||
if (content) {
|
||||
const body = (
|
||||
<Markup
|
||||
ref={node}
|
||||
tabIndex={0}
|
||||
|
@ -207,13 +194,40 @@ const StatusContent: React.FC<IStatusContent> = React.memo(({
|
|||
lang={status.language || undefined}
|
||||
size={textSize}
|
||||
>
|
||||
<ParsedContent html={parsedHtml} mentions={status.mentions} hasQuote={!!status.quote_id} />
|
||||
</Markup>,
|
||||
<ParsedContent html={content} mentions={status.mentions} hasQuote={!!status.quote_id} emojis={status.emojis} />
|
||||
</Markup>
|
||||
);
|
||||
}
|
||||
|
||||
if (collapsed) {
|
||||
output.push(<ReadMoreButton onClick={() => {}} key='read-more' quote={quote} />);
|
||||
if (translationContent && !isTranslationEqual) {
|
||||
output.push(
|
||||
<div className='flex flex-col gap-2 sm:flex-row'>
|
||||
<div className='break-word-nested w-full grow'>
|
||||
{body}
|
||||
</div>
|
||||
<hr className='sm:hidden' />
|
||||
<div className='break-word-nested w-full grow'>
|
||||
<Markup
|
||||
ref={translationNode}
|
||||
tabIndex={0}
|
||||
key='translated_content'
|
||||
className={clsx(baseClassName, {
|
||||
'leading-normal big-emoji': onlyEmoji,
|
||||
})}
|
||||
direction={direction}
|
||||
size={textSize}
|
||||
>
|
||||
<ParsedContent html={translationContent} mentions={status.mentions} hasQuote={!!status.quote_id} />
|
||||
</Markup>
|
||||
</div>
|
||||
</div>,
|
||||
);
|
||||
} else {
|
||||
output.push(body);
|
||||
}
|
||||
|
||||
if (collapsed) {
|
||||
output.push(<ReadMoreButton onClick={() => {}} key='read-more' quote={quote} />);
|
||||
}
|
||||
}
|
||||
|
||||
if (status.poll_id) {
|
||||
|
|
|
@ -17,7 +17,7 @@ const messages = defineMessages({
|
|||
});
|
||||
|
||||
interface IStatusLanguagePicker {
|
||||
status: Pick<Status, 'id' | 'contentMapHtml' | 'currentLanguage'>;
|
||||
status: Pick<Status, 'id' | 'content_map' | 'currentLanguage'>;
|
||||
showLabel?: boolean;
|
||||
}
|
||||
|
||||
|
@ -25,7 +25,7 @@ const StatusLanguagePicker: React.FC<IStatusLanguagePicker> = ({ status, showLab
|
|||
const intl = useIntl();
|
||||
const dispatch = useAppDispatch();
|
||||
|
||||
if (!status.contentMapHtml || Object.keys(status.contentMapHtml).length < 2) return null;
|
||||
if (!status.content_map || Object.keys(status.content_map).length < 2) return null;
|
||||
|
||||
const icon = <Icon className='size-4 text-gray-700 dark:text-gray-600' src={require('@tabler/icons/outline/language.svg')} />;
|
||||
|
||||
|
@ -34,7 +34,7 @@ const StatusLanguagePicker: React.FC<IStatusLanguagePicker> = ({ status, showLab
|
|||
<Text tag='span' theme='muted' size='sm'>·</Text>
|
||||
|
||||
<DropdownMenu
|
||||
items={Object.keys(status.contentMapHtml).map((language) => ({
|
||||
items={Object.keys(status.content_map).map((language) => ({
|
||||
text: languages[language as Language] || language,
|
||||
action: () => dispatch(changeStatusLanguage(status.id, language)),
|
||||
active: language === status.currentLanguage,
|
||||
|
|
|
@ -12,6 +12,7 @@ import Icon from 'pl-fe/components/ui/icon';
|
|||
import Stack from 'pl-fe/components/ui/stack';
|
||||
import Text from 'pl-fe/components/ui/text';
|
||||
import AccountContainer from 'pl-fe/containers/account-container';
|
||||
import Emojify from 'pl-fe/features/emoji/emojify';
|
||||
import StatusTypeIcon from 'pl-fe/features/status/components/status-type-icon';
|
||||
import QuotedStatus from 'pl-fe/features/status/containers/quoted-status-container';
|
||||
import { HotKeys } from 'pl-fe/features/ui/components/hotkeys';
|
||||
|
@ -204,23 +205,17 @@ const Status: React.FC<IStatus> = (props) => {
|
|||
className='hover:underline'
|
||||
>
|
||||
<bdi className='truncate'>
|
||||
<strong
|
||||
className='text-gray-800 dark:text-gray-200'
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: status.account.display_name_html,
|
||||
}}
|
||||
/>
|
||||
<strong className='text-gray-800 dark:text-gray-200'>
|
||||
<Emojify text={status.account.display_name} emojis={status.account.emojis} />
|
||||
</strong>
|
||||
</bdi>
|
||||
</Link>
|
||||
),
|
||||
group: (
|
||||
<Link to={`/groups/${group.id}`} className='hover:underline'>
|
||||
<strong
|
||||
className='text-gray-800 dark:text-gray-200'
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: group.display_name_html,
|
||||
}}
|
||||
/>
|
||||
<strong className='text-gray-800 dark:text-gray-200'>
|
||||
<Emojify text={group.display_name} emojis={group.emojis} />
|
||||
</strong>
|
||||
</Link>
|
||||
),
|
||||
}}
|
||||
|
@ -234,12 +229,9 @@ const Status: React.FC<IStatus> = (props) => {
|
|||
const renderedAccounts = accounts.slice(0, 2).map(account => !!account && (
|
||||
<Link key={account.acct} to={`/@${account.acct}`} className='hover:underline'>
|
||||
<bdi className='truncate'>
|
||||
<strong
|
||||
className='text-gray-800 dark:text-gray-200'
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: account.display_name_html,
|
||||
}}
|
||||
/>
|
||||
<strong className='text-gray-800 dark:text-gray-200'>
|
||||
<Emojify text={status.account.display_name} emojis={status.account.emojis} />
|
||||
</strong>
|
||||
</bdi>
|
||||
</Link>
|
||||
));
|
||||
|
@ -294,7 +286,7 @@ const Status: React.FC<IStatus> = (props) => {
|
|||
<Link to={`/groups/${group.id}`} className='hover:underline'>
|
||||
<bdi className='truncate'>
|
||||
<strong className='text-gray-800 dark:text-gray-200'>
|
||||
<span dangerouslySetInnerHTML={{ __html: group.display_name_html }} />
|
||||
<Emojify text={group.display_name} emojis={group.emojis} />
|
||||
</strong>
|
||||
</bdi>
|
||||
</Link>
|
||||
|
|
|
@ -5,11 +5,9 @@ import { useRouteMatch } from 'react-router-dom';
|
|||
import { groupComposeModal } from 'pl-fe/actions/compose';
|
||||
import ThumbNavigationLink from 'pl-fe/components/thumb-navigation-link';
|
||||
import Icon from 'pl-fe/components/ui/icon';
|
||||
import { useStatContext } from 'pl-fe/contexts/stat-context';
|
||||
import { Entities } from 'pl-fe/entity-store/entities';
|
||||
import { useAppDispatch } from 'pl-fe/hooks/useAppDispatch';
|
||||
import { useAppSelector } from 'pl-fe/hooks/useAppSelector';
|
||||
import { useFeatures } from 'pl-fe/hooks/useFeatures';
|
||||
import { useOwnAccount } from 'pl-fe/hooks/useOwnAccount';
|
||||
import { useModalsStore } from 'pl-fe/stores/modals';
|
||||
import { useUiStore } from 'pl-fe/stores/ui';
|
||||
|
@ -28,13 +26,11 @@ const ThumbNavigation: React.FC = (): JSX.Element => {
|
|||
const intl = useIntl();
|
||||
const dispatch = useAppDispatch();
|
||||
const { account } = useOwnAccount();
|
||||
const features = useFeatures();
|
||||
|
||||
const match = useRouteMatch<{ groupId: string }>('/groups/:groupId');
|
||||
|
||||
const { openSidebar } = useUiStore();
|
||||
const { openModal } = useModalsStore();
|
||||
const { unreadChatsCount } = useStatContext();
|
||||
|
||||
const standalone = useAppSelector(isStandalone);
|
||||
const notificationCount = useAppSelector((state) => state.notifications.unread);
|
||||
|
@ -94,7 +90,7 @@ const ThumbNavigation: React.FC = (): JSX.Element => {
|
|||
/>
|
||||
)} */}
|
||||
|
||||
{account && !features.chats && composeButton}
|
||||
{account && composeButton}
|
||||
|
||||
{(!standalone || account) && (
|
||||
<ThumbNavigationLink
|
||||
|
@ -115,21 +111,6 @@ const ThumbNavigation: React.FC = (): JSX.Element => {
|
|||
count={notificationCount}
|
||||
/>
|
||||
)}
|
||||
|
||||
{account && features.chats && (
|
||||
<>
|
||||
<ThumbNavigationLink
|
||||
src={require('@tabler/icons/outline/messages.svg')}
|
||||
text={intl.formatMessage(messages.chats)}
|
||||
to='/chats'
|
||||
exact
|
||||
count={unreadChatsCount}
|
||||
countMax={9}
|
||||
/>
|
||||
|
||||
{composeButton}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -16,7 +16,7 @@ import { useSettings } from 'pl-fe/hooks/useSettings';
|
|||
import type { Status } from 'pl-fe/normalizers/status';
|
||||
|
||||
interface ITranslateButton {
|
||||
status: Pick<Status, 'id' | 'account' | 'contentHtml' | 'contentMapHtml' | 'language' | 'translating' | 'translation' | 'visibility'>;
|
||||
status: Pick<Status, 'id' | 'account' | 'content' | 'content_map' | 'language' | 'translating' | 'translation' | 'visibility'>;
|
||||
}
|
||||
|
||||
const TranslateButton: React.FC<ITranslateButton> = ({ status }) => {
|
||||
|
@ -36,7 +36,7 @@ const TranslateButton: React.FC<ITranslateButton> = ({ status }) => {
|
|||
allow_unauthenticated: allowUnauthenticated,
|
||||
} = instance.pleroma.metadata.translation;
|
||||
|
||||
const renderTranslate = (me || allowUnauthenticated) && (allowRemote || status.account.local) && ['public', 'unlisted'].includes(status.visibility) && status.contentHtml.length > 0 && status.language !== null && intl.locale !== status.language && !status.contentMapHtml?.[intl.locale];
|
||||
const renderTranslate = (me || allowUnauthenticated) && (allowRemote || status.account.local) && ['public', 'unlisted'].includes(status.visibility) && status.content.length > 0 && status.language !== null && intl.locale !== status.language && !status.content_map?.[intl.locale];
|
||||
|
||||
const supportsLanguages = (translationLanguages[status.language!]?.includes(intl.locale));
|
||||
|
||||
|
@ -64,7 +64,7 @@ const TranslateButton: React.FC<ITranslateButton> = ({ status }) => {
|
|||
<Icon src={require('@tabler/icons/outline/language.svg')} className='size-4' />
|
||||
<span>
|
||||
{status.translation ? (
|
||||
<FormattedMessage id='status.show_original' defaultMessage='Show original' />
|
||||
<FormattedMessage id='status.hide_translation' defaultMessage='Hide translation' />
|
||||
) : status.translating ? (
|
||||
<FormattedMessage id='status.translating' defaultMessage='Translating…' />
|
||||
) : (
|
||||
|
|
|
@ -5,6 +5,7 @@ import Account from 'pl-fe/components/account';
|
|||
import Icon from 'pl-fe/components/icon';
|
||||
import HStack from 'pl-fe/components/ui/hstack';
|
||||
import Text from 'pl-fe/components/ui/text';
|
||||
import Emojify from 'pl-fe/features/emoji/emojify';
|
||||
|
||||
import type { Account as AccountEntity } from 'pl-fe/normalizers/account';
|
||||
|
||||
|
@ -27,7 +28,7 @@ const MovedNote: React.FC<IMovedNote> = ({ from, to }) => (
|
|||
id='notification.move'
|
||||
defaultMessage='{name} moved to {targetName}'
|
||||
values={{
|
||||
name: <span dangerouslySetInnerHTML={{ __html: from.display_name_html }} />,
|
||||
name: <span><Emojify text={from.display_name} emojis={from.emojis} /></span>,
|
||||
targetName: to.acct,
|
||||
}}
|
||||
/>
|
||||
|
|
|
@ -2,16 +2,18 @@ import React from 'react';
|
|||
import { FormattedDate, FormattedMessage, defineMessages, useIntl } from 'react-intl';
|
||||
|
||||
import { useAnnouncements } from 'pl-fe/api/hooks/admin/useAnnouncements';
|
||||
import { ParsedContent } from 'pl-fe/components/parsed-content';
|
||||
import ScrollableList from 'pl-fe/components/scrollable-list';
|
||||
import Button from 'pl-fe/components/ui/button';
|
||||
import Column from 'pl-fe/components/ui/column';
|
||||
import HStack from 'pl-fe/components/ui/hstack';
|
||||
import Stack from 'pl-fe/components/ui/stack';
|
||||
import Text from 'pl-fe/components/ui/text';
|
||||
import { AdminAnnouncement } from 'pl-fe/normalizers/announcement';
|
||||
import { useModalsStore } from 'pl-fe/stores/modals';
|
||||
import toast from 'pl-fe/toast';
|
||||
|
||||
import type { AdminAnnouncement } from 'pl-api';
|
||||
|
||||
const messages = defineMessages({
|
||||
heading: { id: 'column.admin.announcements', defaultMessage: 'Announcements' },
|
||||
deleteConfirm: { id: 'confirmations.admin.delete_announcement.confirm', defaultMessage: 'Delete' },
|
||||
|
@ -47,7 +49,9 @@ const Announcement: React.FC<IAnnouncement> = ({ announcement }) => {
|
|||
return (
|
||||
<div key={announcement.id} className='rounded-lg bg-gray-100 p-4 dark:bg-primary-800'>
|
||||
<Stack space={2}>
|
||||
<Text dangerouslySetInnerHTML={{ __html: announcement.contentHtml }} />
|
||||
<Text>
|
||||
<ParsedContent html={announcement.content} emojis={announcement.emojis} />
|
||||
</Text>
|
||||
{(announcement.starts_at || announcement.ends_at || announcement.all_day) && (
|
||||
<HStack space={2} wrap>
|
||||
{announcement.starts_at && (
|
||||
|
|
|
@ -127,10 +127,9 @@ const Report: React.FC<IReport> = ({ id }) => {
|
|||
|
||||
<Stack>
|
||||
{!!report.comment && report.comment.length > 0 && (
|
||||
<Text
|
||||
tag='blockquote'
|
||||
dangerouslySetInnerHTML={{ __html: report.comment }}
|
||||
/>
|
||||
<Text tag='blockquote'>
|
||||
{report.comment}
|
||||
</Text>
|
||||
)}
|
||||
|
||||
{!!account && (
|
||||
|
|
|
@ -4,6 +4,7 @@ import { useHistory } from 'react-router-dom';
|
|||
|
||||
import { useRelationship } from 'pl-fe/api/hooks/accounts/useRelationship';
|
||||
import DropdownMenu from 'pl-fe/components/dropdown-menu';
|
||||
import { ParsedContent } from 'pl-fe/components/parsed-content';
|
||||
import RelativeTimestamp from 'pl-fe/components/relative-timestamp';
|
||||
import Avatar from 'pl-fe/components/ui/avatar';
|
||||
import HStack from 'pl-fe/components/ui/hstack';
|
||||
|
@ -124,8 +125,9 @@ const ChatListItem: React.FC<IChatListItemInterface> = ({ chat, onClick }) => {
|
|||
truncate
|
||||
className='truncate-child pointer-events-none h-5 w-full'
|
||||
data-testid='chat-last-message'
|
||||
dangerouslySetInnerHTML={{ __html: chat.last_message?.content }}
|
||||
/>
|
||||
>
|
||||
<ParsedContent html={chat.last_message?.content} emojis={chat.last_message.emojis} />
|
||||
</Text>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
|
|
|
@ -5,11 +5,11 @@ import React, { useMemo, useState } from 'react';
|
|||
import { defineMessages, useIntl } from 'react-intl';
|
||||
|
||||
import DropdownMenu from 'pl-fe/components/dropdown-menu';
|
||||
import { ParsedContent } from 'pl-fe/components/parsed-content';
|
||||
import HStack from 'pl-fe/components/ui/hstack';
|
||||
import Icon from 'pl-fe/components/ui/icon';
|
||||
import Stack from 'pl-fe/components/ui/stack';
|
||||
import Text from 'pl-fe/components/ui/text';
|
||||
import emojify from 'pl-fe/features/emoji';
|
||||
import { MediaGallery } from 'pl-fe/features/ui/util/async-components';
|
||||
import { useAppSelector } from 'pl-fe/hooks/useAppSelector';
|
||||
import { ChatKeys, useChatActions } from 'pl-fe/queries/chats';
|
||||
|
@ -18,7 +18,7 @@ import { useModalsStore } from 'pl-fe/stores/modals';
|
|||
import { stripHTML } from 'pl-fe/utils/html';
|
||||
import { onlyEmoji } from 'pl-fe/utils/rich-content';
|
||||
|
||||
import type { Chat, CustomEmoji } from 'pl-api';
|
||||
import type { Chat } from 'pl-api';
|
||||
import type { Menu as IMenu } from 'pl-fe/components/dropdown-menu';
|
||||
import type { ChatMessage as ChatMessageEntity } from 'pl-fe/normalizers/chat-message';
|
||||
|
||||
|
@ -31,10 +31,6 @@ const messages = defineMessages({
|
|||
|
||||
const BIG_EMOJI_LIMIT = 3;
|
||||
|
||||
const makeEmojiMap = (record: ChatMessageEntity) =>
|
||||
record.emojis.reduce((map: Record<string, CustomEmoji>, emoji: CustomEmoji) =>
|
||||
(map[`:${emoji.shortcode}:`] = emoji, map), {});
|
||||
|
||||
const parsePendingContent = (content: string) => escape(content).replace(/(?:\r\n|\r|\n)/g, '<br>');
|
||||
|
||||
const parseContent = (chatMessage: ChatMessageEntity) => {
|
||||
|
@ -42,8 +38,7 @@ const parseContent = (chatMessage: ChatMessageEntity) => {
|
|||
const pending = chatMessage.pending;
|
||||
const deleting = chatMessage.deleting;
|
||||
const formatted = (pending && !deleting) ? parsePendingContent(content) : content;
|
||||
const emojiMap = makeEmojiMap(chatMessage);
|
||||
return emojify(formatted, emojiMap);
|
||||
return formatted;
|
||||
};
|
||||
|
||||
interface IChatMessage {
|
||||
|
@ -244,12 +239,9 @@ const ChatMessage = (props: IChatMessage) => {
|
|||
ref={setBubbleRef}
|
||||
tabIndex={0}
|
||||
>
|
||||
<Text
|
||||
size='sm'
|
||||
theme='inherit'
|
||||
className='break-word-nested'
|
||||
dangerouslySetInnerHTML={{ __html: content }}
|
||||
/>
|
||||
<Text size='sm' theme='inherit' className='break-word-nested'>
|
||||
<ParsedContent html={content} emojis={chatMessage.emojis} />
|
||||
</Text>
|
||||
</div>
|
||||
</HStack>
|
||||
)}
|
||||
|
|
|
@ -67,11 +67,14 @@ interface IComposeForm<ID extends string> {
|
|||
clickableAreaRef?: React.RefObject<HTMLDivElement>;
|
||||
event?: string;
|
||||
group?: string;
|
||||
extra?: React.ReactNode;
|
||||
onSubmit?: () => void;
|
||||
fullScreen?: boolean;
|
||||
withAvatar?: boolean;
|
||||
transparent?: boolean;
|
||||
}
|
||||
|
||||
const ComposeForm = <ID extends string>({ id, shouldCondense, autoFocus, clickableAreaRef, event, group, withAvatar, transparent }: IComposeForm<ID>) => {
|
||||
const ComposeForm = <ID extends string>({ id, shouldCondense, autoFocus, clickableAreaRef, event, group, extra, onSubmit, fullScreen, withAvatar, transparent }: IComposeForm<ID>) => {
|
||||
const history = useHistory();
|
||||
const intl = useIntl();
|
||||
const dispatch = useAppDispatch();
|
||||
|
@ -106,8 +109,17 @@ const ComposeForm = <ID extends string>({ id, shouldCondense, autoFocus, clickab
|
|||
|
||||
const { isDraggedOver } = useDraggedFiles(formRef);
|
||||
|
||||
// const text = editorRef.current?.getEditorState().read($createRemarkExport({
|
||||
// handlers: {
|
||||
// hashtag: (node) => ({ type: 'text', value: node.getTextContent() }),
|
||||
// mention: (node) => ({ type: 'text', value: node.getTextContent() }),
|
||||
// },
|
||||
// })) ?? '';
|
||||
|
||||
const fulltext = [spoilerText, countableText(text)].join('');
|
||||
|
||||
const characterCountProgress = length(text) / maxTootChars;
|
||||
|
||||
const isEmpty = !(fulltext.trim() || anyMedia);
|
||||
const condensed = shouldCondense && !isDraggedOver && !composeFocused && isEmpty && !isUploading;
|
||||
const shouldAutoFocus = autoFocus && !showSearch;
|
||||
|
@ -142,7 +154,8 @@ const ComposeForm = <ID extends string>({ id, shouldCondense, autoFocus, clickab
|
|||
if (!canSubmit) return;
|
||||
e?.preventDefault();
|
||||
|
||||
dispatch(submitCompose(id, { history })).then(() => {
|
||||
|
||||
dispatch(submitCompose(id, { history, onSubmit, propagate: fullScreen })).then(() => {
|
||||
editorRef.current?.dispatchCommand(CLEAR_EDITOR_COMMAND, undefined);
|
||||
}).catch(() => {});
|
||||
};
|
||||
|
@ -226,7 +239,7 @@ const ComposeForm = <ID extends string>({ id, shouldCondense, autoFocus, clickab
|
|||
if (features.postLanguages) selectButtons.push(<LanguageDropdown key='language-dropdown' composeId={id} />);
|
||||
|
||||
return (
|
||||
<Stack className='w-full' space={4} ref={formRef} onClick={handleClick} element='form' onSubmit={handleSubmit}>
|
||||
<Stack className='w-full' grow={fullScreen} space={4} ref={formRef} onClick={handleClick} element='form' onSubmit={handleSubmit}>
|
||||
<WarningContainer composeId={id} />
|
||||
|
||||
{!shouldCondense && !event && !group && groupId && <ReplyGroupIndicator composeId={id} />}
|
||||
|
@ -268,6 +281,7 @@ const ComposeForm = <ID extends string>({ id, shouldCondense, autoFocus, clickab
|
|||
handleSubmit={handleSubmit}
|
||||
onFocus={handleComposeFocus}
|
||||
onPaste={onPaste}
|
||||
fullScreen
|
||||
/>
|
||||
</Suspense>
|
||||
</div>
|
||||
|
@ -285,7 +299,7 @@ const ComposeForm = <ID extends string>({ id, shouldCondense, autoFocus, clickab
|
|||
{renderButtons()}
|
||||
|
||||
<HStack space={4} alignItems='center' className='ml-auto rtl:ml-0 rtl:mr-auto'>
|
||||
{maxTootChars && (
|
||||
{characterCountProgress >= 0.2 && maxTootChars && (
|
||||
<HStack space={1} alignItems='center'>
|
||||
<TextCharacterCounter max={maxTootChars} text={text} />
|
||||
<VisualCharacterCounter max={maxTootChars} text={text} />
|
||||
|
|
|
@ -3,6 +3,7 @@ import { FormattedMessage } from 'react-intl';
|
|||
|
||||
import Link from 'pl-fe/components/link';
|
||||
import Text from 'pl-fe/components/ui/text';
|
||||
import Emojify from 'pl-fe/features/emoji/emojify';
|
||||
import { useAppSelector } from 'pl-fe/hooks/useAppSelector';
|
||||
import { makeGetStatus } from 'pl-fe/selectors';
|
||||
|
||||
|
@ -28,10 +29,11 @@ const ReplyGroupIndicator = (props: IReplyGroupIndicator) => {
|
|||
id='compose.reply_group_indicator.message'
|
||||
defaultMessage='Posting to {groupLink}'
|
||||
values={{
|
||||
groupLink: <Link
|
||||
to={`/groups/${group.id}`}
|
||||
dangerouslySetInnerHTML={{ __html: group.display_name_html }}
|
||||
/>,
|
||||
groupLink: (
|
||||
<Link to={`/groups/${group.id}`}>
|
||||
<Emojify text={group.display_name} emojis={group.emojis} />
|
||||
</Link>
|
||||
),
|
||||
}}
|
||||
/>
|
||||
</Text>
|
||||
|
|
|
@ -12,7 +12,7 @@ import type { Status } from 'pl-fe/normalizers/status';
|
|||
|
||||
interface IReplyIndicator {
|
||||
className?: string;
|
||||
status?: Pick<Status, 'account_id' | 'contentHtml' | 'created_at' | 'hidden' | 'media_attachments' | 'mentions' | 'search_index' | 'sensitive' | 'spoiler_text' | 'quote_id'>;
|
||||
status?: Pick<Status, 'account_id' | 'content' | 'created_at' | 'emojis' | 'hidden' | 'media_attachments' | 'mentions' | 'search_index' | 'sensitive' | 'spoiler_text' | 'quote_id'>;
|
||||
onCancel?: () => void;
|
||||
hideActions: boolean;
|
||||
}
|
||||
|
@ -52,7 +52,7 @@ const ReplyIndicator: React.FC<IReplyIndicator> = ({ className, status, hideActi
|
|||
size='sm'
|
||||
direction={getTextDirection(status.search_index)}
|
||||
>
|
||||
<ParsedContent html={status.contentHtml} mentions={status.mentions} hasQuote={!!status.quote_id} />
|
||||
<ParsedContent html={status.content} mentions={status.mentions} hasQuote={!!status.quote_id} emojis={status.emojis} />
|
||||
</Markup>
|
||||
|
||||
{status.media_attachments.length > 0 && (
|
||||
|
|
|
@ -62,6 +62,7 @@ interface IComposeEditor {
|
|||
onChange?(text: string): void;
|
||||
onFocus?: React.FocusEventHandler<HTMLDivElement>;
|
||||
placeholder?: string;
|
||||
fullScreen?: boolean;
|
||||
}
|
||||
|
||||
const theme: InitialConfigType['theme'] = {
|
||||
|
@ -97,6 +98,7 @@ const ComposeEditor = React.forwardRef<LexicalEditor, IComposeEditor>(({
|
|||
onFocus,
|
||||
onPaste,
|
||||
placeholder,
|
||||
fullScreen,
|
||||
}, ref) => {
|
||||
const dispatch = useAppDispatch();
|
||||
const { content_type: contentType } = useCompose(composeId);
|
||||
|
@ -196,16 +198,17 @@ const ComposeEditor = React.forwardRef<LexicalEditor, IComposeEditor>(({
|
|||
|
||||
return (
|
||||
<LexicalComposer key={isWysiwyg ? 'wysiwyg' : ''} initialConfig={initialConfig}>
|
||||
<div className={clsx('lexical relative', className)} data-markup>
|
||||
<div className={clsx('lexical relative', fullScreen && 'h-full', className)} data-markup>
|
||||
<RichTextPlugin
|
||||
contentEditable={
|
||||
<div onFocus={onFocus} onPaste={handlePaste} ref={onRef}>
|
||||
<div className={clsx(fullScreen && 'h-full')} onFocus={onFocus} onPaste={handlePaste} ref={onRef}>
|
||||
<ContentEditable
|
||||
tabIndex={0}
|
||||
className={clsx(
|
||||
'relative z-10 text-[1rem] outline-none transition-[min-height] motion-reduce:transition-none',
|
||||
editableClassName,
|
||||
{
|
||||
'h-full': fullScreen,
|
||||
'min-h-[39px]': condensed,
|
||||
'min-h-[99px]': !condensed,
|
||||
},
|
||||
|
@ -216,7 +219,7 @@ const ComposeEditor = React.forwardRef<LexicalEditor, IComposeEditor>(({
|
|||
placeholder={(
|
||||
<div
|
||||
className={clsx(
|
||||
'pointer-events-none absolute top-0 select-none text-[1rem] text-gray-600 dark:placeholder:text-gray-600',
|
||||
'pointer-events-none absolute select-none text-[1rem] text-gray-600 dark:placeholder:text-gray-600',
|
||||
placeholderClassName,
|
||||
)}
|
||||
>
|
||||
|
|
108
packages/pl-fe/src/features/compose/index.tsx
Normal file
108
packages/pl-fe/src/features/compose/index.tsx
Normal file
|
@ -0,0 +1,108 @@
|
|||
/* eslint-disable promise/catch-or-return */
|
||||
import React, { useEffect, useMemo } from 'react';
|
||||
import { FormattedMessage, useIntl } from 'react-intl';
|
||||
import { useLocation } from 'react-router-dom';
|
||||
|
||||
import { COMPOSE_QUOTE, COMPOSE_REPLY, setComposeToStatus } from 'pl-fe/actions/compose';
|
||||
import { fetchDraftStatuses } from 'pl-fe/actions/draft-statuses';
|
||||
import { fetchStatus } from 'pl-fe/actions/statuses';
|
||||
import Stack from 'pl-fe/components/ui/stack';
|
||||
import ComposeForm from 'pl-fe/features/compose/components/compose-form';
|
||||
import { useAppDispatch } from 'pl-fe/hooks/useAppDispatch';
|
||||
import { useCompose } from 'pl-fe/hooks/useCompose';
|
||||
import { useFeatures } from 'pl-fe/hooks/useFeatures';
|
||||
import { makeGetStatus, selectOwnAccount } from 'pl-fe/selectors';
|
||||
import { useSettingsStore } from 'pl-fe/stores/settings';
|
||||
|
||||
import { buildStatus as buildDraftStatus } from '../draft-statuses/builder';
|
||||
|
||||
const getStatus = makeGetStatus();
|
||||
|
||||
const ComposePage = () => {
|
||||
const { search } = useLocation();
|
||||
const params = new URLSearchParams(search);
|
||||
const dispatch = useAppDispatch();
|
||||
const intl = useIntl();
|
||||
const { createStatusExplicitAddressing } = useFeatures();
|
||||
const { settings } = useSettingsStore();
|
||||
|
||||
const compose = useCompose('compose-modal');
|
||||
|
||||
const { id: statusId, privacy, group_id: groupId } = compose!;
|
||||
|
||||
const inReplyTo = params.get('in_reply_to');
|
||||
const quote = params.get('quote');
|
||||
const draftId = params.get('draft_id');
|
||||
|
||||
const heading = useMemo(() => {
|
||||
if (statusId) {
|
||||
return <FormattedMessage id='navigation_bar.compose_edit' defaultMessage='Edit post' />;
|
||||
} else if (privacy === 'direct') {
|
||||
return <FormattedMessage id='navigation_bar.compose_direct' defaultMessage='Direct message' />;
|
||||
} else if (inReplyTo && groupId) {
|
||||
return <FormattedMessage id='navigation_bar.compose_group_reply' defaultMessage='Reply to group post' />;
|
||||
} else if (groupId) {
|
||||
return <FormattedMessage id='navigation_bar.compose_group' defaultMessage='Compose to group' />;
|
||||
} else if (inReplyTo) {
|
||||
return <FormattedMessage id='navigation_bar.compose_reply' defaultMessage='Reply to post' />;
|
||||
} else if (quote) {
|
||||
return <FormattedMessage id='navigation_bar.compose_quote' defaultMessage='Quote post' />;
|
||||
} else {
|
||||
return <FormattedMessage id='navigation_bar.compose' defaultMessage='Compose new post' />;
|
||||
}
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (inReplyTo) dispatch(fetchStatus(inReplyTo, intl))
|
||||
.then((_) => dispatch((dispatch, getState) => {
|
||||
const state = getState();
|
||||
|
||||
const status = getStatus(state, { id: inReplyTo });
|
||||
|
||||
dispatch({
|
||||
type: COMPOSE_REPLY,
|
||||
id: 'compose-modal',
|
||||
status: status,
|
||||
account: selectOwnAccount(state),
|
||||
explicitAddressing: createStatusExplicitAddressing,
|
||||
preserveSpoilers: settings.preserveSpoilers,
|
||||
});
|
||||
}));
|
||||
else if (quote) dispatch(fetchStatus(quote, intl))
|
||||
.then((_) => dispatch((dispatch, getState) => {
|
||||
const state = getState();
|
||||
|
||||
const status = getStatus(state, { id: quote });
|
||||
|
||||
dispatch({
|
||||
type: COMPOSE_QUOTE,
|
||||
id: 'compose-modal',
|
||||
status: status,
|
||||
account: selectOwnAccount(state),
|
||||
explicitAddressing: createStatusExplicitAddressing,
|
||||
});
|
||||
}));
|
||||
else if (draftId) dispatch(fetchDraftStatuses())
|
||||
.then(() => dispatch((dispatch, getState) => {
|
||||
const state = getState();
|
||||
const draftStatus = state.draft_statuses.get(draftId);
|
||||
|
||||
if (draftStatus) {
|
||||
const status = buildDraftStatus(state, draftStatus);
|
||||
|
||||
dispatch(setComposeToStatus(status as any, status.poll, draftStatus.text, draftStatus.spoiler_text, draftStatus.content_type, false, draftStatus.draft_id, draftStatus.editorState));
|
||||
}
|
||||
}));
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<Stack className='h-full bg-white p-4 black:bg-black dark:bg-gray-900' space={2}>
|
||||
<h3 className='grow-0 truncate text-lg font-bold leading-6 text-gray-900 dark:text-white'>
|
||||
{heading}
|
||||
</h3>
|
||||
<ComposeForm id='compose-modal' onSubmit={() => window.close()} fullScreen />
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
|
||||
export default ComposePage;
|
|
@ -70,13 +70,13 @@ const AccountCard: React.FC<IAccountCard> = ({ id }) => {
|
|||
withRelationship={false}
|
||||
/>
|
||||
|
||||
{!!account.note_emojified && (
|
||||
{!!account.note && (
|
||||
<Text
|
||||
truncate
|
||||
align='left'
|
||||
className='line-clamp-2 inline text-ellipsis [&_br]:hidden [&_p:first-child]:inline [&_p:first-child]:truncate [&_p]:hidden'
|
||||
>
|
||||
<ParsedContent html={account.note_emojified} />
|
||||
<ParsedContent html={account.note} emojis={account.emojis} />
|
||||
</Text>
|
||||
)}
|
||||
</Stack>
|
||||
|
|
|
@ -6,6 +6,7 @@ import { cancelDraftStatus } from 'pl-fe/actions/draft-statuses';
|
|||
import Button from 'pl-fe/components/ui/button';
|
||||
import HStack from 'pl-fe/components/ui/hstack';
|
||||
import { useAppDispatch } from 'pl-fe/hooks/useAppDispatch';
|
||||
import { userTouching } from 'pl-fe/is-mobile';
|
||||
import { useModalsStore } from 'pl-fe/stores/modals';
|
||||
import { useSettingsStore } from 'pl-fe/stores/settings';
|
||||
|
||||
|
@ -31,23 +32,21 @@ const DraftStatusActionBar: React.FC<IDraftStatusActionBar> = ({ source, status
|
|||
const dispatch = useAppDispatch();
|
||||
|
||||
const handleCancelClick = () => {
|
||||
dispatch((_, getState) => {
|
||||
|
||||
const deleteModal = settings.deleteModal;
|
||||
if (!deleteModal) {
|
||||
dispatch(cancelDraftStatus(source.draft_id));
|
||||
} else {
|
||||
openModal('CONFIRM', {
|
||||
heading: intl.formatMessage(messages.deleteHeading),
|
||||
message: intl.formatMessage(messages.deleteMessage),
|
||||
confirm: intl.formatMessage(messages.deleteConfirm),
|
||||
onConfirm: () => dispatch(cancelDraftStatus(source.draft_id)),
|
||||
});
|
||||
}
|
||||
});
|
||||
const deleteModal = settings.deleteModal;
|
||||
if (!deleteModal) {
|
||||
dispatch(cancelDraftStatus(source.draft_id));
|
||||
} else {
|
||||
openModal('CONFIRM', {
|
||||
heading: intl.formatMessage(messages.deleteHeading),
|
||||
message: intl.formatMessage(messages.deleteMessage),
|
||||
confirm: intl.formatMessage(messages.deleteConfirm),
|
||||
onConfirm: () => dispatch(cancelDraftStatus(source.draft_id)),
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const handleEditClick = () => {
|
||||
if (!userTouching.matches) return window.open(`/compose?draft_id=${source.draft_id}`, 'targetWindow', 'height=500,width=700');
|
||||
dispatch(setComposeToStatus(status, status.poll, source.text, source.spoiler_text, source.content_type, false, source.draft_id, source.editorState));
|
||||
openModal('COMPOSE');
|
||||
};
|
||||
|
|
103
packages/pl-fe/src/features/emoji/emojify.tsx
Normal file
103
packages/pl-fe/src/features/emoji/emojify.tsx
Normal file
|
@ -0,0 +1,103 @@
|
|||
import split from 'graphemesplit';
|
||||
import React from 'react';
|
||||
|
||||
import { makeEmojiMap } from 'pl-fe/utils/normalizers';
|
||||
|
||||
import unicodeMapping from './mapping';
|
||||
|
||||
import { validEmojiChar } from '.';
|
||||
|
||||
import type { CustomEmoji } from 'pl-api';
|
||||
|
||||
interface IMaybeEmoji {
|
||||
text: string;
|
||||
emojis: Record<string, CustomEmoji>;
|
||||
}
|
||||
|
||||
const MaybeEmoji: React.FC<IMaybeEmoji> = ({ text, emojis }) => {
|
||||
if (text.length < 3) return text;
|
||||
if (text in emojis) {
|
||||
const emoji = emojis[text];
|
||||
const filename = emoji.static_url;
|
||||
|
||||
if (filename?.length > 0) {
|
||||
return <img draggable={false} className='emojione' alt={text} title={text} src={filename} />;
|
||||
}
|
||||
}
|
||||
|
||||
return text;
|
||||
};
|
||||
|
||||
interface IEmojify {
|
||||
text: string;
|
||||
emojis?: Array<CustomEmoji> | Record<string, CustomEmoji>;
|
||||
}
|
||||
|
||||
const Emojify: React.FC<IEmojify> = ({ text, emojis = {} }) => React.useMemo(() => {
|
||||
if (Array.isArray(emojis)) emojis = makeEmojiMap(emojis);
|
||||
|
||||
const nodes = [];
|
||||
|
||||
let stack = '';
|
||||
let open = false;
|
||||
|
||||
const clearStack = () => {
|
||||
if (stack.length) nodes.push(stack);
|
||||
open = false;
|
||||
stack = '';
|
||||
};
|
||||
|
||||
for (let c of split(text)) {
|
||||
// convert FE0E selector to FE0F so it can be found in unimap
|
||||
if (c.codePointAt(c.length - 1) === 65038) {
|
||||
c = c.slice(0, -1) + String.fromCodePoint(65039);
|
||||
}
|
||||
|
||||
// unqualified emojis aren't in emoji-mart's mappings so we just add FEOF
|
||||
const unqualified = c + String.fromCodePoint(65039);
|
||||
|
||||
if (c in unicodeMapping) {
|
||||
clearStack();
|
||||
|
||||
const { unified, shortcode } = unicodeMapping[c];
|
||||
|
||||
nodes.push(
|
||||
<img draggable={false} className='emojione' alt={c} title={`:${shortcode}:`} src={`/packs/emoji/${unified}.svg`} />,
|
||||
);
|
||||
} else if (unqualified in unicodeMapping) {
|
||||
clearStack();
|
||||
|
||||
const { unified, shortcode } = unicodeMapping[unqualified];
|
||||
|
||||
nodes.push(
|
||||
<img draggable={false} className='emojione' alt={unqualified} title={`:${shortcode}:`} src={`/packs/emoji/${unified}.svg`} />,
|
||||
);
|
||||
} else if (c === ':') {
|
||||
if (!open) {
|
||||
clearStack();
|
||||
}
|
||||
|
||||
stack += ':';
|
||||
|
||||
// we see another : we convert it and clear the stack buffer
|
||||
if (open) {
|
||||
nodes.push(<MaybeEmoji text={stack} emojis={emojis} />);
|
||||
stack = '';
|
||||
}
|
||||
|
||||
open = !open;
|
||||
} else {
|
||||
stack += c;
|
||||
|
||||
if (open && !validEmojiChar(c)) {
|
||||
clearStack();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (stack.length) nodes.push(stack);
|
||||
|
||||
return nodes;
|
||||
}, [text, emojis]);
|
||||
|
||||
export { Emojify as default };
|
|
@ -1,7 +1,3 @@
|
|||
import split from 'graphemesplit';
|
||||
|
||||
import unicodeMapping from './mapping';
|
||||
|
||||
import type { Emoji as EmojiMart, CustomEmoji as EmojiMartCustom } from './data';
|
||||
import type { CustomEmoji as BaseCustomEmoji } from 'pl-api';
|
||||
|
||||
|
@ -57,148 +53,6 @@ const isAlphaNumeric = (c: string) => {
|
|||
const validEmojiChar = (c: string) =>
|
||||
isAlphaNumeric(c) || ['_', '-', '.'].includes(c);
|
||||
|
||||
const convertCustom = (shortname: string, filename: string) =>
|
||||
`<img draggable="false" class="emojione transition-transform ease-linear no-reduce-motion:hover:scale-125" alt="${shortname}" title="${shortname}" src="${filename}" />`;
|
||||
|
||||
const convertUnicode = (c: string) => {
|
||||
const { unified, shortcode } = unicodeMapping[c];
|
||||
|
||||
return `<img draggable="false" class="emojione transition-transform ease-linear no-reduce-motion:hover:scale-125" alt="${c}" title=":${shortcode}:" src="/packs/emoji/${unified}.svg" />`;
|
||||
};
|
||||
|
||||
const convertEmoji = (str: string, customEmojis: any) => {
|
||||
if (str.length < 3) return str;
|
||||
if (str in customEmojis) {
|
||||
const emoji = customEmojis[str];
|
||||
const filename = emoji.static_url;
|
||||
|
||||
if (filename?.length > 0) {
|
||||
return convertCustom(str, filename);
|
||||
}
|
||||
}
|
||||
|
||||
return str;
|
||||
};
|
||||
|
||||
const emojifyText = (str: string, customEmojis = {}) => {
|
||||
let buf = '';
|
||||
let stack = '';
|
||||
let open = false;
|
||||
|
||||
const clearStack = () => {
|
||||
buf += stack;
|
||||
open = false;
|
||||
stack = '';
|
||||
};
|
||||
|
||||
for (let c of split(str)) {
|
||||
// convert FE0E selector to FE0F so it can be found in unimap
|
||||
if (c.codePointAt(c.length - 1) === 65038) {
|
||||
c = c.slice(0, -1) + String.fromCodePoint(65039);
|
||||
}
|
||||
|
||||
// unqualified emojis aren't in emoji-mart's mappings so we just add FEOF
|
||||
const unqualified = c + String.fromCodePoint(65039);
|
||||
|
||||
if (c in unicodeMapping) {
|
||||
if (open) { // unicode emoji inside colon
|
||||
clearStack();
|
||||
}
|
||||
|
||||
buf += convertUnicode(c);
|
||||
} else if (unqualified in unicodeMapping) {
|
||||
if (open) { // unicode emoji inside colon
|
||||
clearStack();
|
||||
}
|
||||
|
||||
buf += convertUnicode(unqualified);
|
||||
} else if (c === ':') {
|
||||
stack += ':';
|
||||
|
||||
// we see another : we convert it and clear the stack buffer
|
||||
if (open) {
|
||||
buf += convertEmoji(stack, customEmojis);
|
||||
stack = '';
|
||||
}
|
||||
|
||||
open = !open;
|
||||
} else {
|
||||
if (open) {
|
||||
stack += c;
|
||||
|
||||
// if the stack is non-null and we see invalid chars it's a string not emoji
|
||||
// so we push it to the return result and clear it
|
||||
if (!validEmojiChar(c)) {
|
||||
clearStack();
|
||||
}
|
||||
} else {
|
||||
buf += c;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// never found a closing colon so it's just a raw string
|
||||
if (open) {
|
||||
buf += stack;
|
||||
}
|
||||
|
||||
return buf;
|
||||
};
|
||||
|
||||
const parseHTML = (str: string): { text: boolean; data: string }[] => {
|
||||
const tokens = [];
|
||||
let buf = '';
|
||||
let stack = '';
|
||||
let open = false;
|
||||
|
||||
for (const c of str) {
|
||||
if (c === '<') {
|
||||
if (open) {
|
||||
tokens.push({ text: true, data: stack });
|
||||
stack = '<';
|
||||
} else {
|
||||
tokens.push({ text: true, data: buf });
|
||||
stack = '<';
|
||||
open = true;
|
||||
}
|
||||
} else if (c === '>') {
|
||||
if (open) {
|
||||
open = false;
|
||||
tokens.push({ text: false, data: stack + '>' });
|
||||
stack = '';
|
||||
buf = '';
|
||||
} else {
|
||||
buf += '>';
|
||||
}
|
||||
|
||||
} else {
|
||||
if (open) {
|
||||
stack += c;
|
||||
} else {
|
||||
buf += c;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (open) {
|
||||
tokens.push({ text: true, data: buf + stack });
|
||||
} else if (buf !== '') {
|
||||
tokens.push({ text: true, data: buf });
|
||||
}
|
||||
|
||||
return tokens;
|
||||
};
|
||||
|
||||
const emojify = (str: string, customEmojis: Record<string, BaseCustomEmoji> = {}) =>
|
||||
parseHTML(str)
|
||||
.map(({ text, data }) => {
|
||||
if (!text) return data;
|
||||
if (data.length === 0 || data === ' ') return data;
|
||||
|
||||
return emojifyText(data, customEmojis);
|
||||
})
|
||||
.join('');
|
||||
|
||||
const buildCustomEmojis = (customEmojis: Array<BaseCustomEmoji>) => {
|
||||
const emojis: EmojiMart<EmojiMartCustom>[] = [];
|
||||
|
||||
|
@ -225,5 +79,5 @@ export {
|
|||
isCustomEmoji,
|
||||
isNativeEmoji,
|
||||
buildCustomEmojis,
|
||||
emojify as default,
|
||||
validEmojiChar,
|
||||
};
|
||||
|
|
|
@ -19,6 +19,7 @@ import IconButton from 'pl-fe/components/ui/icon-button';
|
|||
import Stack from 'pl-fe/components/ui/stack';
|
||||
import Text from 'pl-fe/components/ui/text';
|
||||
import VerificationBadge from 'pl-fe/components/verification-badge';
|
||||
import Emojify from 'pl-fe/features/emoji/emojify';
|
||||
import { useAppDispatch } from 'pl-fe/hooks/useAppDispatch';
|
||||
import { useFeatures } from 'pl-fe/hooks/useFeatures';
|
||||
import { useOwnAccount } from 'pl-fe/hooks/useOwnAccount';
|
||||
|
@ -414,7 +415,7 @@ const EventHeader: React.FC<IEventHeader> = ({ status }) => {
|
|||
name: (
|
||||
<Link className='mention inline-block' to={`/@${account.acct}`}>
|
||||
<HStack space={1} alignItems='center' grow>
|
||||
<span dangerouslySetInnerHTML={{ __html: account.display_name_html }} />
|
||||
<span><Emojify text={account.display_name} emojis={account.emojis} /></span>
|
||||
{account.verified && <VerificationBadge />}
|
||||
</HStack>
|
||||
</Link>
|
||||
|
|
|
@ -181,7 +181,7 @@ const EventInformation: React.FC<IEventInformation> = ({ params }) => {
|
|||
|
||||
return (
|
||||
<Stack className='mt-4 sm:p-2' space={2}>
|
||||
{!!status.contentHtml.trim() && (
|
||||
{!!status.content.trim() && (
|
||||
<Stack space={1}>
|
||||
<Text size='xl' weight='bold'>
|
||||
<FormattedMessage id='event.description' defaultMessage='Description' />
|
||||
|
|
|
@ -10,6 +10,7 @@ import Text from 'pl-fe/components/ui/text';
|
|||
import VerificationBadge from 'pl-fe/components/verification-badge';
|
||||
import { useAppSelector } from 'pl-fe/hooks/useAppSelector';
|
||||
|
||||
import Emojify from '../emoji/emojify';
|
||||
import ActionButton from '../ui/components/action-button';
|
||||
import { HotKeys } from '../ui/components/hotkeys';
|
||||
|
||||
|
@ -41,14 +42,9 @@ const SuggestionItem: React.FC<ISuggestionItem> = ({ accountId }) => {
|
|||
|
||||
<Stack>
|
||||
<HStack alignItems='center' justifyContent='center' space={1}>
|
||||
<Text
|
||||
weight='semibold'
|
||||
dangerouslySetInnerHTML={{ __html: account.display_name_html }}
|
||||
truncate
|
||||
align='center'
|
||||
size='sm'
|
||||
className='max-w-[95%]'
|
||||
/>
|
||||
<Text weight='semibold' truncate align='center' size='sm' className='max-w-[95%]'>
|
||||
<Emojify text={account.display_name} emojis={account.emojis} />
|
||||
</Text>
|
||||
|
||||
{account.verified && <VerificationBadge />}
|
||||
</HStack>
|
||||
|
|
|
@ -10,6 +10,7 @@ import HStack from 'pl-fe/components/ui/hstack';
|
|||
import Icon from 'pl-fe/components/ui/icon';
|
||||
import Stack from 'pl-fe/components/ui/stack';
|
||||
import Text from 'pl-fe/components/ui/text';
|
||||
import Emojify from 'pl-fe/features/emoji/emojify';
|
||||
import { useModalsStore } from 'pl-fe/stores/modals';
|
||||
import { isDefaultHeader } from 'pl-fe/utils/accounts';
|
||||
|
||||
|
@ -141,9 +142,10 @@ const GroupHeader: React.FC<IGroupHeader> = ({ group }) => {
|
|||
<Text
|
||||
size='xl'
|
||||
weight='bold'
|
||||
dangerouslySetInnerHTML={{ __html: group.display_name_html }}
|
||||
data-testid='group-name'
|
||||
/>
|
||||
>
|
||||
<Emojify text={group.display_name} emojis={group.emojis} />
|
||||
</Text>
|
||||
|
||||
<Stack data-testid='group-meta' space={1} alignItems='center'>
|
||||
<HStack className='text-gray-700 dark:text-gray-600' space={2} wrap>
|
||||
|
@ -157,7 +159,7 @@ const GroupHeader: React.FC<IGroupHeader> = ({ group }) => {
|
|||
align='center'
|
||||
className='[&_a]:text-primary-600 [&_a]:hover:underline [&_a]:dark:text-accent-blue'
|
||||
>
|
||||
<ParsedContent html={group.note_emojified} />
|
||||
<ParsedContent html={group.note} emojis={group.emojis} />
|
||||
</Text>
|
||||
</Stack>
|
||||
|
||||
|
|
|
@ -18,6 +18,7 @@ import { useAppSelector } from 'pl-fe/hooks/useAppSelector';
|
|||
import { useInstance } from 'pl-fe/hooks/useInstance';
|
||||
import toast from 'pl-fe/toast';
|
||||
import { isDefaultAvatar, isDefaultHeader } from 'pl-fe/utils/accounts';
|
||||
import { unescapeHTML } from 'pl-fe/utils/html';
|
||||
|
||||
import AvatarPicker from '../edit-profile/components/avatar-picker';
|
||||
import HeaderPicker from '../edit-profile/components/header-picker';
|
||||
|
@ -51,7 +52,7 @@ const EditGroup: React.FC<IEditGroup> = ({ params: { groupId } }) => {
|
|||
const header = useImageField({ maxPixels: 1920 * 1080, preview: nonDefaultHeader(group?.header) });
|
||||
|
||||
const displayName = useTextField(group?.display_name);
|
||||
const note = useTextField(group?.note_plain);
|
||||
const note = useTextField(unescapeHTML(group?.note));
|
||||
|
||||
const maxName = Number(instance.configuration.groups.max_characters_name);
|
||||
const maxNote = Number(instance.configuration.groups.max_characters_description);
|
||||
|
|
|
@ -13,6 +13,7 @@ import Text from 'pl-fe/components/ui/text';
|
|||
import { useModalsStore } from 'pl-fe/stores/modals';
|
||||
import toast from 'pl-fe/toast';
|
||||
|
||||
import Emojify from '../emoji/emojify';
|
||||
import ColumnForbidden from '../ui/components/column-forbidden';
|
||||
|
||||
type RouteParams = { groupId: string };
|
||||
|
@ -86,7 +87,7 @@ const ManageGroup: React.FC<IManageGroup> = ({ params }) => {
|
|||
|
||||
<List>
|
||||
<ListItem label={intl.formatMessage(messages.editGroup)} to={`/groups/${group.id}/manage/edit`}>
|
||||
<span dangerouslySetInnerHTML={{ __html: group.display_name_html }} />
|
||||
<span><Emojify text={group.display_name} emojis={group.emojis} /></span>
|
||||
</ListItem>
|
||||
</List>
|
||||
</>
|
||||
|
|
|
@ -7,13 +7,14 @@ import HStack from 'pl-fe/components/ui/hstack';
|
|||
import Icon from 'pl-fe/components/ui/icon';
|
||||
import Stack from 'pl-fe/components/ui/stack';
|
||||
import Text from 'pl-fe/components/ui/text';
|
||||
import Emojify from 'pl-fe/features/emoji/emojify';
|
||||
import GroupActionButton from 'pl-fe/features/group/components/group-action-button';
|
||||
import { shortNumberFormat } from 'pl-fe/utils/numbers';
|
||||
|
||||
import type { Group } from 'pl-fe/normalizers/group';
|
||||
|
||||
interface IGroupListItem {
|
||||
group: Pick<Group, 'id' | 'avatar' | 'avatar_description' | 'display_name_html' | 'locked' | 'members_count' | 'relationship'>;
|
||||
group: Pick<Group, 'id' | 'avatar' | 'avatar_description' | 'display_name' | 'emojis' | 'locked' | 'members_count' | 'relationship'>;
|
||||
withJoinAction?: boolean;
|
||||
}
|
||||
|
||||
|
@ -34,11 +35,9 @@ const GroupListItem = (props: IGroupListItem) => {
|
|||
/>
|
||||
|
||||
<Stack className='overflow-hidden'>
|
||||
<Text
|
||||
weight='bold'
|
||||
dangerouslySetInnerHTML={{ __html: group.display_name_html }}
|
||||
truncate
|
||||
/>
|
||||
<Text weight='bold' truncate>
|
||||
<Emojify text={group.display_name} emojis={group.emojis} />
|
||||
</Text>
|
||||
|
||||
<HStack className='text-gray-700 dark:text-gray-600' space={1} alignItems='center'>
|
||||
<Icon
|
||||
|
|
|
@ -5,9 +5,11 @@ import {
|
|||
importFollows,
|
||||
importBlocks,
|
||||
importMutes,
|
||||
importArchive,
|
||||
} from 'pl-fe/actions/import-data';
|
||||
import Column from 'pl-fe/components/ui/column';
|
||||
import { useFeatures } from 'pl-fe/hooks/useFeatures';
|
||||
import { useInstance } from 'pl-fe/hooks/useInstance';
|
||||
|
||||
import DataImporter from './components/data-importer';
|
||||
|
||||
|
@ -34,8 +36,15 @@ const muteMessages = defineMessages({
|
|||
submit: { id: 'import_data.actions.import_mutes', defaultMessage: 'Import mutes' },
|
||||
});
|
||||
|
||||
const archiveMessages = defineMessages({
|
||||
input_label: { id: 'import_data.archive_label', defaultMessage: 'Archive' },
|
||||
input_hint: { id: 'import_data.hints.archive', defaultMessage: 'Archive containing an archive of statuses' },
|
||||
submit: { id: 'import_data.actions.import_archive', defaultMessage: 'Import archive' },
|
||||
});
|
||||
|
||||
const ImportData = () => {
|
||||
const intl = useIntl();
|
||||
const instance = useInstance();
|
||||
const features = useFeatures();
|
||||
|
||||
return (
|
||||
|
@ -43,6 +52,9 @@ const ImportData = () => {
|
|||
{features.importFollows && <DataImporter action={importFollows} messages={followMessages} allowOverwrite={features.importOverwrite} />}
|
||||
{features.importBlocks && <DataImporter action={importBlocks} messages={blockMessages} allowOverwrite={features.importOverwrite} />}
|
||||
{features.importMutes && <DataImporter action={importMutes} messages={muteMessages} />}
|
||||
{instance.pleroma.metadata.features.includes('bigbuffet') && (
|
||||
<DataImporter action={importArchive} messages={archiveMessages} accept='.tar,.tar.gz,.zip' />
|
||||
)}
|
||||
</Column>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import DOMPurify from 'isomorphic-dompurify';
|
||||
import React, { useMemo } from 'react';
|
||||
import React from 'react';
|
||||
|
||||
import Markup from 'pl-fe/components/markup';
|
||||
import { ParsedContent } from 'pl-fe/components/parsed-content';
|
||||
import Stack from 'pl-fe/components/ui/stack';
|
||||
import { useInstance } from 'pl-fe/hooks/useInstance';
|
||||
import { getTextDirection } from 'pl-fe/utils/rtl';
|
||||
|
@ -10,7 +10,6 @@ import { LogoText } from './logo-text';
|
|||
|
||||
const SiteBanner: React.FC = () => {
|
||||
const instance = useInstance();
|
||||
const description = useMemo(() => DOMPurify.sanitize(instance.description), [instance.description]);
|
||||
|
||||
return (
|
||||
<Stack space={6}>
|
||||
|
@ -21,9 +20,10 @@ const SiteBanner: React.FC = () => {
|
|||
{instance.description.trim().length > 0 && (
|
||||
<Markup
|
||||
size='lg'
|
||||
dangerouslySetInnerHTML={{ __html: description }}
|
||||
direction={getTextDirection(description)}
|
||||
/>
|
||||
direction={getTextDirection(instance.description)}
|
||||
>
|
||||
<ParsedContent html={instance.description} />
|
||||
</Markup>
|
||||
)}
|
||||
</Stack>
|
||||
);
|
||||
|
|
|
@ -50,7 +50,7 @@ const LandingTimeline = () => {
|
|||
{timelineEnabled ? (
|
||||
<PullToRefresh onRefresh={handleRefresh}>
|
||||
<Timeline
|
||||
listClassName='black:p-0 black:sm:p-4 black:sm:pt-0'
|
||||
className='black:p-0 black:sm:p-4 black:sm:pt-0'
|
||||
loadMoreClassName='black:sm:mx-4'
|
||||
scrollKey={`${timelineId}_timeline`}
|
||||
timelineId={timelineId}
|
||||
|
|
|
@ -13,6 +13,7 @@ import HStack from 'pl-fe/components/ui/hstack';
|
|||
import Text from 'pl-fe/components/ui/text';
|
||||
import AccountContainer from 'pl-fe/containers/account-container';
|
||||
import StatusContainer from 'pl-fe/containers/status-container';
|
||||
import Emojify from 'pl-fe/features/emoji/emojify';
|
||||
import { HotKeys } from 'pl-fe/features/ui/components/hotkeys';
|
||||
import { useAppDispatch } from 'pl-fe/hooks/useAppDispatch';
|
||||
import { useAppSelector } from 'pl-fe/hooks/useAppSelector';
|
||||
|
@ -37,14 +38,15 @@ const notificationForScreenReader = (intl: IntlShape, message: string, timestamp
|
|||
return output.join(', ');
|
||||
};
|
||||
|
||||
const buildLink = (account: Pick<Account, 'acct' | 'display_name_html' | 'id'>): JSX.Element => (
|
||||
const buildLink = (account: Pick<Account, 'acct' | 'display_name' | 'emojis' | 'id'>): JSX.Element => (
|
||||
<HoverAccountWrapper key={account.acct} element='bdi' accountId={account.id}>
|
||||
<Link
|
||||
className='font-bold text-gray-800 hover:underline dark:text-gray-200'
|
||||
title={account.acct}
|
||||
to={`/@${account.acct}`}
|
||||
dangerouslySetInnerHTML={{ __html: account.display_name_html }}
|
||||
/>
|
||||
>
|
||||
<Emojify text={account.display_name} emojis={account.emojis} />
|
||||
</Link>
|
||||
</HoverAccountWrapper>
|
||||
);
|
||||
|
||||
|
@ -153,7 +155,7 @@ const messages: Record<NotificationType | 'reply', MessageDescriptor> = defineMe
|
|||
const buildMessage = (
|
||||
intl: IntlShape,
|
||||
type: NotificationType | 'reply',
|
||||
accounts: Array<Pick<Account, 'acct' | 'display_name_html' | 'id'>>,
|
||||
accounts: Array<Pick<Account, 'acct' | 'display_name' | 'emojis' | 'id'>>,
|
||||
targetName: string,
|
||||
instanceTitle: string,
|
||||
): React.ReactNode => {
|
||||
|
|
|
@ -214,6 +214,12 @@ const Preferences = () => {
|
|||
>
|
||||
<SettingToggle settings={settings} settingPath={['demetricator']} onChange={onToggleChange} />
|
||||
</ListItem>
|
||||
|
||||
{features.emojiReacts && (
|
||||
<ListItem label={<FormattedMessage id='preferences.fields.wrench_label' defaultMessage='Display wrench reaction button' />} >
|
||||
<SettingToggle settings={settings} settingPath={['showWrenchButton']} onChange={onToggleChange} />
|
||||
</ListItem>
|
||||
)}
|
||||
</List>
|
||||
|
||||
<List>
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import React, { useEffect } from 'react';
|
||||
import { defineMessages, FormattedMessage, useIntl } from 'react-intl';
|
||||
import { defineMessages, useIntl } from 'react-intl';
|
||||
|
||||
import { fetchMfa } from 'pl-fe/actions/mfa';
|
||||
import List, { ListItem } from 'pl-fe/components/list';
|
||||
|
@ -13,7 +13,7 @@ import { useOwnAccount } from 'pl-fe/hooks/useOwnAccount';
|
|||
|
||||
import Preferences from '../preferences';
|
||||
|
||||
import MessagesSettings from './components/messages-settings';
|
||||
// import MessagesSettings from './components/messages-settings';
|
||||
|
||||
const any = (arr: Array<any>): boolean => arr.some(Boolean);
|
||||
|
||||
|
@ -126,7 +126,7 @@ const Settings = () => {
|
|||
</>
|
||||
)}
|
||||
|
||||
{features.chats ? (
|
||||
{/* {features.chats ? (
|
||||
<>
|
||||
<CardHeader>
|
||||
<CardTitle title={<FormattedMessage id='column.chats' defaultMessage='Chats' />} />
|
||||
|
@ -136,7 +136,7 @@ const Settings = () => {
|
|||
<MessagesSettings />
|
||||
</CardBody>
|
||||
</>
|
||||
) : null}
|
||||
) : null} */}
|
||||
|
||||
<CardHeader>
|
||||
<CardTitle title={intl.formatMessage(messages.preferences)} />
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import React, { useRef } from 'react';
|
||||
import { FormattedDate, FormattedMessage, useIntl } from 'react-intl';
|
||||
import { defineMessages, FormattedDate, FormattedMessage, useIntl } from 'react-intl';
|
||||
import { Link } from 'react-router-dom';
|
||||
|
||||
import Account from 'pl-fe/components/account';
|
||||
|
@ -15,6 +15,7 @@ import HStack from 'pl-fe/components/ui/hstack';
|
|||
import Icon from 'pl-fe/components/ui/icon';
|
||||
import Stack from 'pl-fe/components/ui/stack';
|
||||
import Text from 'pl-fe/components/ui/text';
|
||||
import Emojify from 'pl-fe/features/emoji/emojify';
|
||||
import QuotedStatus from 'pl-fe/features/status/containers/quoted-status-container';
|
||||
|
||||
import StatusInteractionBar from './status-interaction-bar';
|
||||
|
@ -22,6 +23,10 @@ import StatusTypeIcon from './status-type-icon';
|
|||
|
||||
import type { SelectedStatus } from 'pl-fe/selectors';
|
||||
|
||||
const messages = defineMessages({
|
||||
applicationName: { id: 'status.application_name', defaultMessage: 'Sent from {name}' },
|
||||
});
|
||||
|
||||
interface IDetailedStatus {
|
||||
status: SelectedStatus;
|
||||
withMedia?: boolean;
|
||||
|
@ -62,7 +67,7 @@ const DetailedStatus: React.FC<IDetailedStatus> = ({
|
|||
<Link to={`/groups/${status.group.id}`} className='hover:underline'>
|
||||
<bdi className='truncate'>
|
||||
<strong className='text-gray-800 dark:text-gray-200'>
|
||||
<span dangerouslySetInnerHTML={{ __html: status.group.display_name_html }} />
|
||||
<Emojify text={status.account.display_name} emojis={status.account.emojis} />
|
||||
</strong>
|
||||
</bdi>
|
||||
</Link>
|
||||
|
@ -144,35 +149,40 @@ const DetailedStatus: React.FC<IDetailedStatus> = ({
|
|||
|
||||
<HStack space={1} alignItems='center'>
|
||||
<span>
|
||||
<a href={actualStatus.url} target='_blank' rel='noopener' className='hover:underline'>
|
||||
<Text tag='span' theme='muted' size='sm'>
|
||||
<Text tag='span' theme='muted' size='sm'>
|
||||
<a href={actualStatus.url} target='_blank' rel='noopener' className='hover:underline'>
|
||||
<FormattedDate value={new Date(actualStatus.created_at)} hour12 year='numeric' month='short' day='2-digit' hour='numeric' minute='2-digit' />
|
||||
</Text>
|
||||
</a>
|
||||
|
||||
{actualStatus.application && (
|
||||
<a href={(actualStatus.application.website) ? actualStatus.application.website : '#' } target='_blank' rel='noopener' className='hover:underline ml-2'>
|
||||
<Text tag='span' theme='muted' size='sm'>
|
||||
({actualStatus.application.name})
|
||||
</Text>
|
||||
</a>
|
||||
)}
|
||||
|
||||
{actualStatus.edited_at && (
|
||||
<>
|
||||
{' · '}
|
||||
<div
|
||||
className='inline hover:underline'
|
||||
onClick={handleOpenCompareHistoryModal}
|
||||
role='button'
|
||||
tabIndex={0}
|
||||
>
|
||||
<Text tag='span' theme='muted' size='sm'>
|
||||
{actualStatus.application && (
|
||||
<>
|
||||
{' · '}
|
||||
<a
|
||||
href={(actualStatus.application.website) ? actualStatus.application.website : '#'}
|
||||
target='_blank'
|
||||
rel='noopener'
|
||||
className='hover:underline'
|
||||
title={intl.formatMessage(messages.applicationName, { name: actualStatus.application.name })}
|
||||
>
|
||||
{actualStatus.application.name}
|
||||
</a>
|
||||
</>
|
||||
)}
|
||||
|
||||
{actualStatus.edited_at && (
|
||||
<>
|
||||
{' · '}
|
||||
<div
|
||||
className='inline hover:underline'
|
||||
onClick={handleOpenCompareHistoryModal}
|
||||
role='button'
|
||||
tabIndex={0}
|
||||
>
|
||||
<FormattedMessage id='status.edited' defaultMessage='Edited {date}' values={{ date: intl.formatDate(new Date(actualStatus.edited_at), { hour12: true, month: 'short', day: '2-digit', hour: 'numeric', minute: '2-digit' }) }} />
|
||||
</Text>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</Text>
|
||||
</span>
|
||||
|
||||
<StatusTypeIcon status={actualStatus} />
|
||||
|
|
|
@ -8,6 +8,7 @@ import Avatar from 'pl-fe/components/ui/avatar';
|
|||
import Button from 'pl-fe/components/ui/button';
|
||||
import HStack from 'pl-fe/components/ui/hstack';
|
||||
import { useAppDispatch } from 'pl-fe/hooks/useAppDispatch';
|
||||
import { userTouching } from 'pl-fe/is-mobile';
|
||||
import { useModalsStore } from 'pl-fe/stores/modals';
|
||||
|
||||
const ComposeButton = () => {
|
||||
|
@ -26,7 +27,10 @@ const ComposeButton = () => {
|
|||
|
||||
const HomeComposeButton = () => {
|
||||
const { openModal } = useModalsStore();
|
||||
const onOpenCompose = () => openModal('COMPOSE');
|
||||
const onOpenCompose = () => {
|
||||
if (!userTouching.matches) return window.open('/compose', 'targetWindow', 'height=500,width=700');
|
||||
openModal('COMPOSE');
|
||||
};
|
||||
|
||||
return (
|
||||
<Button
|
||||
|
|
|
@ -2,7 +2,7 @@ import React from 'react';
|
|||
import { FormattedMessage } from 'react-intl';
|
||||
|
||||
import Text from 'pl-fe/components/ui/text';
|
||||
import emojify from 'pl-fe/features/emoji';
|
||||
import Emojify from 'pl-fe/features/emoji/emojify';
|
||||
import { usePlFeConfig } from 'pl-fe/hooks/usePlFeConfig';
|
||||
import sourceCode from 'pl-fe/utils/code';
|
||||
|
||||
|
@ -12,10 +12,9 @@ const LinkFooter: React.FC = (): JSX.Element => {
|
|||
return (
|
||||
<Text theme='muted' size='sm'>
|
||||
{plFeConfig.linkFooterMessage ? (
|
||||
<span
|
||||
className='inline-block align-middle'
|
||||
dangerouslySetInnerHTML={{ __html: emojify(plFeConfig.linkFooterMessage) }}
|
||||
/>
|
||||
<span className='inline-block align-middle'>
|
||||
<Emojify text={plFeConfig.linkFooterMessage} />
|
||||
</span>
|
||||
) : (
|
||||
<FormattedMessage
|
||||
id='getting_started.open_source_notice'
|
||||
|
|
|
@ -9,6 +9,7 @@ import Modal from 'pl-fe/components/ui/modal';
|
|||
import Spinner from 'pl-fe/components/ui/spinner';
|
||||
import Stack from 'pl-fe/components/ui/stack';
|
||||
import Text from 'pl-fe/components/ui/text';
|
||||
import Emojify from 'pl-fe/features/emoji/emojify';
|
||||
import { useAppDispatch } from 'pl-fe/hooks/useAppDispatch';
|
||||
import { useAppSelector } from 'pl-fe/hooks/useAppSelector';
|
||||
|
||||
|
@ -42,8 +43,7 @@ const CompareHistoryModal: React.FC<BaseModalProps & CompareHistoryModalProps> =
|
|||
body = (
|
||||
<div className='divide-y divide-solid divide-gray-200 dark:divide-gray-800'>
|
||||
{versions?.map((version) => {
|
||||
const content = <ParsedContent html={version.contentHtml} mentions={status?.mentions} hasQuote={!!status?.quote_id} />;
|
||||
const spoilerContent = { __html: version.spoilerHtml };
|
||||
const content = <ParsedContent html={version.content} mentions={status?.mentions} hasQuote={!!status?.quote_id} emojis={version.emojis} />;
|
||||
|
||||
const poll = typeof version.poll !== 'string' && version.poll;
|
||||
|
||||
|
@ -51,7 +51,9 @@ const CompareHistoryModal: React.FC<BaseModalProps & CompareHistoryModalProps> =
|
|||
<div className='flex flex-col py-2 first:pt-0 last:pb-0'>
|
||||
{version.spoiler_text?.length > 0 && (
|
||||
<>
|
||||
<span dangerouslySetInnerHTML={spoilerContent} />
|
||||
<span>
|
||||
<Emojify text={version.spoiler_text} emojis={version.emojis} />
|
||||
</span>
|
||||
<hr />
|
||||
</>
|
||||
)}
|
||||
|
@ -71,7 +73,9 @@ const CompareHistoryModal: React.FC<BaseModalProps & CompareHistoryModalProps> =
|
|||
role='radio'
|
||||
/>
|
||||
|
||||
<span dangerouslySetInnerHTML={{ __html: option.title_emojified }} />
|
||||
<span>
|
||||
<ParsedContent html={option.title} emojis={version.emojis} />
|
||||
</span>
|
||||
</HStack>
|
||||
))}
|
||||
</Stack>
|
||||
|
|
|
@ -324,7 +324,7 @@ const ComposeEventModal: React.FC<BaseModalProps & ComposeEventModalProps> = ({
|
|||
onChange={onChangeHasEndTime}
|
||||
/>
|
||||
<Text tag='span' theme='muted'>
|
||||
<FormattedMessage id='compose_event.fields.has_end_time' defaultMessage='The event has an end date' />
|
||||
<FormattedMessage id='compose_event.fields.has_end_time' defaultMessage='This event has an end date' />
|
||||
</Text>
|
||||
</HStack>
|
||||
{endTime && (
|
||||
|
|
|
@ -6,6 +6,7 @@ import ScrollableList from 'pl-fe/components/scrollable-list';
|
|||
import Modal from 'pl-fe/components/ui/modal';
|
||||
import Spinner from 'pl-fe/components/ui/spinner';
|
||||
import AccountContainer from 'pl-fe/containers/account-container';
|
||||
import Emojify from 'pl-fe/features/emoji/emojify';
|
||||
import { useAppSelector } from 'pl-fe/hooks/useAppSelector';
|
||||
import { makeGetAccount } from 'pl-fe/selectors';
|
||||
|
||||
|
@ -31,7 +32,13 @@ const FamiliarFollowersModal: React.FC<BaseModalProps & FamiliarFollowersModalPr
|
|||
if (!account || !familiarFollowerIds) {
|
||||
body = <Spinner />;
|
||||
} else {
|
||||
const emptyMessage = <FormattedMessage id='account.familiar_followers.empty' defaultMessage='No one you know follows {name}.' values={{ name: <span dangerouslySetInnerHTML={{ __html: account.display_name_html }} /> }} />;
|
||||
const emptyMessage = (
|
||||
<FormattedMessage
|
||||
id='account.familiar_followers.empty'
|
||||
defaultMessage='No one you know follows {name}.'
|
||||
values={{ name: <span><Emojify text={account.display_name} emojis={account.emojis} /></span> }}
|
||||
/>
|
||||
);
|
||||
|
||||
body = (
|
||||
<ScrollableList
|
||||
|
@ -54,7 +61,7 @@ const FamiliarFollowersModal: React.FC<BaseModalProps & FamiliarFollowersModalPr
|
|||
<FormattedMessage
|
||||
id='column.familiar_followers'
|
||||
defaultMessage='People you know following {name}'
|
||||
values={{ name: <span dangerouslySetInnerHTML={{ __html: account?.display_name_html || '' }} /> }}
|
||||
values={{ name: !!account && <span><Emojify text={account.display_name} emojis={account.emojis} /></span> }}
|
||||
/>
|
||||
}
|
||||
onClose={onClickClose}
|
||||
|
|
|
@ -102,8 +102,8 @@ const HotkeysModal: React.FC<BaseModalProps> = ({ onClose }) => {
|
|||
label: <FormattedMessage id='keyboard_shortcuts.back' defaultMessage='to navigate back' />,
|
||||
},
|
||||
isLoggedIn && {
|
||||
key: <><Hotkey>s</Hotkey>, <Hotkey>/</Hotkey></>,
|
||||
label: <FormattedMessage id='keyboard_shortcuts.search' defaultMessage='to focus search' />,
|
||||
key: <><Hotkey>g</Hotkey> + <Hotkey>s</Hotkey></>,
|
||||
label: <FormattedMessage id='keyboard_shortcuts.search' defaultMessage='to open search page' />,
|
||||
},
|
||||
{
|
||||
key: <Hotkey>esc</Hotkey>,
|
||||
|
|
|
@ -64,7 +64,7 @@ const ConfirmationStep: React.FC<IConfirmationStep> = ({ group }) => {
|
|||
size='md'
|
||||
className='mx-auto max-w-sm [&_a]:text-primary-600 [&_a]:hover:underline [&_a]:dark:text-accent-blue'
|
||||
>
|
||||
<ParsedContent html={group.note_emojified} />
|
||||
<ParsedContent html={group.note} emojis={group.emojis} />
|
||||
</Text>
|
||||
</Stack>
|
||||
</Stack>
|
||||
|
|
|
@ -5,6 +5,7 @@ import { FormattedMessage } from 'react-intl';
|
|||
import { fetchPinnedAccounts } from 'pl-fe/actions/accounts';
|
||||
import Widget from 'pl-fe/components/ui/widget';
|
||||
import AccountContainer from 'pl-fe/containers/account-container';
|
||||
import Emojify from 'pl-fe/features/emoji/emojify';
|
||||
import { WhoToFollowPanel } from 'pl-fe/features/ui/util/async-components';
|
||||
import { useAppDispatch } from 'pl-fe/hooks/useAppDispatch';
|
||||
import { useAppSelector } from 'pl-fe/hooks/useAppSelector';
|
||||
|
@ -12,7 +13,7 @@ import { useAppSelector } from 'pl-fe/hooks/useAppSelector';
|
|||
import type { Account } from 'pl-fe/normalizers/account';
|
||||
|
||||
interface IPinnedAccountsPanel {
|
||||
account: Pick<Account, 'id' | 'display_name_html'>;
|
||||
account: Pick<Account, 'id' | 'display_name' | 'emojis'>;
|
||||
limit: number;
|
||||
}
|
||||
|
||||
|
@ -36,7 +37,7 @@ const PinnedAccountsPanel: React.FC<IPinnedAccountsPanel> = ({ account, limit })
|
|||
id='pinned_accounts.title'
|
||||
defaultMessage='{name}’s choices'
|
||||
values={{
|
||||
name: <span dangerouslySetInnerHTML={{ __html: account.display_name_html }} />,
|
||||
name: <span><Emojify text={account.display_name} emojis={account.emojis} /></span>,
|
||||
}}
|
||||
/>}
|
||||
>
|
||||
|
|
|
@ -8,7 +8,7 @@ import ProfileField from '../profile-field';
|
|||
import type { Account } from 'pl-fe/normalizers/account';
|
||||
|
||||
interface IProfileFieldsPanel {
|
||||
account: Pick<Account, 'fields'>;
|
||||
account: Pick<Account, 'emojis' | 'fields'>;
|
||||
}
|
||||
|
||||
/** Custom profile fields for sidebar. */
|
||||
|
@ -16,7 +16,7 @@ const ProfileFieldsPanel: React.FC<IProfileFieldsPanel> = ({ account }) => (
|
|||
<Widget>
|
||||
<Stack space={4}>
|
||||
{account.fields.map((field, i) => (
|
||||
<ProfileField field={field} key={i} />
|
||||
<ProfileField field={field} key={i} emojis={account.emojis} />
|
||||
))}
|
||||
</Stack>
|
||||
</Widget>
|
||||
|
|
|
@ -10,6 +10,7 @@ import HStack from 'pl-fe/components/ui/hstack';
|
|||
import Icon from 'pl-fe/components/ui/icon';
|
||||
import Stack from 'pl-fe/components/ui/stack';
|
||||
import Text from 'pl-fe/components/ui/text';
|
||||
import Emojify from 'pl-fe/features/emoji/emojify';
|
||||
import { useAppSelector } from 'pl-fe/hooks/useAppSelector';
|
||||
import { usePlFeConfig } from 'pl-fe/hooks/usePlFeConfig';
|
||||
import { capitalize } from 'pl-fe/utils/strings';
|
||||
|
@ -122,8 +123,6 @@ const ProfileInfoPanel: React.FC<IProfileInfoPanel> = ({ account, username }) =>
|
|||
);
|
||||
}
|
||||
|
||||
const deactivated = account.deactivated ?? false;
|
||||
const displayNameHtml = deactivated ? { __html: intl.formatMessage(messages.deactivated) } : { __html: account.display_name_html };
|
||||
const memberSinceDate = intl.formatDate(account.created_at, { month: 'long', year: 'numeric' });
|
||||
const badges = getBadges();
|
||||
|
||||
|
@ -132,7 +131,11 @@ const ProfileInfoPanel: React.FC<IProfileInfoPanel> = ({ account, username }) =>
|
|||
<Stack space={2}>
|
||||
<Stack>
|
||||
<HStack space={1} alignItems='center'>
|
||||
<Text size='lg' weight='bold' dangerouslySetInnerHTML={displayNameHtml} truncate />
|
||||
<Text size='lg' weight='bold' truncate>
|
||||
{account.deactivated
|
||||
? <FormattedMessage id='account.deactivated' defaultMessage='Deactivated' />
|
||||
: <Emojify text={account.display_name} emojis={account.emojis} />}
|
||||
</Text>
|
||||
|
||||
{account.bot && <Badge slug='bot' title={intl.formatMessage(messages.bot)} />}
|
||||
|
||||
|
@ -160,9 +163,9 @@ const ProfileInfoPanel: React.FC<IProfileInfoPanel> = ({ account, username }) =>
|
|||
|
||||
<ProfileStats account={account} />
|
||||
|
||||
{!!account.note_emojified && (
|
||||
{!!account.note && (
|
||||
<Markup size='sm'>
|
||||
<ParsedContent html={account.note_emojified} />
|
||||
<ParsedContent html={account.note} emojis={account.emojis} />
|
||||
</Markup>
|
||||
)}
|
||||
|
||||
|
@ -208,7 +211,7 @@ const ProfileInfoPanel: React.FC<IProfileInfoPanel> = ({ account, username }) =>
|
|||
{account.fields.length > 0 && (
|
||||
<Stack space={2} className='mt-4 xl:hidden'>
|
||||
{account.fields.map((field, i) => (
|
||||
<ProfileField field={field} key={i} />
|
||||
<ProfileField field={field} key={i} emojis={account.emojis} />
|
||||
))}
|
||||
</Stack>
|
||||
)}
|
||||
|
|
|
@ -9,6 +9,7 @@ import HStack from 'pl-fe/components/ui/hstack';
|
|||
import Stack from 'pl-fe/components/ui/stack';
|
||||
import Text from 'pl-fe/components/ui/text';
|
||||
import VerificationBadge from 'pl-fe/components/verification-badge';
|
||||
import Emojify from 'pl-fe/features/emoji/emojify';
|
||||
import { useAppSelector } from 'pl-fe/hooks/useAppSelector';
|
||||
import { useSettings } from 'pl-fe/hooks/useSettings';
|
||||
import { getAcct } from 'pl-fe/utils/accounts';
|
||||
|
@ -29,7 +30,6 @@ const UserPanel: React.FC<IUserPanel> = ({ accountId, action, badges, domain })
|
|||
const fqn = useAppSelector((state) => displayFqn(state));
|
||||
|
||||
if (!account) return null;
|
||||
const displayNameHtml = { __html: account.display_name_html };
|
||||
const acct = !account.acct.includes('@') && domain ? `${account.acct}@${domain}` : account.acct;
|
||||
const header = account.header;
|
||||
const verified = account.verified;
|
||||
|
@ -67,7 +67,9 @@ const UserPanel: React.FC<IUserPanel> = ({ accountId, action, badges, domain })
|
|||
<Stack>
|
||||
<Link to={`/@${account.acct}`}>
|
||||
<HStack space={1} alignItems='center'>
|
||||
<Text size='lg' weight='bold' dangerouslySetInnerHTML={displayNameHtml} truncate />
|
||||
<Text size='lg' weight='bold' truncate>
|
||||
<Emojify text={account.display_name} emojis={account.emojis} />
|
||||
</Text>
|
||||
|
||||
{verified && <VerificationBadge />}
|
||||
|
||||
|
|
|
@ -4,7 +4,7 @@ import React from 'react';
|
|||
import PollOption from 'pl-fe/components/polls/poll-option';
|
||||
import Stack from 'pl-fe/components/ui/stack';
|
||||
|
||||
import type { Poll } from 'pl-fe/normalizers/poll';
|
||||
import type { Poll } from 'pl-api';
|
||||
|
||||
interface IPollPreview {
|
||||
poll: Poll;
|
||||
|
|
|
@ -9,6 +9,7 @@ import HoverAccountWrapper from 'pl-fe/components/hover-account-wrapper';
|
|||
import HStack from 'pl-fe/components/ui/hstack';
|
||||
import Text from 'pl-fe/components/ui/text';
|
||||
import VerificationBadge from 'pl-fe/components/verification-badge';
|
||||
import Emojify from 'pl-fe/features/emoji/emojify';
|
||||
import { useAppDispatch } from 'pl-fe/hooks/useAppDispatch';
|
||||
import { useAppSelector } from 'pl-fe/hooks/useAppSelector';
|
||||
import { useFeatures } from 'pl-fe/hooks/useFeatures';
|
||||
|
@ -51,12 +52,9 @@ const ProfileFamiliarFollowers: React.FC<IProfileFamiliarFollowers> = ({ account
|
|||
<HoverAccountWrapper accountId={account.id} key={account.id} element='span'>
|
||||
<Link className='mention inline-block' to={`/@${account.acct}`}>
|
||||
<HStack space={1} alignItems='center' grow>
|
||||
<Text
|
||||
size='sm'
|
||||
theme='primary'
|
||||
truncate
|
||||
dangerouslySetInnerHTML={{ __html: account.display_name_html }}
|
||||
/>
|
||||
<Text size='sm' theme='primary' truncate>
|
||||
<Emojify text={account.display_name} emojis={account.emojis} />
|
||||
</Text>
|
||||
|
||||
{account.verified && <VerificationBadge />}
|
||||
</HStack>
|
||||
|
|
|
@ -3,9 +3,12 @@ import React from 'react';
|
|||
import { defineMessages, useIntl, FormatDateOptions } from 'react-intl';
|
||||
|
||||
import Markup from 'pl-fe/components/markup';
|
||||
import { ParsedContent } from 'pl-fe/components/parsed-content';
|
||||
import HStack from 'pl-fe/components/ui/hstack';
|
||||
import Icon from 'pl-fe/components/ui/icon';
|
||||
import Emojify from 'pl-fe/features/emoji/emojify';
|
||||
import { CryptoAddress, LightningAddress } from 'pl-fe/features/ui/util/async-components';
|
||||
import { unescapeHTML } from 'pl-fe/utils/html';
|
||||
|
||||
import type { Account } from 'pl-fe/normalizers/account';
|
||||
|
||||
|
@ -28,32 +31,35 @@ const dateFormatOptions: FormatDateOptions = {
|
|||
|
||||
interface IProfileField {
|
||||
field: Account['fields'][number];
|
||||
emojis?: Account['emojis'];
|
||||
}
|
||||
|
||||
/** Renders a single profile field. */
|
||||
const ProfileField: React.FC<IProfileField> = ({ field }) => {
|
||||
const ProfileField: React.FC<IProfileField> = ({ field, emojis }) => {
|
||||
const intl = useIntl();
|
||||
|
||||
if (isTicker(field.name)) {
|
||||
return (
|
||||
<CryptoAddress
|
||||
ticker={getTicker(field.name).toLowerCase()}
|
||||
address={field.value_plain}
|
||||
address={unescapeHTML(field.value)}
|
||||
/>
|
||||
);
|
||||
} else if (isZapEmoji(field.name)) {
|
||||
return <LightningAddress address={field.value_plain} />;
|
||||
return <LightningAddress address={unescapeHTML(field.value)} />;
|
||||
}
|
||||
|
||||
return (
|
||||
<dl>
|
||||
<dt title={field.name}>
|
||||
<Markup weight='bold' tag='span' dangerouslySetInnerHTML={{ __html: field.name_emojified }} />
|
||||
<Markup weight='bold' tag='span'>
|
||||
<Emojify text={field.name} emojis={emojis} />
|
||||
</Markup>
|
||||
</dt>
|
||||
|
||||
<dd
|
||||
className={clsx({ 'text-success-500': field.verified_at })}
|
||||
title={field.value_plain}
|
||||
title={unescapeHTML(field.value)}
|
||||
>
|
||||
<HStack space={2} alignItems='center'>
|
||||
{field.verified_at && (
|
||||
|
@ -62,7 +68,9 @@ const ProfileField: React.FC<IProfileField> = ({ field }) => {
|
|||
</span>
|
||||
)}
|
||||
|
||||
<Markup className='overflow-hidden break-words' tag='span' dangerouslySetInnerHTML={{ __html: field.value_emojified }} />
|
||||
<Markup className='overflow-hidden break-words' tag='span'>
|
||||
<ParsedContent html={field.value} emojis={emojis} />
|
||||
</Markup>
|
||||
</HStack>
|
||||
</dd>
|
||||
</dl>
|
||||
|
|
|
@ -83,7 +83,6 @@ import {
|
|||
Backups,
|
||||
MfaForm,
|
||||
ChatIndex,
|
||||
ChatWidget,
|
||||
ServerInfo,
|
||||
Dashboard,
|
||||
ModerationLog,
|
||||
|
@ -356,7 +355,6 @@ const UI: React.FC<IUI> = ({ children }) => {
|
|||
const node = useRef<HTMLDivElement | null>(null);
|
||||
const me = useAppSelector(state => state.me);
|
||||
const { account } = useOwnAccount();
|
||||
const features = useFeatures();
|
||||
const vapidKey = useAppSelector(state => getVapidKey(state));
|
||||
|
||||
const { isDropdownMenuOpen } = useUiStore();
|
||||
|
@ -481,13 +479,13 @@ const UI: React.FC<IUI> = ({ children }) => {
|
|||
<SidebarMenu />
|
||||
</Suspense>
|
||||
|
||||
{me && features.chats && (
|
||||
{/* {me && features.chats && (
|
||||
<div className='hidden xl:block'>
|
||||
<Suspense fallback={<div className='fixed bottom-0 z-[99] flex h-16 w-96 animate-pulse flex-col rounded-t-lg bg-white shadow-3xl dark:bg-gray-900 ltr:right-5 rtl:left-5' />}>
|
||||
<ChatWidget />
|
||||
</Suspense>
|
||||
</div>
|
||||
)}
|
||||
)} */}
|
||||
|
||||
<ThumbNavigation />
|
||||
|
||||
|
|
|
@ -14,7 +14,7 @@ import type { LexicalEditor } from 'lexical';
|
|||
const keyMap = {
|
||||
help: '?',
|
||||
new: 'n',
|
||||
search: ['/', 's'],
|
||||
search: ['/', 's', 'g s'],
|
||||
forceNew: 'option+n',
|
||||
reply: 'r',
|
||||
favourite: 'f',
|
||||
|
|
|
@ -8,6 +8,7 @@ import { ScrollContext } from 'react-router-scroll-4';
|
|||
import * as BuildConfig from 'pl-fe/build-config';
|
||||
import LoadingScreen from 'pl-fe/components/loading-screen';
|
||||
import SiteErrorBoundary from 'pl-fe/components/site-error-boundary';
|
||||
import ComposePage from 'pl-fe/features/compose';
|
||||
import { ModalRoot, OnboardingWizard } from 'pl-fe/features/ui/util/async-components';
|
||||
import { useAppSelector } from 'pl-fe/hooks/useAppSelector';
|
||||
import { useLoggedIn } from 'pl-fe/hooks/useLoggedIn';
|
||||
|
@ -46,6 +47,8 @@ const PlFeMount = () => {
|
|||
<Redirect exact from='/' to={redirectRootNoLogin} />
|
||||
)}
|
||||
|
||||
<Route path='/compose' component={ComposePage} />
|
||||
|
||||
<Route
|
||||
path='/embed/:statusId'
|
||||
render={(props) => (
|
||||
|
|
|
@ -1,14 +1,7 @@
|
|||
import clsx from 'clsx';
|
||||
import React, { useRef } from 'react';
|
||||
import { useIntl } from 'react-intl';
|
||||
import { Link } from 'react-router-dom';
|
||||
import React from 'react';
|
||||
|
||||
import { uploadCompose } from 'pl-fe/actions/compose';
|
||||
import Avatar from 'pl-fe/components/ui/avatar';
|
||||
import Card, { CardBody } from 'pl-fe/components/ui/card';
|
||||
import HStack from 'pl-fe/components/ui/hstack';
|
||||
import Layout from 'pl-fe/components/ui/layout';
|
||||
import ComposeForm from 'pl-fe/features/compose/components/compose-form';
|
||||
import LinkFooter from 'pl-fe/features/ui/components/link-footer';
|
||||
import {
|
||||
WhoToFollowPanel,
|
||||
|
@ -19,12 +12,9 @@ import {
|
|||
BirthdayPanel,
|
||||
AnnouncementsPanel,
|
||||
} from 'pl-fe/features/ui/util/async-components';
|
||||
import { useAppDispatch } from 'pl-fe/hooks/useAppDispatch';
|
||||
import { useAppSelector } from 'pl-fe/hooks/useAppSelector';
|
||||
import { useDraggedFiles } from 'pl-fe/hooks/useDraggedFiles';
|
||||
import { useFeatures } from 'pl-fe/hooks/useFeatures';
|
||||
import { useIsMobile } from 'pl-fe/hooks/useIsMobile';
|
||||
import { useOwnAccount } from 'pl-fe/hooks/useOwnAccount';
|
||||
import { usePlFeConfig } from 'pl-fe/hooks/usePlFeConfig';
|
||||
|
||||
interface IHomeLayout {
|
||||
|
@ -32,62 +22,18 @@ interface IHomeLayout {
|
|||
}
|
||||
|
||||
const HomeLayout: React.FC<IHomeLayout> = ({ children }) => {
|
||||
const intl = useIntl();
|
||||
const dispatch = useAppDispatch();
|
||||
|
||||
const me = useAppSelector(state => state.me);
|
||||
const { account } = useOwnAccount();
|
||||
const features = useFeatures();
|
||||
const plFeConfig = usePlFeConfig();
|
||||
|
||||
const composeId = 'home';
|
||||
const composeBlock = useRef<HTMLDivElement>(null);
|
||||
const isMobile = useIsMobile();
|
||||
|
||||
const hasCrypto = typeof plFeConfig.cryptoAddresses.getIn([0, 'ticker']) === 'string';
|
||||
const cryptoLimit = plFeConfig.cryptoDonatePanel.get('limit', 0);
|
||||
|
||||
const { isDragging, isDraggedOver } = useDraggedFiles(composeBlock, (files) => {
|
||||
dispatch(uploadCompose(composeId, files, intl));
|
||||
});
|
||||
|
||||
const acct = account ? account.acct : '';
|
||||
const avatar = account ? account.avatar : '';
|
||||
|
||||
return (
|
||||
<>
|
||||
<Layout.Main className={clsx('black:space-y-0 dark:divide-gray-800', { 'pt-3 sm:pt-0 space-y-3': !isMobile })}>
|
||||
{me && (
|
||||
<Card
|
||||
className={clsx('relative z-[1] border-gray-200 transition black:border-b black:border-gray-800 dark:border-gray-800', {
|
||||
'border-2 border-primary-600 border-dashed z-[99]': isDragging,
|
||||
'ring-2 ring-offset-2 ring-primary-600': isDraggedOver,
|
||||
'border-b': isMobile,
|
||||
})}
|
||||
variant='rounded'
|
||||
ref={composeBlock}
|
||||
>
|
||||
<CardBody>
|
||||
<HStack alignItems='start' space={2}>
|
||||
<Link to={`/@${acct}`}>
|
||||
<Avatar src={avatar} alt={account?.avatar_description} size={42} />
|
||||
</Link>
|
||||
|
||||
<div className='w-full translate-y-0.5'>
|
||||
<ComposeForm
|
||||
id={composeId}
|
||||
shouldCondense
|
||||
autoFocus={false}
|
||||
clickableAreaRef={composeBlock}
|
||||
withAvatar
|
||||
transparent
|
||||
/>
|
||||
</div>
|
||||
</HStack>
|
||||
</CardBody>
|
||||
</Card>
|
||||
)}
|
||||
|
||||
{children}
|
||||
</Layout.Main>
|
||||
|
||||
|
|
|
@ -1538,12 +1538,10 @@
|
|||
"status.reply_all": "",
|
||||
"status.report": "Κατάγγειλε @{name}",
|
||||
"status.sensitive_warning": "Ευαίσθητο περιεχόμενο",
|
||||
"status.sensitive_warning.subtitle": "This content may not be suitable for all audiences.",
|
||||
"status.share": "Μοιράσου",
|
||||
"status.show_filter_reason": "",
|
||||
"status.show_less_all": "Δείξε λιγότερα για όλα",
|
||||
"status.show_more_all": "Δείξε περισσότερα για όλα",
|
||||
"status.show_original": "Show original",
|
||||
"status.spoiler.collapse": "",
|
||||
"status.spoiler.expand": "",
|
||||
"status.title": "Post",
|
||||
|
|
|
@ -1538,12 +1538,10 @@
|
|||
"status.reply_all": "",
|
||||
"status.report": "𐑮𐑦𐑐𐑹𐑑 @{name}",
|
||||
"status.sensitive_warning": "𐑕𐑧𐑯𐑕𐑦𐑑𐑦𐑝 𐑒𐑪𐑯𐑑𐑧𐑯𐑑",
|
||||
"status.sensitive_warning.subtitle": "This content may not be suitable for all audiences.",
|
||||
"status.share": "𐑖𐑺",
|
||||
"status.show_filter_reason": "",
|
||||
"status.show_less_all": "𐑖𐑴 𐑤𐑧𐑕 𐑓 𐑷𐑤",
|
||||
"status.show_more_all": "𐑖𐑴 𐑥𐑹 𐑓 𐑷𐑤",
|
||||
"status.show_original": "Show original",
|
||||
"status.spoiler.collapse": "",
|
||||
"status.spoiler.expand": "",
|
||||
"status.title": "𐑐𐑴𐑕𐑑",
|
||||
|
|
|
@ -439,7 +439,7 @@
|
|||
"compose_event.fields.description_placeholder": "Description",
|
||||
"compose_event.fields.end_time_label": "Event end date",
|
||||
"compose_event.fields.end_time_placeholder": "Event ends on…",
|
||||
"compose_event.fields.has_end_time": "The event has an end date",
|
||||
"compose_event.fields.has_end_time": "This event has an end date",
|
||||
"compose_event.fields.location_label": "Event location",
|
||||
"compose_event.fields.name_label": "Event name",
|
||||
"compose_event.fields.name_placeholder": "Name",
|
||||
|
@ -879,16 +879,20 @@
|
|||
"icon_button.icons": "Icons",
|
||||
"icon_button.label": "Select icon",
|
||||
"import_data.actions.import": "Import",
|
||||
"import_data.actions.import_archive": "Import archive",
|
||||
"import_data.actions.import_blocks": "Import blocks",
|
||||
"import_data.actions.import_follows": "Import follows",
|
||||
"import_data.actions.import_mutes": "Import mutes",
|
||||
"import_data.archive_label": "Archive",
|
||||
"import_data.blocks_label": "Blocks",
|
||||
"import_data.follows_label": "Follows",
|
||||
"import_data.hints.archive": "Archive containing an archive of statuses",
|
||||
"import_data.hints.blocks": "CSV file containing a list of blocked accounts",
|
||||
"import_data.hints.follows": "CSV file containing a list of followed accounts",
|
||||
"import_data.hints.mutes": "CSV file containing a list of muted accounts",
|
||||
"import_data.mutes_label": "Mutes",
|
||||
"import_data.overwrite": "Overwrite instead of appending",
|
||||
"import_data.success.archive": "Archive imported successfully",
|
||||
"import_data.success.blocks": "Blocks imported successfully",
|
||||
"import_data.success.followers": "Followers imported successfully",
|
||||
"import_data.success.mutes": "Mutes imported successfully",
|
||||
|
@ -958,7 +962,7 @@
|
|||
"keyboard_shortcuts.react": "to react",
|
||||
"keyboard_shortcuts.reply": "to reply",
|
||||
"keyboard_shortcuts.requests": "to open follow requests list",
|
||||
"keyboard_shortcuts.search": "to focus search",
|
||||
"keyboard_shortcuts.search": "to open search page",
|
||||
"keyboard_shortcuts.toggle_hidden": "to show/hide text behind CW",
|
||||
"keyboard_shortcuts.toggle_sensitivity": "to show/hide media",
|
||||
"keyboard_shortcuts.toot": "to start a new post",
|
||||
|
@ -1061,7 +1065,6 @@
|
|||
"mute_modal.auto_expire": "Automatically expire mute?",
|
||||
"mute_modal.duration": "Duration",
|
||||
"mute_modal.hide_notifications": "Hide notifications from this user?",
|
||||
"navigation.chats": "Chats",
|
||||
"navigation.compose": "Compose",
|
||||
"navigation.compose_group": "Compose to group",
|
||||
"navigation.dashboard": "Dashboard",
|
||||
|
@ -1256,6 +1259,7 @@
|
|||
"preferences.fields.theme": "Theme",
|
||||
"preferences.fields.underline_links_label": "Always underline links in posts",
|
||||
"preferences.fields.unfollow_modal_label": "Show confirmation dialog before unfollowing someone",
|
||||
"preferences.fields.wrench_label": "Display wrench reaction button",
|
||||
"preferences.hints.demetricator": "Decrease social media anxiety by hiding all numbers from the site.",
|
||||
"preferences.notifications.advanced": "Show all notification categories",
|
||||
"preferences.options.content_type_html": "HTML",
|
||||
|
@ -1433,6 +1437,7 @@
|
|||
"status.add_known_language": "Do not auto-translate posts in {language}.",
|
||||
"status.admin_account": "Moderate @{name}",
|
||||
"status.admin_status": "Open this post in the moderation interface",
|
||||
"status.application_name": "Sent from {name}",
|
||||
"status.approval.pending": "Pending approval",
|
||||
"status.approval.rejected": "Rejected",
|
||||
"status.bookmark": "Bookmark",
|
||||
|
@ -1493,7 +1498,6 @@
|
|||
"status.show_filter_reason": "Show anyway",
|
||||
"status.show_less_all": "Show less for all",
|
||||
"status.show_more_all": "Show more for all",
|
||||
"status.show_original": "Show original",
|
||||
"status.spoiler.collapse": "Collapse",
|
||||
"status.spoiler.expand": "Expand",
|
||||
"status.title": "Post details",
|
||||
|
@ -1512,6 +1516,7 @@
|
|||
"status.visibility.local": "The post is only visible to users on your instance",
|
||||
"status.visibility.mutuals_only": "The post is only visible to people who mutually follow the author",
|
||||
"status.visibility.private": "The post is only visible to followers of the author",
|
||||
"status.wrench": "Wrench reaction",
|
||||
"status_list.queue_label": "Click to see {count} new {count, plural, one {post} other {posts}}",
|
||||
"statuses.quote_tombstone": "Post is unavailable.",
|
||||
"statuses.tombstone": "One or more posts are unavailable.",
|
||||
|
|
|
@ -1649,12 +1649,10 @@
|
|||
"status.reply_all": "",
|
||||
"status.report": "Reportar",
|
||||
"status.sensitive_warning": "Contenido sensible",
|
||||
"status.sensitive_warning.subtitle": "This content may not be suitable for all audiences.",
|
||||
"status.share": "Compartir",
|
||||
"status.show_filter_reason": "Mostrar de todos modos",
|
||||
"status.show_less_all": "Mostrar menos para todo",
|
||||
"status.show_more_all": "Mostrar más para todo",
|
||||
"status.show_original": "Show original",
|
||||
"status.spoiler.collapse": "",
|
||||
"status.spoiler.expand": "",
|
||||
"status.title": "Post",
|
||||
|
|
|
@ -1538,12 +1538,10 @@
|
|||
"status.reply_all": "",
|
||||
"status.report": "گزارش دادن @{name}",
|
||||
"status.sensitive_warning": "محتوای حساس",
|
||||
"status.sensitive_warning.subtitle": "This content may not be suitable for all audiences.",
|
||||
"status.share": "همرسانی",
|
||||
"status.show_filter_reason": "",
|
||||
"status.show_less_all": "نمایش کمتر همه",
|
||||
"status.show_more_all": "نمایش بیشتر همه",
|
||||
"status.show_original": "Show original",
|
||||
"status.spoiler.collapse": "",
|
||||
"status.spoiler.expand": "",
|
||||
"status.title": "Post",
|
||||
|
|
|
@ -1567,12 +1567,10 @@
|
|||
"status.reply_all": "",
|
||||
"status.report": "Signaler @{name}",
|
||||
"status.sensitive_warning": "Contenu sensible",
|
||||
"status.sensitive_warning.subtitle": "This content may not be suitable for all audiences.",
|
||||
"status.share": "Partager",
|
||||
"status.show_filter_reason": "",
|
||||
"status.show_less_all": "Tout replier",
|
||||
"status.show_more_all": "Tout déplier",
|
||||
"status.show_original": "Show original",
|
||||
"status.spoiler.collapse": "",
|
||||
"status.spoiler.expand": "",
|
||||
"status.title": "Détail de la publication",
|
||||
|
|
|
@ -497,8 +497,6 @@
|
|||
"compose_form.sensitive.marked": "",
|
||||
"compose_form.sensitive.unmarked": "",
|
||||
"compose_form.spoiler.marked": "Text is hidden behind warning",
|
||||
"compose_form.spoiler.unmarked": "Text is not hidden",
|
||||
"compose_form.spoiler_placeholder": "Write your warning here",
|
||||
"compose_form.spoiler_remove": "Remove sensitive",
|
||||
"compose_form.spoiler_title": "Sensitive content",
|
||||
"confirmation_modal.cancel": "Cancel",
|
||||
|
@ -1538,12 +1536,10 @@
|
|||
"status.reply_all": "",
|
||||
"status.report": "Report @{name}",
|
||||
"status.sensitive_warning": "Sensitive content",
|
||||
"status.sensitive_warning.subtitle": "This content may not be suitable for all audiences.",
|
||||
"status.share": "Share",
|
||||
"status.show_filter_reason": "",
|
||||
"status.show_less_all": "Show less for all",
|
||||
"status.show_more_all": "Show more for all",
|
||||
"status.show_original": "Show original",
|
||||
"status.spoiler.collapse": "",
|
||||
"status.spoiler.expand": "",
|
||||
"status.title": "Post",
|
||||
|
|
|
@ -1596,12 +1596,10 @@
|
|||
"status.reply_all": "",
|
||||
"status.report": "דיווח על @{name}",
|
||||
"status.sensitive_warning": "תוכן רגיש",
|
||||
"status.sensitive_warning.subtitle": "This content may not be suitable for all audiences.",
|
||||
"status.share": "שיתוף",
|
||||
"status.show_filter_reason": "",
|
||||
"status.show_less_all": "הצג פחות לכולם",
|
||||
"status.show_more_all": "הראה יותר לכולם",
|
||||
"status.show_original": "Show original",
|
||||
"status.spoiler.collapse": "",
|
||||
"status.spoiler.expand": "",
|
||||
"status.title": "פוסט",
|
||||
|
|
|
@ -497,8 +497,6 @@
|
|||
"compose_form.sensitive.marked": "",
|
||||
"compose_form.sensitive.unmarked": "",
|
||||
"compose_form.spoiler.marked": "Text is hidden behind warning",
|
||||
"compose_form.spoiler.unmarked": "Text is not hidden",
|
||||
"compose_form.spoiler_placeholder": "Write your warning here",
|
||||
"compose_form.spoiler_remove": "Remove sensitive",
|
||||
"compose_form.spoiler_title": "Sensitive content",
|
||||
"confirmation_modal.cancel": "Cancel",
|
||||
|
@ -1538,12 +1536,10 @@
|
|||
"status.reply_all": "",
|
||||
"status.report": "Report @{name}",
|
||||
"status.sensitive_warning": "Sensitive content",
|
||||
"status.sensitive_warning.subtitle": "This content may not be suitable for all audiences.",
|
||||
"status.share": "Share",
|
||||
"status.show_filter_reason": "",
|
||||
"status.show_less_all": "Show less for all",
|
||||
"status.show_more_all": "Show more for all",
|
||||
"status.show_original": "Show original",
|
||||
"status.spoiler.collapse": "",
|
||||
"status.spoiler.expand": "",
|
||||
"status.title": "Post",
|
||||
|
|
|
@ -1561,12 +1561,10 @@
|
|||
"status.reply_all": "",
|
||||
"status.report": "Prijavi @{name}",
|
||||
"status.sensitive_warning": "Osjetljiv sadržaj",
|
||||
"status.sensitive_warning.subtitle": "This content may not be suitable for all audiences.",
|
||||
"status.share": "Share",
|
||||
"status.show_filter_reason": "",
|
||||
"status.show_less_all": "Show less for all",
|
||||
"status.show_more_all": "Prikaži više za sve",
|
||||
"status.show_original": "Show original",
|
||||
"status.spoiler.collapse": "",
|
||||
"status.spoiler.expand": "",
|
||||
"status.title": "Post",
|
||||
|
|
|
@ -1538,12 +1538,10 @@
|
|||
"status.reply_all": "",
|
||||
"status.report": "@{name} jelentése",
|
||||
"status.sensitive_warning": "Szenzitív tartalom",
|
||||
"status.sensitive_warning.subtitle": "This content may not be suitable for all audiences.",
|
||||
"status.share": "Megosztás",
|
||||
"status.show_filter_reason": "",
|
||||
"status.show_less_all": "Kevesebbet mindenhol",
|
||||
"status.show_more_all": "Többet mindenhol",
|
||||
"status.show_original": "Show original",
|
||||
"status.spoiler.collapse": "",
|
||||
"status.spoiler.expand": "",
|
||||
"status.title": "Post",
|
||||
|
|
|
@ -497,7 +497,6 @@
|
|||
"compose_form.sensitive.marked": "",
|
||||
"compose_form.sensitive.unmarked": "",
|
||||
"compose_form.spoiler.marked": "Text is hidden behind warning",
|
||||
"compose_form.spoiler.unmarked": "Text is not hidden",
|
||||
"compose_form.spoiler_placeholder": "Averto di kontenajo",
|
||||
"compose_form.spoiler_remove": "Remove sensitive",
|
||||
"compose_form.spoiler_title": "Sensitive content",
|
||||
|
@ -1538,12 +1537,10 @@
|
|||
"status.reply_all": "",
|
||||
"status.report": "Denuncar @{name}",
|
||||
"status.sensitive_warning": "Trubliva kontenajo",
|
||||
"status.sensitive_warning.subtitle": "This content may not be suitable for all audiences.",
|
||||
"status.share": "Share",
|
||||
"status.show_filter_reason": "",
|
||||
"status.show_less_all": "Show less for all",
|
||||
"status.show_more_all": "Show more for all",
|
||||
"status.show_original": "Show original",
|
||||
"status.spoiler.collapse": "",
|
||||
"status.spoiler.expand": "",
|
||||
"status.title": "Post",
|
||||
|
|
|
@ -1538,12 +1538,10 @@
|
|||
"status.reply_all": "",
|
||||
"status.report": "Kæra @{name}",
|
||||
"status.sensitive_warning": "Viðkvæmt efni",
|
||||
"status.sensitive_warning.subtitle": "This content may not be suitable for all audiences.",
|
||||
"status.share": "Deila",
|
||||
"status.show_filter_reason": "",
|
||||
"status.show_less_all": "Sýna minna fyrir allt",
|
||||
"status.show_more_all": "Sýna meira fyrir allt",
|
||||
"status.show_original": "Show original",
|
||||
"status.spoiler.collapse": "",
|
||||
"status.spoiler.expand": "",
|
||||
"status.title": "Færsla",
|
||||
|
|
|
@ -1538,12 +1538,10 @@
|
|||
"status.reply_all": "",
|
||||
"status.report": "Шағым @{name}",
|
||||
"status.sensitive_warning": "Нәзік контент",
|
||||
"status.sensitive_warning.subtitle": "This content may not be suitable for all audiences.",
|
||||
"status.share": "Бөлісу",
|
||||
"status.show_filter_reason": "",
|
||||
"status.show_less_all": "Бәрін аздап көрсет",
|
||||
"status.show_more_all": "Бәрін толығымен",
|
||||
"status.show_original": "Show original",
|
||||
"status.spoiler.collapse": "",
|
||||
"status.spoiler.expand": "",
|
||||
"status.title": "Post",
|
||||
|
|
|
@ -1538,12 +1538,10 @@
|
|||
"status.reply_all": "",
|
||||
"status.report": "신고",
|
||||
"status.sensitive_warning": "민감한 미디어",
|
||||
"status.sensitive_warning.subtitle": "This content may not be suitable for all audiences.",
|
||||
"status.share": "공유",
|
||||
"status.show_filter_reason": "",
|
||||
"status.show_less_all": "모두 접기",
|
||||
"status.show_more_all": "모두 펼치기",
|
||||
"status.show_original": "Show original",
|
||||
"status.spoiler.collapse": "",
|
||||
"status.spoiler.expand": "",
|
||||
"status.title": "Post",
|
||||
|
|
|
@ -497,8 +497,6 @@
|
|||
"compose_form.sensitive.marked": "",
|
||||
"compose_form.sensitive.unmarked": "",
|
||||
"compose_form.spoiler.marked": "Text is hidden behind warning",
|
||||
"compose_form.spoiler.unmarked": "Text is not hidden",
|
||||
"compose_form.spoiler_placeholder": "Write your warning here",
|
||||
"compose_form.spoiler_remove": "Remove sensitive",
|
||||
"compose_form.spoiler_title": "Sensitive content",
|
||||
"confirmation_modal.cancel": "Cancel",
|
||||
|
@ -1538,12 +1536,10 @@
|
|||
"status.reply_all": "",
|
||||
"status.report": "Report @{name}",
|
||||
"status.sensitive_warning": "Sensitive content",
|
||||
"status.sensitive_warning.subtitle": "This content may not be suitable for all audiences.",
|
||||
"status.share": "Share",
|
||||
"status.show_filter_reason": "",
|
||||
"status.show_less_all": "Show less for all",
|
||||
"status.show_more_all": "Show more for all",
|
||||
"status.show_original": "Show original",
|
||||
"status.spoiler.collapse": "",
|
||||
"status.spoiler.expand": "",
|
||||
"status.title": "Post",
|
||||
|
|
|
@ -1538,12 +1538,10 @@
|
|||
"status.reply_all": "",
|
||||
"status.report": "Report @{name}",
|
||||
"status.sensitive_warning": "Sensitive content",
|
||||
"status.sensitive_warning.subtitle": "This content may not be suitable for all audiences.",
|
||||
"status.share": "Share",
|
||||
"status.show_filter_reason": "",
|
||||
"status.show_less_all": "Show less for all",
|
||||
"status.show_more_all": "Show more for all",
|
||||
"status.show_original": "Show original",
|
||||
"status.spoiler.collapse": "",
|
||||
"status.spoiler.expand": "",
|
||||
"status.title": "Post",
|
||||
|
|
|
@ -497,8 +497,6 @@
|
|||
"compose_form.sensitive.marked": "",
|
||||
"compose_form.sensitive.unmarked": "",
|
||||
"compose_form.spoiler.marked": "Text is hidden behind warning",
|
||||
"compose_form.spoiler.unmarked": "Text is not hidden",
|
||||
"compose_form.spoiler_placeholder": "Write your warning here",
|
||||
"compose_form.spoiler_remove": "Remove sensitive",
|
||||
"compose_form.spoiler_title": "Sensitive content",
|
||||
"confirmation_modal.cancel": "Cancel",
|
||||
|
@ -1538,12 +1536,10 @@
|
|||
"status.reply_all": "",
|
||||
"status.report": "Report @{name}",
|
||||
"status.sensitive_warning": "Sensitive content",
|
||||
"status.sensitive_warning.subtitle": "This content may not be suitable for all audiences.",
|
||||
"status.share": "Share",
|
||||
"status.show_filter_reason": "",
|
||||
"status.show_less_all": "Show less for all",
|
||||
"status.show_more_all": "Show more for all",
|
||||
"status.show_original": "Show original",
|
||||
"status.spoiler.collapse": "",
|
||||
"status.spoiler.expand": "",
|
||||
"status.title": "Post",
|
||||
|
|
|
@ -497,8 +497,6 @@
|
|||
"compose_form.sensitive.marked": "",
|
||||
"compose_form.sensitive.unmarked": "",
|
||||
"compose_form.spoiler.marked": "Text is hidden behind warning",
|
||||
"compose_form.spoiler.unmarked": "Text is not hidden",
|
||||
"compose_form.spoiler_placeholder": "Write your warning here",
|
||||
"compose_form.spoiler_remove": "Remove sensitive",
|
||||
"compose_form.spoiler_title": "Sensitive content",
|
||||
"confirmation_modal.cancel": "Cancel",
|
||||
|
@ -1538,12 +1536,10 @@
|
|||
"status.reply_all": "",
|
||||
"status.report": "Report @{name}",
|
||||
"status.sensitive_warning": "Sensitive content",
|
||||
"status.sensitive_warning.subtitle": "This content may not be suitable for all audiences.",
|
||||
"status.share": "Share",
|
||||
"status.show_filter_reason": "",
|
||||
"status.show_less_all": "Show less for all",
|
||||
"status.show_more_all": "Show more for all",
|
||||
"status.show_original": "Show original",
|
||||
"status.spoiler.collapse": "",
|
||||
"status.spoiler.expand": "",
|
||||
"status.title": "Post",
|
||||
|
|
|
@ -1538,12 +1538,10 @@
|
|||
"status.reply_all": "",
|
||||
"status.report": "Report @{name}",
|
||||
"status.sensitive_warning": "Sensitive content",
|
||||
"status.sensitive_warning.subtitle": "This content may not be suitable for all audiences.",
|
||||
"status.share": "Share",
|
||||
"status.show_filter_reason": "",
|
||||
"status.show_less_all": "Show less for all",
|
||||
"status.show_more_all": "Show more for all",
|
||||
"status.show_original": "Show original",
|
||||
"status.spoiler.collapse": "",
|
||||
"status.spoiler.expand": "",
|
||||
"status.title": "Post",
|
||||
|
|
|
@ -1538,12 +1538,10 @@
|
|||
"status.reply_all": "",
|
||||
"status.report": "Senhalar @{name}",
|
||||
"status.sensitive_warning": "Contengut sensible",
|
||||
"status.sensitive_warning.subtitle": "This content may not be suitable for all audiences.",
|
||||
"status.share": "Partejar",
|
||||
"status.show_filter_reason": "",
|
||||
"status.show_less_all": "Los tornar plegar totes",
|
||||
"status.show_more_all": "Los desplegar totes",
|
||||
"status.show_original": "Show original",
|
||||
"status.spoiler.collapse": "",
|
||||
"status.spoiler.expand": "",
|
||||
"status.title": "Post",
|
||||
|
|
|
@ -1104,7 +1104,7 @@
|
|||
"navigation_bar.preferences": "Preferencje",
|
||||
"navigation_bar.profile_directory": "Katalog profilów",
|
||||
"new_event_panel.action": "Utwórz wydarzenie",
|
||||
"new_event_panel.subtitle": "Nie możesz znaleźć tego, czeko szukasz? Utwórz własne wydarzenie.",
|
||||
"new_event_panel.subtitle": "Nie możesz znaleźć tego, czego szukasz? Zaplanuj własne wydarzenie.",
|
||||
"new_event_panel.title": "Utwórz nowe wydarzenie",
|
||||
"new_group_panel.action": "Utwórz grupę",
|
||||
"new_group_panel.subtitle": "Nie możesz znaleźć tego, czego szukasz? Utwórz własną prywatną lub publiczną grupę.",
|
||||
|
@ -1112,15 +1112,15 @@
|
|||
"notification.admin.report": "{name} zgłosił(a) {target}",
|
||||
"notification.admin.sign_up": "{name} zarejestrował(a) się",
|
||||
"notification.bite": "{name} ugryzł(a) Cię",
|
||||
"notification.favourite": "{name} dodał(a) Twój wpis do ulubionych",
|
||||
"notification.follow": "{name} zaczął(-ęła) Cię obserwować",
|
||||
"notification.follow_request": "{name} poprosił(a) Cię o możliwość obserwacji",
|
||||
"notification.favourite": "{name} {count, plural, one {dodał(a)} other {dodali}} Twój wpis do ulubionych",
|
||||
"notification.follow": "{name} {count, plural, one {zaczął(-ęła)} other {zaczęli}} Cię obserwować",
|
||||
"notification.follow_request": "{name} {count, plural, one {poprosił(a)} other {poprosili}} Cię o możliwość obserwacji",
|
||||
"notification.mention": "{name} wspomniał(a) o tobie",
|
||||
"notification.moderation_warning": "Otrzymałeś(-aś) ostrzeżenie moderacyjne",
|
||||
"notification.more": "{count} więcej",
|
||||
"notification.move": "{name} przeniósł(-osła) się na {targetName}",
|
||||
"notification.pleroma:chat_mention": "{name} wysłał(a) Ci wiadomośść",
|
||||
"notification.pleroma:emoji_reaction": "{name} zareagował(a) na Twój wpis",
|
||||
"notification.more": "{count, plural, one {# inny użytkownik} other {# innych użytkowników}}",
|
||||
"notification.move": "{name} {count, plural, one {przeniosł(a)} other {przenieśli}} się na {targetName}",
|
||||
"notification.pleroma:chat_mention": "{name} {count, plural, one {wysłał(a)} other {wysłali}} Ci wiadomośść",
|
||||
"notification.pleroma:emoji_reaction": "{name} {count, plural, one {zareagował(a)} other {zareagowali}} na Twój wpis",
|
||||
"notification.pleroma:event_reminder": "Wydarzenie w którym bierzesz udział wkrótce się zaczyna",
|
||||
"notification.pleroma:participation_accepted": "Twoje zgłoszenie udziału do wydarzenia zostało przyjęte",
|
||||
"notification.pleroma:participation_request": "{name} chce wziąć udział w Twoim wydarzeniu",
|
||||
|
@ -1130,6 +1130,7 @@
|
|||
"notification.severed_relationships": "Utracono połączenia z {name}",
|
||||
"notification.status": "{name} właśnie opublikował(a) wpis",
|
||||
"notification.update": "{name} zedytował(a) wpis, który podbiłeś(-aś)",
|
||||
"notification.user_approved": "Witamy w {instance}!",
|
||||
"notifications.filter.all": "Wszystkie",
|
||||
"notifications.filter.boosts": "Podbicia",
|
||||
"notifications.filter.events": "Wydarzenia",
|
||||
|
@ -1490,7 +1491,7 @@
|
|||
"status.read_more": "Czytaj dalej",
|
||||
"status.reblog": "Podbij",
|
||||
"status.reblog_private": "Podbij dla odbiorców oryginalnego wpisu",
|
||||
"status.reblogged_by": "{name} podbił(a)",
|
||||
"status.reblogged_by": "{name} {count, plural, one {podbił(a)} other {podbili}}",
|
||||
"status.reblogged_by_with_group": "{name} udostępnił(a) z {group}",
|
||||
"status.reblogs.empty": "Nikt nie podbił jeszcze tego wpisu. Gdy ktoś to zrobi, pojawi się tutaj.",
|
||||
"status.redraft": "Usuń i przeredaguj",
|
||||
|
|
|
@ -1538,12 +1538,10 @@
|
|||
"status.reply_all": "",
|
||||
"status.report": "Denunciar @{name}",
|
||||
"status.sensitive_warning": "Conteúdo sensível",
|
||||
"status.sensitive_warning.subtitle": "This content may not be suitable for all audiences.",
|
||||
"status.share": "Compartilhar",
|
||||
"status.show_filter_reason": "",
|
||||
"status.show_less_all": "Mostrar menos para todas as mensagens",
|
||||
"status.show_more_all": "Mostrar mais para todas as mensagens",
|
||||
"status.show_original": "Show original",
|
||||
"status.spoiler.collapse": "",
|
||||
"status.spoiler.expand": "",
|
||||
"status.title": "Post",
|
||||
|
|
|
@ -1538,12 +1538,10 @@
|
|||
"status.reply_all": "",
|
||||
"status.report": "Denunciar @{name}",
|
||||
"status.sensitive_warning": "Conteúdo sensível",
|
||||
"status.sensitive_warning.subtitle": "This content may not be suitable for all audiences.",
|
||||
"status.share": "Partilhar",
|
||||
"status.show_filter_reason": "",
|
||||
"status.show_less_all": "Mostrar menos para todas",
|
||||
"status.show_more_all": "Mostrar mais para todas",
|
||||
"status.show_original": "Show original",
|
||||
"status.spoiler.collapse": "",
|
||||
"status.spoiler.expand": "",
|
||||
"status.title": "Post",
|
||||
|
|
|
@ -1538,12 +1538,10 @@
|
|||
"status.reply_all": "",
|
||||
"status.report": "Raportează @{name}",
|
||||
"status.sensitive_warning": "Conținut sensibil",
|
||||
"status.sensitive_warning.subtitle": "This content may not be suitable for all audiences.",
|
||||
"status.share": "Distribuie",
|
||||
"status.show_filter_reason": "",
|
||||
"status.show_less_all": "Arată mai puțin pentru toți",
|
||||
"status.show_more_all": "Arată mai mult pentru toți",
|
||||
"status.show_original": "Show original",
|
||||
"status.spoiler.collapse": "",
|
||||
"status.spoiler.expand": "",
|
||||
"status.title": "Post",
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue