Spoiler: Simplify opening logic (#3816)
This commit is contained in:
parent
a32251a2b3
commit
2496ff8dcb
@ -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,
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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 <ins data-entity-type={entity.type}>{renderNestedMessagePart()}</ins>;
|
||||
case ApiMessageEntityTypes.Spoiler:
|
||||
return <Spoiler messageId={messageId}>{renderNestedMessagePart()}</Spoiler>;
|
||||
return <Spoiler containerId={containerId}>{renderNestedMessagePart()}</Spoiler>;
|
||||
case ApiMessageEntityTypes.CustomEmoji:
|
||||
return (
|
||||
<CustomEmoji
|
||||
|
||||
@ -12,84 +12,55 @@ import './Spoiler.scss';
|
||||
|
||||
type OwnProps = {
|
||||
children?: React.ReactNode;
|
||||
messageId?: number;
|
||||
containerId?: string;
|
||||
};
|
||||
|
||||
const READING_SYMBOLS_PER_SECOND = 23; // Heuristics
|
||||
const MIN_HIDE_TIMEOUT = 5000; // 5s
|
||||
const MAX_HIDE_TIMEOUT = 60000; // 1m
|
||||
|
||||
const actionsByMessageId: Map<number, {
|
||||
reveal: VoidFunction;
|
||||
conceal: VoidFunction;
|
||||
contentLength: number;
|
||||
}[]> = new Map();
|
||||
const revealByContainerId: Map<string, VoidFunction[]> = new Map();
|
||||
|
||||
const buildClassName = createClassNameBuilder('Spoiler');
|
||||
|
||||
const Spoiler: FC<OwnProps> = ({
|
||||
children,
|
||||
messageId,
|
||||
containerId,
|
||||
}) => {
|
||||
// eslint-disable-next-line no-null/no-null
|
||||
const contentRef = useRef<HTMLDivElement>(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<HTMLDivElement, 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 (
|
||||
<span
|
||||
className={buildClassName(
|
||||
'&',
|
||||
!isRevealed && 'concealed',
|
||||
!isRevealed && Boolean(messageId) && 'animated',
|
||||
!isRevealed && Boolean(containerId) && 'animated',
|
||||
)}
|
||||
onClick={messageId && !isRevealed ? handleClick : undefined}
|
||||
onClick={containerId && !isRevealed ? handleClick : undefined}
|
||||
data-entity-type={ApiMessageEntityTypes.Spoiler}
|
||||
>
|
||||
<span className={buildClassName('content')} ref={contentRef}>
|
||||
|
||||
@ -207,7 +207,7 @@ const MediaViewer: FC<StateProps> = ({
|
||||
const prevMediaId = usePrevious(mediaId);
|
||||
const prevAvatarOwner = usePrevious<ApiChat | ApiUser | undefined>(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;
|
||||
|
||||
|
||||
@ -147,7 +147,7 @@ const MediaViewerContent: FC<OwnProps & StateProps> = (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);
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user