From f335249104ec02083d0c4b5bb4a73d4232518af7 Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Sun, 17 Nov 2024 22:53:17 -0600 Subject: [PATCH 1/3] StatusContent: replace custom emojis on render --- src/components/status-content.tsx | 30 ++++++++++++++++++++++++------ 1 file changed, 24 insertions(+), 6 deletions(-) diff --git a/src/components/status-content.tsx b/src/components/status-content.tsx index 3ee565f0e..265c53323 100644 --- a/src/components/status-content.tsx +++ b/src/components/status-content.tsx @@ -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 = ({ const [collapsed, setCollapsed] = useState(false); const node = useRef(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 = ({ }); 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,22 @@ const StatusContent: React.FC = ({ return null; } + if (domNode instanceof DOMText) { + const parts = domNode.data.split(' ').map((part) => { + const match = part.match(/^:(\w+):$/); + if (!match) return part; + + const [, shortcode] = match; + const customEmoji = customEmojis.find((e) => e.shortcode === shortcode); + + if (!customEmoji) return part; + + return {part}; + }); + + return <>{parts}; + } + if (domNode instanceof Element && domNode.name === 'a') { const classes = domNode.attribs.class?.split(' '); From 36e27eb592769e62a9939cb00715a5689cf5a472 Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Sun, 17 Nov 2024 23:34:34 -0600 Subject: [PATCH 2/3] StatusContent: fix custom emoji parsing --- src/components/status-content.tsx | 24 ++++++++++++++++-------- 1 file changed, 16 insertions(+), 8 deletions(-) diff --git a/src/components/status-content.tsx b/src/components/status-content.tsx index 265c53323..4924d6509 100644 --- a/src/components/status-content.tsx +++ b/src/components/status-content.tsx @@ -92,17 +92,25 @@ const StatusContent: React.FC = ({ } if (domNode instanceof DOMText) { - const parts = domNode.data.split(' ').map((part) => { - const match = part.match(/^:(\w+):$/); - if (!match) return part; + const parts: Array = []; - const [, shortcode] = match; - const customEmoji = customEmojis.find((e) => e.shortcode === shortcode); + const textNodes = domNode.data.split(/:\w+:/); + const shortcodes = [...domNode.data.matchAll(/:(\w+):/g)]; - if (!customEmoji) return part; + for (let i = 0; i < textNodes.length; i++) { + parts.push(textNodes[i]); - return {part}; - }); + if (shortcodes[i]) { + const [text, shortcode] = shortcodes[i]; + const customEmoji = customEmojis.find((e) => e.shortcode === shortcode); + + if (customEmoji) { + parts.push({shortcode}); + } else { + parts.push(text); + } + } + } return <>{parts}; } From 9e72bfb3e1ee6de99d87c11a21d3b1becec9a15a Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Sun, 17 Nov 2024 23:38:38 -0600 Subject: [PATCH 3/3] Remove status.contentHtml --- src/components/translate-button.tsx | 2 +- src/features/compose/components/reply-indicator.tsx | 2 +- src/features/event/event-information.tsx | 2 +- src/features/ui/components/modals/compare-history-modal.tsx | 2 +- src/normalizers/status.ts | 1 - src/reducers/statuses.ts | 3 +-- src/schemas/status.ts | 5 +++-- 7 files changed, 8 insertions(+), 9 deletions(-) diff --git a/src/components/translate-button.tsx b/src/components/translate-button.tsx index b386d8967..f047c30c4 100644 --- a/src/components/translate-button.tsx +++ b/src/components/translate-button.tsx @@ -31,7 +31,7 @@ const TranslateButton: React.FC = ({ 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)); diff --git a/src/features/compose/components/reply-indicator.tsx b/src/features/compose/components/reply-indicator.tsx index 2b7bb5a15..dacdcf309 100644 --- a/src/features/compose/components/reply-indicator.tsx +++ b/src/features/compose/components/reply-indicator.tsx @@ -49,7 +49,7 @@ const ReplyIndicator: React.FC = ({ className, status, hideActi diff --git a/src/features/event/event-information.tsx b/src/features/event/event-information.tsx index 95796b934..0b01c27dd 100644 --- a/src/features/event/event-information.tsx +++ b/src/features/event/event-information.tsx @@ -195,7 +195,7 @@ const EventInformation: React.FC = ({ params }) => { return ( - {!!status.contentHtml.trim() && ( + {!!status.content.trim() && ( diff --git a/src/features/ui/components/modals/compare-history-modal.tsx b/src/features/ui/components/modals/compare-history-modal.tsx index 16bdf18d5..eb4ae960f 100644 --- a/src/features/ui/components/modals/compare-history-modal.tsx +++ b/src/features/ui/components/modals/compare-history-modal.tsx @@ -43,7 +43,7 @@ const CompareHistoryModal: React.FC = ({ onClose, statusId body = (
{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; diff --git a/src/normalizers/status.ts b/src/normalizers/status.ts index 376fd1384..ddca3e888 100644 --- a/src/normalizers/status.ts +++ b/src/normalizers/status.ts @@ -88,7 +88,6 @@ export const StatusRecord = ImmutableRecord({ event: null as ReturnType | null, // Internal fields - contentHtml: '', expectsCard: false, hidden: false, search_index: '', diff --git a/src/reducers/statuses.ts b/src/reducers/statuses.ts index 67c915177..64e6058cd 100644 --- a/src/reducers/statuses.ts +++ b/src/reducers/statuses.ts @@ -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, }); diff --git a/src/schemas/status.ts b/src/schemas/status.ts index 6c04f28ee..1172b35ee 100644 --- a/src/schemas/status.ts +++ b/src/schemas/status.ts @@ -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 = ({ 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: [],