TelegramPWA/src/components/common/embedded/EmbeddedMessage.tsx

288 lines
8.7 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 { ChatTranslatedMessages } from '../../../global/types';
import type { ObserveFn } from '../../../hooks/useIntersectionObserver';
import type { IconName } from '../../../types/icons';
import { CONTENT_NOT_SUPPORTED } from '../../../config';
import {
getMediaContentTypeDescription,
getMessageIsSpoiler,
getMessageMediaHash,
getMessageRoundVideo,
getSenderTitle,
isActionMessage,
isChatChannel,
isChatGroup,
isMessageTranslatable,
isUserId,
} from '../../../global/helpers';
import buildClassName from '../../../util/buildClassName';
import freezeWhenClosed from '../../../util/hoc/freezeWhenClosed';
import { getPictogramDimensions } from '../helpers/mediaDimensions';
import { getPeerColorClass } from '../helpers/peerColor';
import renderText from '../helpers/renderText';
import { renderTextWithEntities } from '../helpers/renderTextWithEntities';
import { useFastClick } from '../../../hooks/useFastClick';
import { useIsIntersecting } from '../../../hooks/useIntersectionObserver';
import useLang from '../../../hooks/useLang';
import useMedia from '../../../hooks/useMedia';
import useThumbnail from '../../../hooks/useThumbnail';
import useMessageTranslation from '../../middle/message/hooks/useMessageTranslation';
import ActionMessage from '../../middle/ActionMessage';
import Icon from '../Icon';
import MediaSpoiler from '../MediaSpoiler';
import MessageSummary from '../MessageSummary';
import EmojiIconBackground from './EmojiIconBackground';
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 wrappedMedia = useMemo(() => {
const replyMedia = replyInfo?.type === 'message' && replyInfo.replyMedia;
if (!replyMedia) return undefined;
return {
content: replyMedia,
};
}, [replyInfo]);
const mediaBlobUrl = useMedia(message && getMessageMediaHash(message, 'pictogram'), !isIntersecting);
const mediaThumbnail = useThumbnail(message || wrappedMedia);
const isRoundVideo = Boolean(message && getMessageRoundVideo(message));
const isSpoiler = Boolean(message && getMessageIsSpoiler(message));
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 = useLang();
const senderTitle = sender ? getSenderTitle(lang, sender)
: (replyForwardInfo?.hiddenUserName || message?.forwardInfo?.hiddenUserName);
const senderChatTitle = senderChat ? getSenderTitle(lang, senderChat) : undefined;
const forwardSenderTitle = forwardSender ? getSenderTitle(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,
noLineBreaks: isInComposer,
emojiSize: EMOJI_SIZE,
});
}
if (!message) {
return customText || renderMediaContentType(wrappedMedia) || NBSP;
}
if (isActionMessage(message)) {
return (
<ActionMessage
message={message}
isEmbedded
observeIntersectionForLoading={observeIntersectionForLoading}
observeIntersectionForPlaying={observeIntersectionForPlaying}
/>
);
}
return (
<MessageSummary
lang={lang}
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 (
<>
{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>
)}
</>
);
}
return (
<div
ref={ref}
className={buildClassName(
'EmbeddedMessage',
className,
getPeerColorClass(sender, noUserColors, true),
isQuote && 'is-quote',
mediaThumbnail && 'with-thumb',
)}
dir={lang.isRtl ? 'rtl' : undefined}
onClick={handleClick}
onMouseDown={handleMouseDown}
>
{mediaThumbnail && renderPictogram(mediaThumbnail, mediaBlobUrl, isRoundVideo, isProtected, isSpoiler)}
{sender?.color?.backgroundEmojiId && (
<EmojiIconBackground
emojiDocumentId={sender.color.backgroundEmojiId}
className="EmbeddedMessage--background-icons"
/>
)}
<div className="message-text">
<p className={buildClassName('embedded-text-wrapper', isQuote && 'multiline')}>
{renderTextContent()}
</p>
<div className="message-title">
{renderSender()}
{forwardSenderTitle && !areSendersSame && (
<>
<Icon name={forwardSender ? 'share-filled' : 'forward'} className="embedded-origin-icon" />
{renderText(forwardSenderTitle)}
</>
)}
</div>
</div>
</div>
);
};
function renderPictogram(
thumbDataUri: string,
blobUrl?: string,
isRoundVideo?: boolean,
isProtected?: boolean,
isSpoiler?: boolean,
) {
const { width, height } = getPictogramDimensions();
const srcUrl = blobUrl || thumbDataUri;
return (
<div className={buildClassName('embedded-thumb', isRoundVideo && 'round')}>
{!isSpoiler && (
<img
src={srcUrl}
width={width}
height={height}
alt=""
className="pictogram"
draggable={false}
/>
)}
<MediaSpoiler thumbDataUri={srcUrl} isVisible={Boolean(isSpoiler)} width={width} height={height} />
{isProtected && <span className="protector" />}
</div>
);
}
export const ClosableEmbeddedMessage = freezeWhenClosed(EmbeddedMessage);
export default EmbeddedMessage;