Merge branch 'custom-render' into 'main'
StatusContent: replace custom emojis on render See merge request soapbox-pub/soapbox!3260
This commit is contained in:
commit
19aeb7ebc6
|
@ -1,10 +1,11 @@
|
|||
import chevronRightIcon from '@tabler/icons/outline/chevron-right.svg';
|
||||
import clsx from 'clsx';
|
||||
import graphemesplit from 'graphemesplit';
|
||||
import parse, { Element, type HTMLReactParserOptions, domToReact, type DOMNode } from 'html-react-parser';
|
||||
import parse, { Element, type HTMLReactParserOptions, domToReact, type DOMNode, Text as DOMText } from 'html-react-parser';
|
||||
import { useState, useRef, useLayoutEffect, useMemo, memo } from 'react';
|
||||
import { FormattedMessage } from 'react-intl';
|
||||
|
||||
import { useCustomEmojis } from 'soapbox/api/hooks/useCustomEmojis.ts';
|
||||
import Icon from 'soapbox/components/icon.tsx';
|
||||
|
||||
import { getTextDirection } from '../utils/rtl.ts';
|
||||
|
@ -51,11 +52,12 @@ const StatusContent: React.FC<IStatusContent> = ({
|
|||
const [collapsed, setCollapsed] = useState(false);
|
||||
|
||||
const node = useRef<HTMLDivElement>(null);
|
||||
const { customEmojis } = useCustomEmojis();
|
||||
|
||||
const isOnlyEmoji = useMemo(() => {
|
||||
const textContent = new DOMParser().parseFromString(status.contentHtml, 'text/html').body.firstChild?.textContent;
|
||||
return Boolean(textContent && /^\p{Extended_Pictographic}+$/u.test(textContent) && (graphemesplit(textContent).length <= BIG_EMOJI_LIMIT));
|
||||
}, [status.contentHtml]);
|
||||
const textContent = new DOMParser().parseFromString(status.content, 'text/html').body.firstChild?.textContent ?? '';
|
||||
return Boolean(/^\p{Extended_Pictographic}+$/u.test(textContent) && (graphemesplit(textContent).length <= BIG_EMOJI_LIMIT));
|
||||
}, [status.content]);
|
||||
|
||||
const maybeSetCollapsed = (): void => {
|
||||
if (!node.current) return;
|
||||
|
@ -72,8 +74,8 @@ const StatusContent: React.FC<IStatusContent> = ({
|
|||
});
|
||||
|
||||
const parsedHtml = useMemo((): string => {
|
||||
return translatable && status.translation ? status.translation.get('content')! : status.contentHtml;
|
||||
}, [status.contentHtml, status.translation]);
|
||||
return translatable && status.translation ? status.translation.get('content')! : status.content;
|
||||
}, [status.content, status.translation]);
|
||||
|
||||
if (status.content.length === 0) {
|
||||
return null;
|
||||
|
@ -89,6 +91,30 @@ const StatusContent: React.FC<IStatusContent> = ({
|
|||
return null;
|
||||
}
|
||||
|
||||
if (domNode instanceof DOMText) {
|
||||
const parts: Array<string | JSX.Element> = [];
|
||||
|
||||
const textNodes = domNode.data.split(/:\w+:/);
|
||||
const shortcodes = [...domNode.data.matchAll(/:(\w+):/g)];
|
||||
|
||||
for (let i = 0; i < textNodes.length; i++) {
|
||||
parts.push(textNodes[i]);
|
||||
|
||||
if (shortcodes[i]) {
|
||||
const [text, shortcode] = shortcodes[i];
|
||||
const customEmoji = customEmojis.find((e) => e.shortcode === shortcode);
|
||||
|
||||
if (customEmoji) {
|
||||
parts.push(<img key={i} src={customEmoji.url} alt={shortcode} className='inline-block h-[1em]' />);
|
||||
} else {
|
||||
parts.push(text);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return <>{parts}</>;
|
||||
}
|
||||
|
||||
if (domNode instanceof Element && domNode.name === 'a') {
|
||||
const classes = domNode.attribs.class?.split(' ');
|
||||
|
||||
|
|
|
@ -31,7 +31,7 @@ const TranslateButton: React.FC<ITranslateButton> = ({ status }) => {
|
|||
target_languages: targetLanguages,
|
||||
} = 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;
|
||||
const renderTranslate = (me || allowUnauthenticated) && (allowRemote || status.account.local) && ['public', 'unlisted'].includes(status.visibility) && status.content.length > 0 && status.language !== null && intl.locale !== status.language;
|
||||
|
||||
const supportsLanguages = (!sourceLanguages || sourceLanguages.includes(status.language!)) && (!targetLanguages || targetLanguages.includes(intl.locale));
|
||||
|
||||
|
|
|
@ -49,7 +49,7 @@ const ReplyIndicator: React.FC<IReplyIndicator> = ({ className, status, hideActi
|
|||
<Markup
|
||||
className='break-words'
|
||||
size='sm'
|
||||
dangerouslySetInnerHTML={{ __html: status.contentHtml }}
|
||||
dangerouslySetInnerHTML={{ __html: status.content }}
|
||||
direction={getTextDirection(status.search_index)}
|
||||
/>
|
||||
|
||||
|
|
|
@ -195,7 +195,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' />
|
||||
|
|
|
@ -43,7 +43,7 @@ const CompareHistoryModal: React.FC<ICompareHistoryModal> = ({ onClose, statusId
|
|||
body = (
|
||||
<div className='divide-y divide-solid divide-gray-200 dark:divide-gray-800'>
|
||||
{versions?.map((version) => {
|
||||
const content = { __html: version.contentHtml };
|
||||
const content = { __html: version.content };
|
||||
const spoilerContent = { __html: version.spoilerHtml };
|
||||
|
||||
const poll = typeof version.poll !== 'string' && version.poll;
|
||||
|
|
|
@ -88,7 +88,6 @@ export const StatusRecord = ImmutableRecord({
|
|||
event: null as ReturnType<typeof EventRecord> | null,
|
||||
|
||||
// Internal fields
|
||||
contentHtml: '',
|
||||
expectsCard: false,
|
||||
hidden: false,
|
||||
search_index: '',
|
||||
|
|
|
@ -109,7 +109,6 @@ export const calculateStatus = (
|
|||
if (oldStatus && oldStatus.content === status.content && oldStatus.spoiler_text === status.spoiler_text) {
|
||||
return status.merge({
|
||||
search_index: oldStatus.search_index,
|
||||
contentHtml: oldStatus.contentHtml,
|
||||
spoilerHtml: oldStatus.spoilerHtml,
|
||||
hidden: oldStatus.hidden,
|
||||
});
|
||||
|
@ -120,7 +119,7 @@ export const calculateStatus = (
|
|||
|
||||
return status.merge({
|
||||
search_index: domParser.parseFromString(searchContent, 'text/html').documentElement.textContent || '',
|
||||
contentHtml: DOMPurify.sanitize(stripCompatibilityFeatures(emojify(status.content, emojiMap)), { USE_PROFILES: { html: true } }),
|
||||
content: DOMPurify.sanitize(stripCompatibilityFeatures(status.content), { USE_PROFILES: { html: true } }),
|
||||
spoilerHtml: DOMPurify.sanitize(emojify(escapeTextContentForBrowser(spoilerText), emojiMap), { USE_PROFILES: { html: true } }),
|
||||
hidden: expandSpoilers ? false : spoilerText.length > 0 || status.sensitive,
|
||||
});
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
import escapeTextContentForBrowser from 'escape-html';
|
||||
import DOMPurify from 'isomorphic-dompurify';
|
||||
import { z } from 'zod';
|
||||
|
||||
import emojify from 'soapbox/features/emoji/index.ts';
|
||||
|
@ -106,13 +107,13 @@ type Translation = {
|
|||
const transformStatus = <T extends TransformableStatus>({ pleroma, ...status }: T) => {
|
||||
const emojiMap = makeCustomEmojiMap(status.emojis);
|
||||
|
||||
const contentHtml = stripCompatibilityFeatures(emojify(status.content, emojiMap));
|
||||
const content = DOMPurify.sanitize(stripCompatibilityFeatures(status.content), { USE_PROFILES: { html: true } });
|
||||
const spoilerHtml = emojify(escapeTextContentForBrowser(status.spoiler_text), emojiMap);
|
||||
|
||||
return {
|
||||
...status,
|
||||
approval_status: 'approval' as const,
|
||||
contentHtml,
|
||||
content,
|
||||
expectsCard: false,
|
||||
event: pleroma?.event,
|
||||
filtered: [],
|
||||
|
|
Loading…
Reference in New Issue