pl-fe: Move emojify to status content parser
Signed-off-by: marcin mikołajczak <git@mkljczk.pl>
This commit is contained in:
parent
5b6599f98d
commit
9b40c46d59
23 changed files with 64 additions and 357 deletions
|
@ -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: [],
|
||||
});
|
||||
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -156,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;
|
||||
|
|
|
@ -7,6 +7,7 @@ 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 Emojify from 'pl-fe/features/emoji/emojify';
|
||||
import { useAppDispatch } from 'pl-fe/hooks/useAppDispatch';
|
||||
import { useSettings } from 'pl-fe/hooks/useSettings';
|
||||
import { onlyEmoji as isOnlyEmoji } from 'pl-fe/utils/rich-content';
|
||||
|
@ -106,13 +107,13 @@ const StatusContent: React.FC<IStatusContent> = React.memo(({
|
|||
maybeSetOnlyEmoji();
|
||||
});
|
||||
|
||||
const parsedHtml = useMemo(
|
||||
const content = 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],
|
||||
: (status.content_map && status.currentLanguage)
|
||||
? (status.content_map[status.currentLanguage] || status.content)
|
||||
: status.content,
|
||||
[status.content, status.translation, status.currentLanguage],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
|
@ -121,9 +122,9 @@ const StatusContent: React.FC<IStatusContent> = React.memo(({
|
|||
|
||||
const withSpoiler = status.spoiler_text.length > 0;
|
||||
|
||||
const spoilerText = status.spoilerMapHtml && status.currentLanguage
|
||||
? status.spoilerMapHtml[status.currentLanguage] || status.spoilerHtml
|
||||
: status.spoilerHtml;
|
||||
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', {
|
||||
|
@ -142,11 +143,9 @@ const StatusContent: React.FC<IStatusContent> = React.memo(({
|
|||
if (spoilerText) {
|
||||
output.push(
|
||||
<Text key='spoiler' size='2xl' weight='medium'>
|
||||
<span
|
||||
className={clsx({ 'line-clamp-3': !expanded && lineClamp })}
|
||||
dangerouslySetInnerHTML={{ __html: spoilerText }}
|
||||
ref={spoilerNode}
|
||||
/>
|
||||
<span className={clsx({ 'line-clamp-3': !expanded && lineClamp })} ref={spoilerNode}>
|
||||
<Emojify text={spoilerText} emojis={status.emojis} />
|
||||
</span>
|
||||
{status.content && expandable && (
|
||||
<Button
|
||||
className='ml-2 align-middle'
|
||||
|
@ -179,7 +178,7 @@ const StatusContent: React.FC<IStatusContent> = React.memo(({
|
|||
lang={status.language || undefined}
|
||||
size={textSize}
|
||||
>
|
||||
<ParsedContent html={parsedHtml} mentions={status.mentions} hasQuote={!!status.quote_id} />
|
||||
<ParsedContent html={content} mentions={status.mentions} hasQuote={!!status.quote_id} emojis={status.emojis} />
|
||||
</Markup>,
|
||||
);
|
||||
}
|
||||
|
@ -207,7 +206,7 @@ const StatusContent: React.FC<IStatusContent> = React.memo(({
|
|||
lang={status.language || undefined}
|
||||
size={textSize}
|
||||
>
|
||||
<ParsedContent html={parsedHtml} mentions={status.mentions} hasQuote={!!status.quote_id} />
|
||||
<ParsedContent html={content} mentions={status.mentions} hasQuote={!!status.quote_id} emojis={status.emojis} />
|
||||
</Markup>,
|
||||
);
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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));
|
||||
|
||||
|
|
|
@ -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 && (
|
||||
|
|
|
@ -9,7 +9,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';
|
||||
import Emojify from 'pl-fe/features/emoji/emojify';
|
||||
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'>
|
||||
<Emojify text={content} emojis={chatMessage.emojis} />
|
||||
</Text>
|
||||
</div>
|
||||
</HStack>
|
||||
)}
|
||||
|
|
|
@ -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 && (
|
||||
|
|
|
@ -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>[] = [];
|
||||
|
||||
|
@ -226,5 +80,4 @@ export {
|
|||
isNativeEmoji,
|
||||
buildCustomEmojis,
|
||||
validEmojiChar,
|
||||
emojify as default,
|
||||
};
|
||||
|
|
|
@ -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' />
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
|
|
|
@ -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'
|
||||
|
|
|
@ -43,7 +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 content = <ParsedContent html={version.content} mentions={status?.mentions} hasQuote={!!status?.quote_id} emojis={version.emojis} />;
|
||||
|
||||
const poll = typeof version.poll !== 'string' && version.poll;
|
||||
|
||||
|
|
|
@ -1,20 +0,0 @@
|
|||
import emojify from 'pl-fe/features/emoji';
|
||||
import { makeEmojiMap } from 'pl-fe/utils/normalizers';
|
||||
|
||||
import type { AdminAnnouncement as BaseAdminAnnouncement, Announcement as BaseAnnouncement } from 'pl-api';
|
||||
|
||||
const normalizeAnnouncement = <T extends BaseAnnouncement = BaseAnnouncement>(announcement: T) => {
|
||||
const emojiMap = makeEmojiMap(announcement.emojis);
|
||||
|
||||
const contentHtml = emojify(announcement.content, emojiMap);
|
||||
|
||||
return {
|
||||
...announcement,
|
||||
contentHtml,
|
||||
};
|
||||
};
|
||||
|
||||
type Announcement = ReturnType<typeof normalizeAnnouncement>;
|
||||
type AdminAnnouncement = ReturnType<typeof normalizeAnnouncement<BaseAdminAnnouncement>>;
|
||||
|
||||
export { normalizeAnnouncement, type AdminAnnouncement, type Announcement };
|
|
@ -1,44 +0,0 @@
|
|||
import escapeTextContentForBrowser from 'escape-html';
|
||||
import DOMPurify from 'isomorphic-dompurify';
|
||||
|
||||
import emojify from 'pl-fe/features/emoji';
|
||||
import { makeEmojiMap } from 'pl-fe/utils/normalizers';
|
||||
|
||||
import type { Status as BaseStatus, StatusEdit as BaseStatusEdit, CustomEmoji } from 'pl-api';
|
||||
|
||||
const sanitizeTitle = (text: string, emojiMap: Record<string, CustomEmoji>) => DOMPurify.sanitize(emojify(escapeTextContentForBrowser(text), emojiMap), { ALLOWED_TAGS: ['img'] });
|
||||
|
||||
const normalizePoll = (poll: Exclude<BaseStatus['poll'], null>) => {
|
||||
const emojiMap = makeEmojiMap(poll.emojis);
|
||||
return {
|
||||
...poll,
|
||||
options: poll.options.map(option => ({
|
||||
...option,
|
||||
title_emojified: sanitizeTitle(option.title, emojiMap),
|
||||
title_map_emojified: option.title_map
|
||||
? Object.fromEntries(Object.entries(option.title_map).map(([key, title]) => [key, sanitizeTitle(title, emojiMap)]))
|
||||
: null,
|
||||
})),
|
||||
};
|
||||
};
|
||||
|
||||
const normalizePollEdit = (poll: Exclude<BaseStatusEdit['poll'], null>, emojis: Array<CustomEmoji>) => {
|
||||
const emojiMap = makeEmojiMap(emojis);
|
||||
return {
|
||||
...poll,
|
||||
options: poll.options.map(option => ({
|
||||
...option,
|
||||
title_emojified: sanitizeTitle(option.title, emojiMap),
|
||||
})),
|
||||
};
|
||||
};
|
||||
|
||||
type Poll = ReturnType<typeof normalizePoll>;
|
||||
type PollEdit = ReturnType<typeof normalizePollEdit>;
|
||||
|
||||
export {
|
||||
normalizePoll,
|
||||
normalizePollEdit,
|
||||
type Poll,
|
||||
type PollEdit,
|
||||
};
|
|
@ -1,23 +0,0 @@
|
|||
/**
|
||||
* Status edit normalizer
|
||||
*/
|
||||
import escapeTextContentForBrowser from 'escape-html';
|
||||
|
||||
import emojify from 'pl-fe/features/emoji';
|
||||
import { makeEmojiMap } from 'pl-fe/utils/normalizers';
|
||||
|
||||
import type { StatusEdit as BaseStatusEdit } from 'pl-api';
|
||||
|
||||
const normalizeStatusEdit = (statusEdit: BaseStatusEdit) => {
|
||||
const emojiMap = makeEmojiMap(statusEdit.emojis);
|
||||
|
||||
return {
|
||||
...statusEdit,
|
||||
contentHtml: emojify(statusEdit.content, emojiMap),
|
||||
spoilerHtml: emojify(escapeTextContentForBrowser(statusEdit.spoiler_text), emojiMap),
|
||||
};
|
||||
};
|
||||
|
||||
type StatusEdit = ReturnType<typeof normalizeStatusEdit>
|
||||
|
||||
export { type StatusEdit, normalizeStatusEdit };
|
|
@ -3,18 +3,13 @@
|
|||
* Converts API statuses into our internal format.
|
||||
* @see {@link https://docs.joinmastodon.org/entities/status/}
|
||||
*/
|
||||
import escapeTextContentForBrowser from 'escape-html';
|
||||
import DOMPurify from 'isomorphic-dompurify';
|
||||
import { type Account as BaseAccount, type Status as BaseStatus, type CustomEmoji, type MediaAttachment, mentionSchema, type Translation } from 'pl-api';
|
||||
import { type Account as BaseAccount, type Status as BaseStatus, type MediaAttachment, mentionSchema, type Translation } from 'pl-api';
|
||||
import * as v from 'valibot';
|
||||
|
||||
import emojify from 'pl-fe/features/emoji';
|
||||
import { unescapeHTML } from 'pl-fe/utils/html';
|
||||
import { makeEmojiMap } from 'pl-fe/utils/normalizers';
|
||||
|
||||
import { normalizeAccount } from './account';
|
||||
import { normalizeGroup } from './group';
|
||||
import { normalizePoll } from './poll';
|
||||
|
||||
const domParser = new DOMParser();
|
||||
|
||||
|
@ -23,10 +18,6 @@ type StatusVisibility = 'public' | 'unlisted' | 'private' | 'direct' | 'group' |
|
|||
|
||||
type CalculatedValues = {
|
||||
search_index: string;
|
||||
contentHtml: string;
|
||||
spoilerHtml: string;
|
||||
contentMapHtml?: Record<string, string>;
|
||||
spoilerMapHtml?: Record<string, string>;
|
||||
expanded?: boolean | null;
|
||||
hidden?: boolean | null;
|
||||
translation?: Translation | null | false;
|
||||
|
@ -63,32 +54,20 @@ const buildSearchContent = (status: Pick<BaseStatus, 'poll' | 'mentions' | 'spoi
|
|||
return unescapeHTML(fields.join('\n\n')) || '';
|
||||
};
|
||||
|
||||
const calculateContent = (text: string, emojiMap: Record<string, CustomEmoji>) => emojify(text, emojiMap);
|
||||
const calculateSpoiler = (text: string, emojiMap: Record<string, CustomEmoji>) => DOMPurify.sanitize(emojify(escapeTextContentForBrowser(text), emojiMap), { USE_PROFILES: { html: true } });
|
||||
|
||||
const calculateStatus = (status: BaseStatus, oldStatus?: OldStatus): CalculatedValues => {
|
||||
if (oldStatus && oldStatus.content === status.content && oldStatus.spoiler_text === status.spoiler_text) {
|
||||
const {
|
||||
search_index, contentHtml, spoilerHtml, contentMapHtml, spoilerMapHtml, hidden, expanded, translation, currentLanguage,
|
||||
search_index, hidden, expanded, translation, currentLanguage,
|
||||
} = oldStatus;
|
||||
|
||||
return {
|
||||
search_index, contentHtml, spoilerHtml, contentMapHtml, spoilerMapHtml, hidden, expanded, translation, currentLanguage,
|
||||
search_index, hidden, expanded, translation, currentLanguage,
|
||||
};
|
||||
} else {
|
||||
const searchContent = buildSearchContent(status);
|
||||
const emojiMap = makeEmojiMap(status.emojis);
|
||||
|
||||
return {
|
||||
search_index: domParser.parseFromString(searchContent, 'text/html').documentElement.textContent || '',
|
||||
contentHtml: calculateContent(status.content, emojiMap),
|
||||
spoilerHtml: calculateSpoiler(status.spoiler_text, emojiMap),
|
||||
contentMapHtml: status.content_map
|
||||
? Object.fromEntries(Object.entries(status.content_map)?.map(([key, value]) => [key, calculateContent(value, emojiMap)]))
|
||||
: undefined,
|
||||
spoilerMapHtml: status.spoiler_text_map
|
||||
? Object.fromEntries(Object.entries(status.spoiler_text_map).map(([key, value]) => [key, calculateSpoiler(value, emojiMap)]))
|
||||
: undefined,
|
||||
};
|
||||
}
|
||||
};
|
||||
|
@ -123,9 +102,6 @@ const normalizeStatus = (status: BaseStatus & {
|
|||
} | null) = null;
|
||||
let media_attachments = status.media_attachments;
|
||||
|
||||
// Normalize poll
|
||||
const poll = status.poll ? normalizePoll(status.poll) : null;
|
||||
|
||||
if (status.event) {
|
||||
const firstAttachment = status.media_attachments[0];
|
||||
let banner: MediaAttachment | null = null;
|
||||
|
@ -167,7 +143,6 @@ const normalizeStatus = (status: BaseStatus & {
|
|||
content: status.content === '<p></p>' ? '' : status.content,
|
||||
filtered: status.filtered?.map(result => result.filter.title),
|
||||
event,
|
||||
poll,
|
||||
group,
|
||||
media_attachments,
|
||||
...calculated,
|
||||
|
|
|
@ -1,18 +0,0 @@
|
|||
import emojify from 'pl-fe/features/emoji';
|
||||
import { makeEmojiMap } from 'pl-fe/utils/normalizers';
|
||||
|
||||
import type { Status, Translation as BaseTranslation } from 'pl-api';
|
||||
|
||||
const normalizeTranslation = (translation: BaseTranslation, status: Pick<Status, 'emojis'>) => {
|
||||
const emojiMap = makeEmojiMap(status.emojis);
|
||||
const content = emojify(translation.content, emojiMap);
|
||||
|
||||
return {
|
||||
...translation,
|
||||
content,
|
||||
};
|
||||
};
|
||||
|
||||
type Translation = ReturnType<typeof normalizeTranslation>;
|
||||
|
||||
export { normalizeTranslation, type Translation };
|
|
@ -1,9 +1,8 @@
|
|||
import { Map as ImmutableMap, Record as ImmutableRecord } from 'immutable';
|
||||
|
||||
import { HISTORY_FETCH_REQUEST, HISTORY_FETCH_SUCCESS, HISTORY_FETCH_FAIL, type HistoryAction } from 'pl-fe/actions/history';
|
||||
import { normalizeStatusEdit } from 'pl-fe/normalizers/status-edit';
|
||||
|
||||
import type { StatusEdit as BaseStatusEdit } from 'pl-api';
|
||||
import type { StatusEdit } from 'pl-api';
|
||||
|
||||
const HistoryRecord = ImmutableRecord({
|
||||
loading: false,
|
||||
|
@ -14,8 +13,8 @@ type State = ImmutableMap<string, ReturnType<typeof HistoryRecord>>;
|
|||
|
||||
const initialState: State = ImmutableMap();
|
||||
|
||||
const minifyStatusEdit = (statusEdit: BaseStatusEdit, i: number) => ({
|
||||
...normalizeStatusEdit(statusEdit), account: statusEdit.account.id, original: i === 0,
|
||||
const minifyStatusEdit = ({ account, ...statusEdit }: StatusEdit, i: number) => ({
|
||||
...statusEdit, account_id: account.id, original: i === 0,
|
||||
});
|
||||
|
||||
const history = (state: State = initialState, action: HistoryAction) => {
|
||||
|
|
|
@ -2,7 +2,6 @@ import { Map as ImmutableMap } from 'immutable';
|
|||
import omit from 'lodash/omit';
|
||||
|
||||
import { normalizeStatus, Status as StatusRecord } from 'pl-fe/normalizers/status';
|
||||
import { normalizeTranslation } from 'pl-fe/normalizers/translation';
|
||||
import { simulateEmojiReact, simulateUnEmojiReact } from 'pl-fe/utils/emoji-reacts';
|
||||
|
||||
import {
|
||||
|
@ -173,11 +172,9 @@ const simulateDislike = (
|
|||
|
||||
/** Import translation from translation service into the store. */
|
||||
const importTranslation = (state: State, statusId: string, translation: Translation) => {
|
||||
const result = normalizeTranslation(translation, state.get(statusId)!);
|
||||
|
||||
return state.update(statusId, undefined as any, (status) => ({
|
||||
...status,
|
||||
translation: result,
|
||||
translation: translation,
|
||||
translating: false,
|
||||
}));
|
||||
};
|
||||
|
|
Loading…
Reference in a new issue