diff --git a/src/components/common/MessageText.tsx b/src/components/common/MessageText.tsx index 5cfc39eeb..5805309ee 100644 --- a/src/components/common/MessageText.tsx +++ b/src/components/common/MessageText.tsx @@ -10,6 +10,7 @@ import trimText from '../../util/trimText'; import { extractMessageText, getMessageText, stripCustomEmoji } from '../../global/helpers'; import { renderTextWithEntities } from './helpers/renderTextWithEntities'; import useSyncEffect from '../../hooks/useSyncEffect'; +import useUniqueId from '../../hooks/useUniqueId'; interface OwnProps { messageOrStory: ApiMessage | ApiStory; @@ -57,6 +58,8 @@ function MessageText({ const adaptedFormattedText = isForAnimation && formattedText ? stripCustomEmoji(formattedText) : formattedText; const { text, entities } = adaptedFormattedText || {}; + const containerId = useUniqueId(); + useSyncEffect(() => { textCacheBusterRef.current += 1; }, [text, entities]); @@ -87,7 +90,7 @@ function MessageText({ highlight, emojiSize, shouldRenderAsHtml, - messageId: messageOrStory.id, + containerId, isSimple, isProtected, observeIntersectionForLoading, diff --git a/src/components/common/helpers/renderMessageText.ts b/src/components/common/helpers/renderMessageText.ts index 964873ee6..6a417d6e8 100644 --- a/src/components/common/helpers/renderMessageText.ts +++ b/src/components/common/helpers/renderMessageText.ts @@ -4,6 +4,7 @@ import type { TextPart } from '../../../types'; import type { LangFn } from '../../../hooks/useLang'; import { + getMessageKey, getMessageSummaryDescription, getMessageSummaryEmoji, getMessageSummaryText, @@ -23,6 +24,7 @@ export function renderMessageText({ isProtected, forcePlayback, shouldRenderAsHtml, + isForMediaViewer, } : { message: ApiMessage; highlight?: string; @@ -32,6 +34,7 @@ export function renderMessageText({ isProtected?: boolean; forcePlayback?: boolean; shouldRenderAsHtml?: boolean; + isForMediaViewer?: boolean; }) { const { text, entities } = message.content.text || {}; @@ -40,13 +43,15 @@ export function renderMessageText({ return contentNotSupportedText ? [trimText(contentNotSupportedText, truncateLength)] : undefined; } + const messageKey = getMessageKey(message); + return renderTextWithEntities({ text: trimText(text, truncateLength), entities, highlight, emojiSize, shouldRenderAsHtml, - messageId: message.id, + containerId: `${isForMediaViewer ? 'mv-' : ''}${messageKey}`, isSimple, isProtected, forcePlayback, diff --git a/src/components/common/helpers/renderTextWithEntities.tsx b/src/components/common/helpers/renderTextWithEntities.tsx index 2fbdf767f..e6cf7f19d 100644 --- a/src/components/common/helpers/renderTextWithEntities.tsx +++ b/src/components/common/helpers/renderTextWithEntities.tsx @@ -33,7 +33,7 @@ export function renderTextWithEntities({ highlight, emojiSize, shouldRenderAsHtml, - messageId, + containerId, isSimple, isProtected, observeIntersectionForLoading, @@ -49,7 +49,7 @@ export function renderTextWithEntities({ highlight?: string; emojiSize?: number; shouldRenderAsHtml?: boolean; - messageId?: number; + containerId?: string; isSimple?: boolean; isProtected?: boolean; observeIntersectionForLoading?: ObserveFn; @@ -138,7 +138,7 @@ export function renderTextWithEntities({ entityContent, nestedEntityContent, highlight, - messageId, + containerId, isSimple, isProtected, observeIntersectionForLoading, @@ -320,7 +320,7 @@ function processEntity({ entityContent, nestedEntityContent, highlight, - messageId, + containerId, isSimple, isProtected, observeIntersectionForLoading, @@ -336,7 +336,7 @@ function processEntity({ entityContent: TextPart; nestedEntityContent: TextPart[]; highlight?: string; - messageId?: number; + containerId?: string; isSimple?: boolean; isProtected?: boolean; observeIntersectionForLoading?: ObserveFn; @@ -492,7 +492,7 @@ function processEntity({ case ApiMessageEntityTypes.Underline: return {renderNestedMessagePart()}; case ApiMessageEntityTypes.Spoiler: - return {renderNestedMessagePart()}; + return {renderNestedMessagePart()}; case ApiMessageEntityTypes.CustomEmoji: return ( = new Map(); +const revealByContainerId: Map = new Map(); const buildClassName = createClassNameBuilder('Spoiler'); const Spoiler: FC = ({ children, - messageId, + containerId, }) => { // eslint-disable-next-line no-null/no-null const contentRef = useRef(null); - const [isRevealed, reveal, conceal] = useFlag(); - - const getContentLength = useLastCallback(() => { - if (!contentRef.current) { - return 0; - } - - const textLength = contentRef.current.textContent?.length || 0; - const emojiCount = contentRef.current.querySelectorAll('.emoji').length; - // Optimization: ignore alt, assume that viewing emoji takes same time as viewing 4 characters - return textLength + emojiCount * 4; - }); + const [isRevealed, revealSpoiler] = useFlag(); const handleClick = useLastCallback((e: React.MouseEvent) => { + if (!containerId) return; + e.preventDefault(); e.stopPropagation(); - actionsByMessageId.get(messageId!)?.forEach((actions) => actions.reveal()); - - const totalContentLength = actionsByMessageId.get(messageId!) - ?.reduce((acc, actions) => acc + actions.contentLength, 0) || 0; - const readingMs = Math.round(totalContentLength / READING_SYMBOLS_PER_SECOND) * 1000; - const timeoutMs = Math.max(MIN_HIDE_TIMEOUT, Math.min(readingMs, MAX_HIDE_TIMEOUT)); - - setTimeout(() => { - actionsByMessageId.get(messageId!)?.forEach((actions) => actions.conceal()); - conceal(); - }, timeoutMs); + revealByContainerId.get(containerId)?.forEach((reveal) => reveal()); }); useEffect(() => { - if (!messageId) { + if (!containerId) { return undefined; } - const contentLength = getContentLength(); - - if (actionsByMessageId.has(messageId)) { - actionsByMessageId.get(messageId)!.push({ reveal, conceal, contentLength }); + if (revealByContainerId.has(containerId)) { + revealByContainerId.get(containerId)!.push(revealSpoiler); } else { - actionsByMessageId.set(messageId, [{ reveal, conceal, contentLength }]); + revealByContainerId.set(containerId, [revealSpoiler]); } return () => { - actionsByMessageId.delete(messageId); + revealByContainerId.delete(containerId); }; - }, [conceal, getContentLength, handleClick, isRevealed, messageId, reveal]); + }, [containerId]); return ( diff --git a/src/components/mediaViewer/MediaViewer.tsx b/src/components/mediaViewer/MediaViewer.tsx index 9b5d4d61d..828003852 100644 --- a/src/components/mediaViewer/MediaViewer.tsx +++ b/src/components/mediaViewer/MediaViewer.tsx @@ -207,7 +207,7 @@ const MediaViewer: FC = ({ const prevMediaId = usePrevious(mediaId); const prevAvatarOwner = usePrevious(avatarOwner); const prevBestImageData = usePrevious(bestImageData); - const textParts = message ? renderMessageText({ message, forcePlayback: true }) : undefined; + const textParts = message ? renderMessageText({ message, forcePlayback: true, isForMediaViewer: true }) : undefined; const hasFooter = Boolean(textParts); const shouldAnimateOpening = prevIsHidden && prevMediaId !== mediaId; diff --git a/src/components/mediaViewer/MediaViewerContent.tsx b/src/components/mediaViewer/MediaViewerContent.tsx index 3805b8fb0..f6b1c74ba 100644 --- a/src/components/mediaViewer/MediaViewerContent.tsx +++ b/src/components/mediaViewer/MediaViewerContent.tsx @@ -147,7 +147,7 @@ const MediaViewerContent: FC = (props) => { if (!message) return undefined; const textParts = message.content.action?.type === 'suggestProfilePhoto' ? lang('Conversation.SuggestedPhotoTitle') - : renderMessageText({ message, forcePlayback: true }); + : renderMessageText({ message, forcePlayback: true, isForMediaViewer: true }); const hasFooter = Boolean(textParts); const posterSize = message && calculateMediaViewerDimensions(dimensions!, hasFooter, isVideo);