TelegramPWA/src/components/common/embedded/EmbeddedMessage.tsx
2025-03-01 18:02:01 +01:00

306 lines
9.3 KiB
TypeScript

import type { FC } from '../../../lib/teact/teact';
import React, { useMemo, useRef } from '../../../lib/teact/teact';
import type {
ApiChat,
ApiMessage, ApiPeer, ApiReplyInfo, MediaContainer,
} from '../../../api/types';
import type { ObserveFn } from '../../../hooks/useIntersectionObserver';
import type { ChatTranslatedMessages } from '../../../types';
import type { IconName } from '../../../types/icons';
import { CONTENT_NOT_SUPPORTED } from '../../../config';
import {
getMessageIsSpoiler,
getMessageMediaHash,
getMessageRoundVideo,
getPeerTitle,
isChatChannel,
isChatGroup,
isMessageTranslatable,
isUserId,
} from '../../../global/helpers';
import { getMediaContentTypeDescription } from '../../../global/helpers/messageSummary';
import buildClassName from '../../../util/buildClassName';
import freezeWhenClosed from '../../../util/hoc/freezeWhenClosed';
import { getPictogramDimensions } from '../helpers/mediaDimensions';
import renderText from '../helpers/renderText';
import { renderTextWithEntities } from '../helpers/renderTextWithEntities';
import { useFastClick } from '../../../hooks/useFastClick';
import { useIsIntersecting } from '../../../hooks/useIntersectionObserver';
import useMedia from '../../../hooks/useMedia';
import useOldLang from '../../../hooks/useOldLang';
import useThumbnail from '../../../hooks/useThumbnail';
import useMessageTranslation from '../../middle/message/hooks/useMessageTranslation';
import RippleEffect from '../../ui/RippleEffect';
import Icon from '../icons/Icon';
import MediaSpoiler from '../MediaSpoiler';
import MessageSummary from '../MessageSummary';
import PeerColorWrapper from '../PeerColorWrapper';
import './EmbeddedMessage.scss';
type OwnProps = {
className?: string;
replyInfo?: ApiReplyInfo;
message?: ApiMessage;
sender?: ApiPeer;
senderChat?: ApiChat;
forwardSender?: ApiPeer;
title?: string;
customText?: string;
noUserColors?: boolean;
isProtected?: boolean;
isInComposer?: boolean;
chatTranslations?: ChatTranslatedMessages;
requestedChatTranslationLanguage?: string;
isOpen?: boolean;
observeIntersectionForLoading?: ObserveFn;
observeIntersectionForPlaying?: ObserveFn;
onClick: ((e: React.MouseEvent) => void);
};
const NBSP = '\u00A0';
const EMOJI_SIZE = 17;
const EmbeddedMessage: FC<OwnProps> = ({
className,
message,
replyInfo,
sender,
senderChat,
forwardSender,
title,
customText,
isProtected,
isInComposer,
noUserColors,
chatTranslations,
requestedChatTranslationLanguage,
observeIntersectionForLoading,
observeIntersectionForPlaying,
onClick,
}) => {
// eslint-disable-next-line no-null/no-null
const ref = useRef<HTMLDivElement>(null);
const isIntersecting = useIsIntersecting(ref, observeIntersectionForLoading);
const containedMedia: MediaContainer | undefined = useMemo(() => {
const media = (replyInfo?.type === 'message' && replyInfo.replyMedia) || message?.content;
if (!media) {
return undefined;
}
return {
content: media,
};
}, [message, replyInfo]);
const gif = containedMedia?.content?.video?.isGif ? containedMedia.content.video : undefined;
const isVideoThumbnail = Boolean(gif && !gif.previewPhotoSizes?.length);
const mediaHash = containedMedia && getMessageMediaHash(containedMedia, isVideoThumbnail ? 'full' : 'pictogram');
const mediaBlobUrl = useMedia(mediaHash, !isIntersecting);
const mediaThumbnail = useThumbnail(containedMedia);
const isRoundVideo = Boolean(containedMedia && getMessageRoundVideo(containedMedia));
const isSpoiler = Boolean(containedMedia && getMessageIsSpoiler(containedMedia));
const isQuote = Boolean(replyInfo?.type === 'message' && replyInfo.isQuote);
const replyForwardInfo = replyInfo?.type === 'message' ? replyInfo.replyFrom : undefined;
const shouldTranslate = message && isMessageTranslatable(message);
const { translatedText } = useMessageTranslation(
chatTranslations, message?.chatId, shouldTranslate ? message?.id : undefined, requestedChatTranslationLanguage,
);
const lang = useOldLang();
const senderTitle = sender ? getPeerTitle(lang, sender)
: (replyForwardInfo?.hiddenUserName || message?.forwardInfo?.hiddenUserName);
const senderChatTitle = senderChat ? getPeerTitle(lang, senderChat) : undefined;
const forwardSenderTitle = forwardSender ? getPeerTitle(lang, forwardSender)
: message?.forwardInfo?.hiddenUserName;
const areSendersSame = sender && sender.id === forwardSender?.id;
const { handleClick, handleMouseDown } = useFastClick(onClick);
function renderTextContent() {
if (replyInfo?.type === 'message' && replyInfo.quoteText) {
return renderTextWithEntities({
text: replyInfo.quoteText.text,
entities: replyInfo.quoteText.entities,
asPreview: true,
emojiSize: EMOJI_SIZE,
});
}
if (!message) {
return customText || renderMediaContentType(containedMedia) || NBSP;
}
return (
<MessageSummary
message={message}
noEmoji={Boolean(mediaThumbnail)}
translatedText={translatedText}
observeIntersectionForLoading={observeIntersectionForLoading}
observeIntersectionForPlaying={observeIntersectionForPlaying}
emojiSize={EMOJI_SIZE}
/>
);
}
function renderMediaContentType(media?: MediaContainer) {
if (!media || media.content.text) return NBSP;
const description = getMediaContentTypeDescription(lang, media.content, {});
if (!description || description === CONTENT_NOT_SUPPORTED) return NBSP;
return (
<span>
{renderText(description)}
</span>
);
}
function checkShouldRenderSenderTitle() {
if (!senderChat) return true;
if (isUserId(senderChat?.id)) return true;
if (senderChat.id === sender?.id) return false;
return true;
}
function renderSender() {
if (title) {
return renderText(title);
}
if (!senderTitle) {
return NBSP;
}
let icon: IconName | undefined;
if (senderChat) {
if (isChatChannel(senderChat)) {
icon = 'channel-filled';
}
if (isChatGroup(senderChat)) {
icon = 'group-filled';
}
}
const isReplyToQuote = isInComposer && Boolean(replyInfo && 'quoteText' in replyInfo && replyInfo?.quoteText);
return (
<span className="embedded-sender-wrapper">
{checkShouldRenderSenderTitle() && (
<span className="embedded-sender">
{renderText(isReplyToQuote ? lang('ReplyToQuote', senderTitle) : senderTitle)}
</span>
)}
{icon && <Icon name={icon} className="embedded-chat-icon" />}
{icon && senderChatTitle && (
<span className="embedded-sender-chat">
{renderText(senderChatTitle)}
</span>
)}
</span>
);
}
function renderForwardSender() {
return forwardSenderTitle && !areSendersSame && (
<span className="embedded-forward-sender-wrapper">
<Icon name={forwardSender ? 'share-filled' : 'forward'} className="embedded-origin-icon" />
<span className="forward-sender-title">
{renderText(forwardSenderTitle)}
</span>
</span>
);
}
return (
<PeerColorWrapper
peer={sender}
emojiIconClassName=' className="EmbeddedMessage--background-icons"'
ref={ref}
shouldReset
noUserColors={noUserColors}
className={buildClassName(
'EmbeddedMessage',
className,
isQuote && 'is-quote',
mediaThumbnail && 'with-thumb',
)}
dir={lang.isRtl ? 'rtl' : undefined}
onClick={handleClick}
onMouseDown={handleMouseDown}
>
<div className="hover-effect" />
<RippleEffect />
{mediaThumbnail && renderPictogram(
mediaThumbnail, mediaBlobUrl, isVideoThumbnail, isRoundVideo, isProtected, isSpoiler,
)}
<div className="message-text">
<p className={buildClassName('embedded-text-wrapper', isQuote && 'multiline')}>
{renderTextContent()}
</p>
<div className="message-title">
{renderSender()}
{renderForwardSender()}
</div>
</div>
</PeerColorWrapper>
);
};
function renderPictogram(
thumbDataUri: string,
blobUrl?: string,
isFullVideo?: boolean,
isRoundVideo?: boolean,
isProtected?: boolean,
isSpoiler?: boolean,
) {
const { width, height } = getPictogramDimensions();
const srcUrl = blobUrl || thumbDataUri;
const shouldRenderVideo = isFullVideo && blobUrl;
return (
<div className={buildClassName('embedded-thumb', isRoundVideo && 'round')}>
{!isSpoiler && !shouldRenderVideo && (
<img
src={srcUrl}
width={width}
height={height}
alt=""
className="pictogram"
draggable={false}
/>
)}
{!isSpoiler && shouldRenderVideo && (
<video
src={blobUrl}
width={width}
height={height}
playsInline
disablePictureInPicture
className="pictogram"
/>
)}
<MediaSpoiler
thumbDataUri={shouldRenderVideo ? thumbDataUri : srcUrl}
isVisible={Boolean(isSpoiler)}
width={width}
height={height}
/>
{isProtected && <span className="protector" />}
</div>
);
}
export const ClosableEmbeddedMessage = freezeWhenClosed(EmbeddedMessage);
export default EmbeddedMessage;