mirror of
https://github.com/danog/telegram-tt.git
synced 2025-01-21 21:01:29 +01:00
Introduce Spoilers (#1670)
This commit is contained in:
parent
58453f46b5
commit
a55fbdfcc4
@ -31,7 +31,6 @@ import {
|
||||
} from '../../types';
|
||||
|
||||
import {
|
||||
CONTENT_NOT_SUPPORTED,
|
||||
DELETED_COMMENTS_CHANNEL_ID,
|
||||
LOCAL_MESSAGE_ID_BASE,
|
||||
SERVICE_NOTIFICATIONS_USER_ID,
|
||||
@ -264,12 +263,6 @@ export function buildMessageTextContent(
|
||||
message: string,
|
||||
entities?: GramJs.TypeMessageEntity[],
|
||||
): ApiFormattedText {
|
||||
if (entities?.some((e) => e instanceof GramJs.MessageEntitySpoiler)) {
|
||||
return {
|
||||
text: CONTENT_NOT_SUPPORTED,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
text: message,
|
||||
...(entities && { entities: entities.map(buildApiMessageEntity) }),
|
||||
|
@ -213,6 +213,7 @@ export enum ApiMessageEntityTypes {
|
||||
TextUrl = 'MessageEntityTextUrl',
|
||||
Url = 'MessageEntityUrl',
|
||||
Underline = 'MessageEntityUnderline',
|
||||
Spoiler = 'MessageEntitySpoiler',
|
||||
Unknown = 'MessageEntityUnknown',
|
||||
}
|
||||
|
||||
|
BIN
src/assets/spoiler-dots-black.png
Normal file
BIN
src/assets/spoiler-dots-black.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 3.1 KiB |
BIN
src/assets/spoiler-dots-white.png
Normal file
BIN
src/assets/spoiler-dots-white.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 3.3 KiB |
@ -5,7 +5,6 @@ import { ApiUser, ApiMessage, ApiChat } from '../../api/types';
|
||||
import {
|
||||
getMessageMediaHash,
|
||||
isActionMessage,
|
||||
getMessageSummaryText,
|
||||
getSenderTitle,
|
||||
getMessageRoundVideo,
|
||||
} from '../../modules/helpers';
|
||||
@ -16,6 +15,7 @@ import { ObserveFn, useIsIntersecting } from '../../hooks/useIntersectionObserve
|
||||
import useMedia from '../../hooks/useMedia';
|
||||
import useWebpThumbnail from '../../hooks/useWebpThumbnail';
|
||||
import useLang from '../../hooks/useLang';
|
||||
import { renderMessageSummary } from './helpers/renderMessageText';
|
||||
|
||||
import ActionMessage from '../middle/ActionMessage';
|
||||
|
||||
@ -71,7 +71,7 @@ const EmbeddedMessage: FC<OwnProps> = ({
|
||||
) : isActionMessage(message) ? (
|
||||
<ActionMessage message={message} isEmbedded />
|
||||
) : (
|
||||
renderText(getMessageSummaryText(lang, message, Boolean(mediaThumbnail)))
|
||||
renderMessageSummary(lang, message, Boolean(mediaThumbnail))
|
||||
)}
|
||||
</p>
|
||||
<div className="message-title" dir="auto">{renderText(senderTitle || title || NBSP)}</div>
|
||||
|
@ -2,12 +2,16 @@ import React, { FC, memo, useCallback } from '../../lib/teact/teact';
|
||||
|
||||
import { ApiMessage, ApiWebPage } from '../../api/types';
|
||||
|
||||
import { getFirstLinkInMessage, getMessageSummaryText, getMessageWebPage } from '../../modules/helpers';
|
||||
import {
|
||||
getFirstLinkInMessage, getMessageText,
|
||||
getMessageWebPage,
|
||||
} from '../../modules/helpers';
|
||||
import buildClassName from '../../util/buildClassName';
|
||||
import trimText from '../../util/trimText';
|
||||
import renderText from './helpers/renderText';
|
||||
import { formatPastTimeShort } from '../../util/dateFormat';
|
||||
import useLang from '../../hooks/useLang';
|
||||
import { renderMessageSummary, TextPart } from './helpers/renderMessageText';
|
||||
|
||||
import Media from './Media';
|
||||
import Link from '../ui/Link';
|
||||
@ -24,24 +28,27 @@ type OwnProps = {
|
||||
onMessageClick: (messageId: number, chatId: string) => void;
|
||||
};
|
||||
|
||||
type ApiWebPageWithFormatted = ApiWebPage & { formattedDescription?: TextPart[] };
|
||||
|
||||
const WebLink: FC<OwnProps> = ({
|
||||
message, senderTitle, isProtected, onMessageClick,
|
||||
}) => {
|
||||
const lang = useLang();
|
||||
|
||||
let linkData: ApiWebPage | undefined = getMessageWebPage(message);
|
||||
let linkData: ApiWebPageWithFormatted | undefined = getMessageWebPage(message);
|
||||
|
||||
if (!linkData) {
|
||||
const link = getFirstLinkInMessage(message);
|
||||
if (link) {
|
||||
const { url, domain } = link;
|
||||
const messageText = getMessageSummaryText(lang, message);
|
||||
|
||||
linkData = {
|
||||
siteName: domain.replace(/^www./, ''),
|
||||
url: url.includes('://') ? url : url.includes('@') ? `mailto:${url}` : `http://${url}`,
|
||||
description: messageText !== url ? messageText : undefined,
|
||||
} as ApiWebPage;
|
||||
formattedDescription: getMessageText(message) !== url
|
||||
? renderMessageSummary(lang, message, undefined, undefined, MAX_TEXT_LENGTH, true)
|
||||
: undefined,
|
||||
} as ApiWebPageWithFormatted;
|
||||
}
|
||||
}
|
||||
|
||||
@ -59,11 +66,12 @@ const WebLink: FC<OwnProps> = ({
|
||||
displayUrl,
|
||||
title,
|
||||
description,
|
||||
formattedDescription,
|
||||
photo,
|
||||
video,
|
||||
} = linkData;
|
||||
|
||||
const truncatedDescription = !senderTitle && trimText(description, MAX_TEXT_LENGTH);
|
||||
const truncatedDescription = !senderTitle && description && trimText(description, MAX_TEXT_LENGTH);
|
||||
|
||||
const className = buildClassName(
|
||||
'WebLink scroll-item',
|
||||
@ -83,9 +91,9 @@ const WebLink: FC<OwnProps> = ({
|
||||
<Link isRtl={lang.isRtl} className="site-title" onClick={handleMessageClick}>
|
||||
{renderText(title || siteName || displayUrl)}
|
||||
</Link>
|
||||
{truncatedDescription && (
|
||||
{(truncatedDescription || formattedDescription) && (
|
||||
<Link isRtl={lang.isRtl} className="site-description" onClick={handleMessageClick}>
|
||||
{renderText(truncatedDescription)}
|
||||
{formattedDescription || (truncatedDescription && renderText(truncatedDescription))}
|
||||
</Link>
|
||||
)}
|
||||
<SafeLink
|
||||
|
@ -6,14 +6,13 @@ import {
|
||||
import { LangFn } from '../../../hooks/useLang';
|
||||
import {
|
||||
getChatTitle,
|
||||
getMessageContent,
|
||||
getMessageSummaryText,
|
||||
getMessageContent, getMessageSummaryText,
|
||||
getUserFullName,
|
||||
isUserId,
|
||||
} from '../../../modules/helpers';
|
||||
import trimText from '../../../util/trimText';
|
||||
import { formatCurrency } from '../../../util/formatCurrency';
|
||||
import { TextPart } from './renderMessageText';
|
||||
import { renderMessageSummary, TextPart } from './renderMessageText';
|
||||
import renderText from './renderText';
|
||||
|
||||
import UserLink from '../UserLink';
|
||||
@ -132,44 +131,46 @@ function renderProductContent(message: ApiMessage) {
|
||||
}
|
||||
|
||||
function renderMessageContent(lang: LangFn, message: ApiMessage, options: ActionMessageTextOptions = {}) {
|
||||
const text = getMessageSummaryText(lang, message);
|
||||
const { maxTextLength, isEmbedded, asPlain } = options;
|
||||
|
||||
const text = asPlain
|
||||
? [trimText(getMessageSummaryText(lang, message), maxTextLength)]
|
||||
: renderMessageSummary(lang, message, undefined, undefined, maxTextLength, true);
|
||||
const {
|
||||
photo, video, document, sticker,
|
||||
} = getMessageContent(message);
|
||||
|
||||
const { maxTextLength, isEmbedded, asPlain } = options;
|
||||
|
||||
const showQuotes = isEmbedded && text && !photo && !video && !document && !sticker;
|
||||
let messageText = trimText(text as string, maxTextLength)!;
|
||||
let messageText = text;
|
||||
|
||||
if (isEmbedded) {
|
||||
if (photo) {
|
||||
messageText = 'a photo';
|
||||
messageText = ['a photo'];
|
||||
} else if (video) {
|
||||
messageText = video.isGif ? 'a GIF' : 'a video';
|
||||
messageText = [video.isGif ? 'a GIF' : 'a video'];
|
||||
} else if (document) {
|
||||
messageText = 'a document';
|
||||
messageText = ['a document'];
|
||||
} else if (sticker) {
|
||||
messageText = text;
|
||||
}
|
||||
}
|
||||
|
||||
if (asPlain) {
|
||||
return showQuotes ? `«${messageText}»` : messageText;
|
||||
if (asPlain && messageText) {
|
||||
return (showQuotes ? ['«', ...messageText, '»'] : messageText).join('');
|
||||
}
|
||||
|
||||
if (showQuotes) {
|
||||
return (
|
||||
<span>
|
||||
«
|
||||
<MessageLink className="action-link" message={message}>{renderText(messageText)}</MessageLink>
|
||||
<MessageLink className="action-link" message={message}>{messageText}</MessageLink>
|
||||
»
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<MessageLink className="action-link" message={message}>{renderText(messageText)}</MessageLink>
|
||||
<MessageLink className="action-link" message={message}>{messageText}</MessageLink>
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -4,15 +4,64 @@ import { getDispatch } from '../../../lib/teact/teactn';
|
||||
|
||||
import { ApiMessageEntity, ApiMessageEntityTypes, ApiMessage } from '../../../api/types';
|
||||
|
||||
import { getMessageText } from '../../../modules/helpers';
|
||||
import renderText from './renderText';
|
||||
import {
|
||||
getMessageSummaryText,
|
||||
getMessageSummaryDescription,
|
||||
getMessageSummaryEmoji,
|
||||
getMessageText,
|
||||
TRUNCATED_SUMMARY_LENGTH,
|
||||
} from '../../../modules/helpers';
|
||||
import renderText, { TextFilter } from './renderText';
|
||||
|
||||
import MentionLink from '../../middle/message/MentionLink';
|
||||
import SafeLink from '../SafeLink';
|
||||
import Spoiler from '../spoiler/Spoiler';
|
||||
import { LangFn } from '../../../hooks/useLang';
|
||||
|
||||
export type TextPart = string | Element;
|
||||
|
||||
export function renderMessageText(message: ApiMessage, highlight?: string, shouldRenderHqEmoji?: boolean) {
|
||||
export function renderMessageSummary(
|
||||
lang: LangFn,
|
||||
message: ApiMessage,
|
||||
noEmoji = false,
|
||||
highlight?: string,
|
||||
truncateLength = TRUNCATED_SUMMARY_LENGTH,
|
||||
shouldAddEllipsis?: boolean,
|
||||
): TextPart[] {
|
||||
const hasSpoilers = message.content.text?.entities?.some((l) => l.type === ApiMessageEntityTypes.Spoiler);
|
||||
if (!hasSpoilers) {
|
||||
let text = getMessageSummaryText(lang, message, noEmoji, truncateLength);
|
||||
if (shouldAddEllipsis) {
|
||||
text += '...';
|
||||
}
|
||||
|
||||
if (highlight) {
|
||||
return renderText(text, ['emoji', 'highlight'], {
|
||||
highlight,
|
||||
});
|
||||
} else {
|
||||
return renderText(text);
|
||||
}
|
||||
}
|
||||
|
||||
const text = renderMessageText(message, highlight, undefined, true, truncateLength);
|
||||
const emoji = !noEmoji && getMessageSummaryEmoji(message);
|
||||
const emojiWithSpace = emoji ? `${emoji} ` : '';
|
||||
const description = getMessageSummaryDescription(lang, message, text);
|
||||
return [
|
||||
emojiWithSpace,
|
||||
...(Array.isArray(description) ? description : [description]),
|
||||
shouldAddEllipsis && '...',
|
||||
].filter(Boolean);
|
||||
}
|
||||
|
||||
export function renderMessageText(
|
||||
message: ApiMessage,
|
||||
highlight?: string,
|
||||
shouldRenderHqEmoji?: boolean,
|
||||
isSimple?: boolean,
|
||||
truncateLength?: number,
|
||||
) {
|
||||
const formattedText = message.content.text;
|
||||
|
||||
if (!formattedText || !formattedText.text) {
|
||||
@ -21,7 +70,15 @@ export function renderMessageText(message: ApiMessage, highlight?: string, shoul
|
||||
}
|
||||
const { text, entities } = formattedText;
|
||||
|
||||
return renderTextWithEntities(text, entities, highlight, shouldRenderHqEmoji);
|
||||
return renderTextWithEntities(
|
||||
truncateLength ? text.substr(0, truncateLength) : text,
|
||||
entities,
|
||||
highlight,
|
||||
shouldRenderHqEmoji,
|
||||
undefined,
|
||||
message.id,
|
||||
isSimple,
|
||||
);
|
||||
}
|
||||
|
||||
interface IOrganizedEntity {
|
||||
@ -102,9 +159,11 @@ export function renderTextWithEntities(
|
||||
highlight?: string,
|
||||
shouldRenderHqEmoji?: boolean,
|
||||
shouldRenderAsHtml?: boolean,
|
||||
messageId?: number,
|
||||
isSimple?: boolean,
|
||||
) {
|
||||
if (!entities || !entities.length) {
|
||||
return renderMessagePart(text, highlight, shouldRenderHqEmoji, shouldRenderAsHtml);
|
||||
return renderMessagePart(text, highlight, shouldRenderHqEmoji, shouldRenderAsHtml, isSimple);
|
||||
}
|
||||
|
||||
const result: TextPart[] = [];
|
||||
@ -133,7 +192,7 @@ export function renderTextWithEntities(
|
||||
}
|
||||
if (textBefore) {
|
||||
renderResult.push(...renderMessagePart(
|
||||
textBefore, highlight, shouldRenderHqEmoji, shouldRenderAsHtml,
|
||||
textBefore, highlight, shouldRenderHqEmoji, shouldRenderAsHtml, isSimple,
|
||||
) as TextPart[]);
|
||||
}
|
||||
}
|
||||
@ -176,7 +235,7 @@ export function renderTextWithEntities(
|
||||
// Render the entity itself
|
||||
const newEntity = shouldRenderAsHtml
|
||||
? processEntityAsHtml(entity, entityContent, nestedEntityContent)
|
||||
: processEntity(entity, entityContent, nestedEntityContent);
|
||||
: processEntity(entity, entityContent, nestedEntityContent, highlight, messageId, isSimple);
|
||||
|
||||
if (Array.isArray(newEntity)) {
|
||||
renderResult.push(...newEntity);
|
||||
@ -193,7 +252,7 @@ export function renderTextWithEntities(
|
||||
}
|
||||
if (textAfter) {
|
||||
renderResult.push(...renderMessagePart(
|
||||
textAfter, highlight, shouldRenderHqEmoji, shouldRenderAsHtml,
|
||||
textAfter, highlight, shouldRenderHqEmoji, shouldRenderAsHtml, isSimple,
|
||||
) as TextPart[]);
|
||||
}
|
||||
}
|
||||
@ -226,19 +285,36 @@ function processEntity(
|
||||
entity: ApiMessageEntity,
|
||||
entityContent: TextPart,
|
||||
nestedEntityContent: TextPart[],
|
||||
highlight?: string,
|
||||
messageId?: number,
|
||||
isSimple?: boolean,
|
||||
) {
|
||||
const entityText = typeof entityContent === 'string' && entityContent;
|
||||
const renderedContent = nestedEntityContent.length ? nestedEntityContent : entityContent;
|
||||
|
||||
function renderNestedMessagePart() {
|
||||
return renderMessagePart(
|
||||
renderedContent, highlight, undefined, undefined, isSimple,
|
||||
);
|
||||
}
|
||||
|
||||
if (!entityText) {
|
||||
return renderMessagePart(renderedContent);
|
||||
return renderNestedMessagePart();
|
||||
}
|
||||
|
||||
if (isSimple) {
|
||||
const text = renderNestedMessagePart();
|
||||
if (entity.type === ApiMessageEntityTypes.Spoiler) {
|
||||
return <Spoiler isInactive>{text}</Spoiler>;
|
||||
}
|
||||
return text;
|
||||
}
|
||||
|
||||
switch (entity.type) {
|
||||
case ApiMessageEntityTypes.Bold:
|
||||
return <strong>{renderMessagePart(renderedContent)}</strong>;
|
||||
return <strong>{renderNestedMessagePart()}</strong>;
|
||||
case ApiMessageEntityTypes.Blockquote:
|
||||
return <blockquote>{renderMessagePart(renderedContent)}</blockquote>;
|
||||
return <blockquote>{renderNestedMessagePart()}</blockquote>;
|
||||
case ApiMessageEntityTypes.BotCommand:
|
||||
return (
|
||||
<a
|
||||
@ -246,7 +322,7 @@ function processEntity(
|
||||
className="text-entity-link"
|
||||
dir="auto"
|
||||
>
|
||||
{renderMessagePart(renderedContent)}
|
||||
{renderNestedMessagePart()}
|
||||
</a>
|
||||
);
|
||||
case ApiMessageEntityTypes.Hashtag:
|
||||
@ -256,7 +332,7 @@ function processEntity(
|
||||
className="text-entity-link"
|
||||
dir="auto"
|
||||
>
|
||||
{renderMessagePart(renderedContent)}
|
||||
{renderNestedMessagePart()}
|
||||
</a>
|
||||
);
|
||||
case ApiMessageEntityTypes.Cashtag:
|
||||
@ -266,11 +342,11 @@ function processEntity(
|
||||
className="text-entity-link"
|
||||
dir="auto"
|
||||
>
|
||||
{renderMessagePart(renderedContent)}
|
||||
{renderNestedMessagePart()}
|
||||
</a>
|
||||
);
|
||||
case ApiMessageEntityTypes.Code:
|
||||
return <code className="text-entity-code">{renderMessagePart(renderedContent)}</code>;
|
||||
return <code className="text-entity-code">{renderNestedMessagePart()}</code>;
|
||||
case ApiMessageEntityTypes.Email:
|
||||
return (
|
||||
<a
|
||||
@ -280,21 +356,21 @@ function processEntity(
|
||||
className="text-entity-link"
|
||||
dir="auto"
|
||||
>
|
||||
{renderMessagePart(renderedContent)}
|
||||
{renderNestedMessagePart()}
|
||||
</a>
|
||||
);
|
||||
case ApiMessageEntityTypes.Italic:
|
||||
return <em>{renderMessagePart(renderedContent)}</em>;
|
||||
return <em>{renderNestedMessagePart()}</em>;
|
||||
case ApiMessageEntityTypes.MentionName:
|
||||
return (
|
||||
<MentionLink userId={entity.userId}>
|
||||
{renderMessagePart(renderedContent)}
|
||||
{renderNestedMessagePart()}
|
||||
</MentionLink>
|
||||
);
|
||||
case ApiMessageEntityTypes.Mention:
|
||||
return (
|
||||
<MentionLink username={entityText}>
|
||||
{renderMessagePart(renderedContent)}
|
||||
{renderNestedMessagePart()}
|
||||
</MentionLink>
|
||||
);
|
||||
case ApiMessageEntityTypes.Phone:
|
||||
@ -304,13 +380,13 @@ function processEntity(
|
||||
className="text-entity-link"
|
||||
dir="auto"
|
||||
>
|
||||
{renderMessagePart(renderedContent)}
|
||||
{renderNestedMessagePart()}
|
||||
</a>
|
||||
);
|
||||
case ApiMessageEntityTypes.Pre:
|
||||
return <pre className="text-entity-pre">{renderMessagePart(renderedContent)}</pre>;
|
||||
return <pre className="text-entity-pre">{renderNestedMessagePart()}</pre>;
|
||||
case ApiMessageEntityTypes.Strike:
|
||||
return <del>{renderMessagePart(renderedContent)}</del>;
|
||||
return <del>{renderNestedMessagePart()}</del>;
|
||||
case ApiMessageEntityTypes.TextUrl:
|
||||
case ApiMessageEntityTypes.Url:
|
||||
return (
|
||||
@ -318,13 +394,15 @@ function processEntity(
|
||||
url={getLinkUrl(entityText, entity)}
|
||||
text={entityText}
|
||||
>
|
||||
{renderMessagePart(renderedContent)}
|
||||
{renderNestedMessagePart()}
|
||||
</SafeLink>
|
||||
);
|
||||
case ApiMessageEntityTypes.Underline:
|
||||
return <ins>{renderMessagePart(renderedContent)}</ins>;
|
||||
return <ins>{renderNestedMessagePart()}</ins>;
|
||||
case ApiMessageEntityTypes.Spoiler:
|
||||
return <Spoiler messageId={messageId}>{renderNestedMessagePart()}</Spoiler>;
|
||||
default:
|
||||
return renderMessagePart(renderedContent);
|
||||
return renderNestedMessagePart();
|
||||
}
|
||||
}
|
||||
|
||||
@ -333,12 +411,13 @@ function renderMessagePart(
|
||||
highlight?: string,
|
||||
shouldRenderHqEmoji?: boolean,
|
||||
shouldRenderAsHtml?: boolean,
|
||||
isSimple?: boolean,
|
||||
) {
|
||||
if (Array.isArray(content)) {
|
||||
const result: TextPart[] = [];
|
||||
|
||||
content.forEach((c) => {
|
||||
result.push(...renderMessagePart(c, highlight, shouldRenderHqEmoji, shouldRenderAsHtml));
|
||||
result.push(...renderMessagePart(c, highlight, shouldRenderHqEmoji, shouldRenderAsHtml, isSimple));
|
||||
});
|
||||
|
||||
return result;
|
||||
@ -350,10 +429,15 @@ function renderMessagePart(
|
||||
|
||||
const emojiFilter = shouldRenderHqEmoji ? 'hq_emoji' : 'emoji';
|
||||
|
||||
const filters: TextFilter[] = [emojiFilter];
|
||||
if (!isSimple) {
|
||||
filters.push('br');
|
||||
}
|
||||
|
||||
if (highlight) {
|
||||
return renderText(content, [emojiFilter, 'br', 'highlight'], { highlight });
|
||||
return renderText(content, filters.concat('highlight'), { highlight });
|
||||
} else {
|
||||
return renderText(content, [emojiFilter, 'br']);
|
||||
return renderText(content, filters);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -11,16 +11,17 @@ import MentionLink from '../../middle/message/MentionLink';
|
||||
import SafeLink from '../SafeLink';
|
||||
|
||||
type TextPart = string | Element;
|
||||
export type TextFilter = (
|
||||
'escape_html' | 'hq_emoji' | 'emoji' | 'emoji_html' | 'br' | 'br_html' | 'highlight' | 'links' |
|
||||
'simple_markdown' | 'simple_markdown_html'
|
||||
);
|
||||
|
||||
const RE_LETTER_OR_DIGIT = /^[\d\wа-яё]$/i;
|
||||
const SIMPLE_MARKDOWN_REGEX = /(\*\*|__).+?\1/g;
|
||||
|
||||
export default function renderText(
|
||||
part: TextPart,
|
||||
filters: Array<(
|
||||
'escape_html' | 'hq_emoji' | 'emoji' | 'emoji_html' | 'br' | 'br_html' | 'highlight' | 'links' |
|
||||
'simple_markdown' | 'simple_markdown_html'
|
||||
)> = ['emoji'],
|
||||
filters: Array<TextFilter> = ['emoji'],
|
||||
params?: { highlight: string | undefined },
|
||||
): TextPart[] {
|
||||
if (typeof part !== 'string') {
|
||||
|
31
src/components/common/spoiler/Spoiler.scss
Normal file
31
src/components/common/spoiler/Spoiler.scss
Normal file
@ -0,0 +1,31 @@
|
||||
.Spoiler {
|
||||
transition: color 250ms ease;
|
||||
|
||||
&:not(.is-revealed) {
|
||||
cursor: pointer;
|
||||
color: transparent;
|
||||
background-image: url('../../../assets/spoiler-dots-black.png');
|
||||
background-size: auto 100%;
|
||||
border-radius: 0.5rem;
|
||||
|
||||
&.animate {
|
||||
animation: pulse-opacity-light 1.75s linear infinite;
|
||||
}
|
||||
}
|
||||
|
||||
html.theme-dark &:not(.is-revealed), html.theme-light .ListItem.selected &:not(.is-revealed) {
|
||||
background-image: url('../../../assets/spoiler-dots-white.png');
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes pulse-opacity-light {
|
||||
25% {
|
||||
opacity: 1;
|
||||
}
|
||||
50% {
|
||||
opacity: 0.25;
|
||||
}
|
||||
75% {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
66
src/components/common/spoiler/Spoiler.tsx
Normal file
66
src/components/common/spoiler/Spoiler.tsx
Normal file
@ -0,0 +1,66 @@
|
||||
import React, {
|
||||
FC, memo, useCallback, useEffect,
|
||||
} from '../../../lib/teact/teact';
|
||||
|
||||
import buildClassName from '../../../util/buildClassName';
|
||||
import useFlag from '../../../hooks/useFlag';
|
||||
|
||||
import './Spoiler.scss';
|
||||
|
||||
type OwnProps = {
|
||||
children?: React.ReactNode;
|
||||
messageId?: number;
|
||||
isInactive?: boolean;
|
||||
};
|
||||
|
||||
const spoilersByMessageId: Map<number, VoidFunction[]> = new Map();
|
||||
|
||||
const Spoiler: FC<OwnProps> = ({
|
||||
children,
|
||||
messageId,
|
||||
isInactive,
|
||||
}) => {
|
||||
const [isRevealed, reveal] = useFlag();
|
||||
|
||||
const handleClick = useCallback(() => {
|
||||
if (!messageId) return;
|
||||
|
||||
spoilersByMessageId.get(messageId)?.forEach((_reveal) => _reveal());
|
||||
}, [messageId]);
|
||||
|
||||
useEffect(() => {
|
||||
if (isRevealed && messageId) {
|
||||
spoilersByMessageId.delete(messageId);
|
||||
return undefined;
|
||||
}
|
||||
|
||||
if (!messageId) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
if (spoilersByMessageId.has(messageId)) {
|
||||
spoilersByMessageId.get(messageId)!.push(reveal);
|
||||
} else {
|
||||
spoilersByMessageId.set(messageId, [reveal]);
|
||||
}
|
||||
|
||||
return () => {
|
||||
spoilersByMessageId.delete(messageId);
|
||||
};
|
||||
}, [handleClick, isRevealed, messageId, reveal]);
|
||||
|
||||
return (
|
||||
<span
|
||||
className={buildClassName(
|
||||
'Spoiler',
|
||||
isRevealed && 'is-revealed',
|
||||
!isInactive && 'animate',
|
||||
)}
|
||||
onClick={!isInactive && !isRevealed ? handleClick : undefined}
|
||||
>
|
||||
{children}
|
||||
</span>
|
||||
);
|
||||
};
|
||||
|
||||
export default memo(Spoiler);
|
@ -20,7 +20,6 @@ import {
|
||||
getMessageSenderName,
|
||||
isChatChannel,
|
||||
getMessageMediaHash,
|
||||
getMessageSummaryText,
|
||||
getMessageMediaThumbDataUri,
|
||||
getMessageVideo,
|
||||
getMessageSticker,
|
||||
@ -40,6 +39,7 @@ import useChatContextActions from '../../../hooks/useChatContextActions';
|
||||
import useFlag from '../../../hooks/useFlag';
|
||||
import useMedia from '../../../hooks/useMedia';
|
||||
import { ChatAnimationTypes } from './hooks';
|
||||
import { renderMessageSummary } from '../../common/helpers/renderMessageText';
|
||||
|
||||
import Avatar from '../../common/Avatar';
|
||||
import VerifiedIcon from '../../common/VerifiedIcon';
|
||||
@ -241,15 +241,14 @@ const Chat: FC<OwnProps & StateProps> = ({
|
||||
|
||||
return (
|
||||
<p className="last-message" dir={lang.isRtl ? 'auto' : 'ltr'}>
|
||||
{renderText(renderActionMessageText(
|
||||
{renderActionMessageText(
|
||||
lang,
|
||||
lastMessage,
|
||||
actionOrigin,
|
||||
actionTargetUsers,
|
||||
actionTargetMessage,
|
||||
actionTargetChatId,
|
||||
{ asPlain: true },
|
||||
) as string)}
|
||||
)}
|
||||
</p>
|
||||
);
|
||||
}
|
||||
@ -264,7 +263,7 @@ const Chat: FC<OwnProps & StateProps> = ({
|
||||
<span className="colon">:</span>
|
||||
</>
|
||||
)}
|
||||
{renderMessageSummary(lang, lastMessage!, mediaBlobUrl || mediaThumbnail, isRoundVideo)}
|
||||
{renderSummary(lang, lastMessage!, mediaBlobUrl || mediaThumbnail, isRoundVideo)}
|
||||
</p>
|
||||
);
|
||||
}
|
||||
@ -333,16 +332,16 @@ const Chat: FC<OwnProps & StateProps> = ({
|
||||
);
|
||||
};
|
||||
|
||||
function renderMessageSummary(lang: LangFn, message: ApiMessage, blobUrl?: string, isRoundVideo?: boolean) {
|
||||
function renderSummary(lang: LangFn, message: ApiMessage, blobUrl?: string, isRoundVideo?: boolean) {
|
||||
if (!blobUrl) {
|
||||
return renderText(getMessageSummaryText(lang, message));
|
||||
return renderMessageSummary(lang, message);
|
||||
}
|
||||
|
||||
return (
|
||||
<span className="media-preview">
|
||||
<img src={blobUrl} alt="" className={isRoundVideo ? 'round' : undefined} />
|
||||
{getMessageVideo(message) && <i className="icon-play" />}
|
||||
{renderText(getMessageSummaryText(lang, message, true))}
|
||||
{renderMessageSummary(lang, message, true)}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
@ -12,7 +12,6 @@ import {
|
||||
getChatTitle,
|
||||
getPrivateChatUserId,
|
||||
getMessageMediaHash,
|
||||
getMessageSummaryText,
|
||||
getMessageMediaThumbDataUri,
|
||||
getMessageVideo,
|
||||
getMessageRoundVideo,
|
||||
@ -23,6 +22,7 @@ import useMedia from '../../../hooks/useMedia';
|
||||
import { formatPastTimeShort } from '../../../util/dateFormat';
|
||||
import useLang, { LangFn } from '../../../hooks/useLang';
|
||||
import useSelectWithEnter from '../../../hooks/useSelectWithEnter';
|
||||
import { renderMessageSummary } from '../../common/helpers/renderMessageText';
|
||||
|
||||
import Avatar from '../../common/Avatar';
|
||||
import VerifiedIcon from '../../common/VerifiedIcon';
|
||||
@ -98,7 +98,7 @@ const ChatMessage: FC<OwnProps & StateProps> = ({
|
||||
</div>
|
||||
<div className="subtitle">
|
||||
<div className="message" dir="auto">
|
||||
{renderMessageSummary(lang, message, mediaBlobUrl || mediaThumbnail, searchQuery, isRoundVideo)}
|
||||
{renderSummary(lang, message, mediaBlobUrl || mediaThumbnail, searchQuery, isRoundVideo)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -106,18 +106,18 @@ const ChatMessage: FC<OwnProps & StateProps> = ({
|
||||
);
|
||||
};
|
||||
|
||||
function renderMessageSummary(
|
||||
function renderSummary(
|
||||
lang: LangFn, message: ApiMessage, blobUrl?: string, searchQuery?: string, isRoundVideo?: boolean,
|
||||
) {
|
||||
if (!blobUrl) {
|
||||
return renderText(getMessageSummaryText(lang, message));
|
||||
return renderMessageSummary(lang, message, undefined, searchQuery);
|
||||
}
|
||||
|
||||
return (
|
||||
<span className="media-preview">
|
||||
<img src={blobUrl} alt="" className={isRoundVideo ? 'round' : undefined} />
|
||||
{getMessageVideo(message) && <i className="icon-play" />}
|
||||
{renderText(getMessageSummaryText(lang, message, true), ['emoji', 'highlight'], { highlight: searchQuery })}
|
||||
{renderMessageSummary(lang, message, true, searchQuery)}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
@ -6,10 +6,10 @@ import { getDispatch, withGlobal } from '../../../lib/teact/teactn';
|
||||
import { ApiChat, ApiMessage } from '../../../api/types';
|
||||
import { LoadMoreDirection } from '../../../types';
|
||||
|
||||
import { getMessageSummaryText } from '../../../modules/helpers';
|
||||
import { MEMO_EMPTY_ARRAY } from '../../../util/memo';
|
||||
import { throttle } from '../../../util/schedulers';
|
||||
import useLang from '../../../hooks/useLang';
|
||||
import { renderMessageSummary } from '../../common/helpers/renderMessageText';
|
||||
|
||||
import InfiniteScroll from '../../ui/InfiniteScroll';
|
||||
import ChatMessage from './ChatMessage';
|
||||
@ -76,7 +76,7 @@ const ChatMessageResults: FC<OwnProps & StateProps> = ({
|
||||
}, [foundIds, globalMessagesByChatId]);
|
||||
|
||||
function renderFoundMessage(message: ApiMessage) {
|
||||
const text = getMessageSummaryText(lang, message);
|
||||
const text = renderMessageSummary(lang, message);
|
||||
const chat = chatsById[message.chatId];
|
||||
|
||||
if (!text || !chat) {
|
||||
|
@ -8,10 +8,14 @@ import { LoadMoreDirection } from '../../../types';
|
||||
|
||||
import { IS_SINGLE_COLUMN_LAYOUT } from '../../../util/environment';
|
||||
import { unique } from '../../../util/iteratees';
|
||||
import { getMessageSummaryText, sortChatIds, filterUsersByName } from '../../../modules/helpers';
|
||||
import {
|
||||
sortChatIds,
|
||||
filterUsersByName,
|
||||
} from '../../../modules/helpers';
|
||||
import { MEMO_EMPTY_ARRAY } from '../../../util/memo';
|
||||
import { throttle } from '../../../util/schedulers';
|
||||
import useLang from '../../../hooks/useLang';
|
||||
import { renderMessageSummary } from '../../common/helpers/renderMessageText';
|
||||
|
||||
import InfiniteScroll from '../../ui/InfiniteScroll';
|
||||
import LeftSearchResultChat from './LeftSearchResultChat';
|
||||
@ -154,7 +158,7 @@ const ChatResults: FC<OwnProps & StateProps> = ({
|
||||
}, [shouldShowMoreGlobal]);
|
||||
|
||||
function renderFoundMessage(message: ApiMessage) {
|
||||
const text = getMessageSummaryText(lang, message);
|
||||
const text = renderMessageSummary(lang, message);
|
||||
const chat = chatsById[message.chatId];
|
||||
|
||||
if (!text || !chat) {
|
||||
|
@ -14,7 +14,6 @@ import {
|
||||
} from '../../modules/selectors';
|
||||
import { isChatChannel } from '../../modules/helpers';
|
||||
import buildClassName from '../../util/buildClassName';
|
||||
import renderText from '../common/helpers/renderText';
|
||||
import { renderActionMessageText } from '../common/helpers/renderActionMessageText';
|
||||
import useEnsureMessage from '../../hooks/useEnsureMessage';
|
||||
import useContextMenuHandlers from '../../hooks/useContextMenuHandlers';
|
||||
@ -96,7 +95,7 @@ const ActionMessage: FC<OwnProps & StateProps> = ({
|
||||
targetUsers,
|
||||
targetMessage,
|
||||
targetChatId,
|
||||
isEmbedded ? { isEmbedded: true, asPlain: true } : undefined,
|
||||
isEmbedded ? { isEmbedded: true } : undefined,
|
||||
);
|
||||
const {
|
||||
isContextMenuOpen, contextMenuPosition,
|
||||
@ -111,7 +110,7 @@ const ActionMessage: FC<OwnProps & StateProps> = ({
|
||||
};
|
||||
|
||||
if (isEmbedded) {
|
||||
return <span className="embedded-action-message">{renderText(content as string)}</span>;
|
||||
return <span className="embedded-action-message">{content}</span>;
|
||||
}
|
||||
|
||||
const className = buildClassName(
|
||||
|
@ -3,8 +3,7 @@ import React, { FC, memo, useCallback } from '../../lib/teact/teact';
|
||||
import { ApiMessage } from '../../api/types';
|
||||
|
||||
import { getPictogramDimensions } from '../common/helpers/mediaDimensions';
|
||||
import { getMessageMediaHash, getMessageSummaryText } from '../../modules/helpers';
|
||||
import renderText from '../common/helpers/renderText';
|
||||
import { getMessageMediaHash } from '../../modules/helpers';
|
||||
import useMedia from '../../hooks/useMedia';
|
||||
import useWebpThumbnail from '../../hooks/useWebpThumbnail';
|
||||
|
||||
@ -14,6 +13,7 @@ import RippleEffect from '../ui/RippleEffect';
|
||||
import buildClassName from '../../util/buildClassName';
|
||||
import useFlag from '../../hooks/useFlag';
|
||||
import useLang from '../../hooks/useLang';
|
||||
import { renderMessageSummary } from '../common/helpers/renderMessageText';
|
||||
|
||||
import PinnedMessageNavigation from './PinnedMessageNavigation';
|
||||
|
||||
@ -35,7 +35,7 @@ const HeaderPinnedMessage: FC<OwnProps> = ({
|
||||
const mediaThumbnail = useWebpThumbnail(message);
|
||||
const mediaBlobUrl = useMedia(getMessageMediaHash(message, 'pictogram'));
|
||||
|
||||
const text = getMessageSummaryText(lang, message, Boolean(mediaThumbnail));
|
||||
const text = renderMessageSummary(lang, message, Boolean(mediaThumbnail));
|
||||
const [isUnpinDialogOpen, openUnpinDialog, closeUnpinDialog] = useFlag();
|
||||
|
||||
const handleUnpinMessage = useCallback(() => {
|
||||
@ -89,7 +89,7 @@ const HeaderPinnedMessage: FC<OwnProps> = ({
|
||||
<div className="title" dir="auto">
|
||||
{customTitle || `${lang('PinnedMessage')} ${index > 0 ? `#${count - index}` : ''}`}
|
||||
</div>
|
||||
<p dir="auto">{renderText(text)}</p>
|
||||
<p dir="auto">{text}</p>
|
||||
</div>
|
||||
|
||||
<RippleEffect />
|
||||
|
@ -12,7 +12,6 @@ import {
|
||||
selectCurrentTextSearch,
|
||||
} from '../../modules/selectors';
|
||||
import {
|
||||
getMessageSummaryText,
|
||||
getChatTitle,
|
||||
getUserFullName,
|
||||
isChatChannel,
|
||||
@ -23,6 +22,7 @@ import { orderBy } from '../../util/iteratees';
|
||||
import { MEMO_EMPTY_ARRAY } from '../../util/memo';
|
||||
import useKeyboardListNavigation from '../../hooks/useKeyboardListNavigation';
|
||||
import useHistoryBack from '../../hooks/useHistoryBack';
|
||||
import { renderMessageSummary } from '../common/helpers/renderMessageText';
|
||||
|
||||
import InfiniteScroll from '../ui/InfiniteScroll';
|
||||
import ListItem from '../ui/ListItem';
|
||||
@ -109,7 +109,7 @@ const RightSearch: FC<OwnProps & StateProps> = ({
|
||||
message, senderUser, senderChat, onClick,
|
||||
}: Result) => {
|
||||
const title = senderChat ? getChatTitle(lang, senderChat) : getUserFullName(senderUser);
|
||||
const text = getMessageSummaryText(lang, message);
|
||||
const text = renderMessageSummary(lang, message, undefined, query);
|
||||
|
||||
return (
|
||||
<ListItem
|
||||
@ -123,7 +123,7 @@ const RightSearch: FC<OwnProps & StateProps> = ({
|
||||
<LastMessageMeta message={message} />
|
||||
</div>
|
||||
<div className="subtitle" dir="auto">
|
||||
{renderText(text, ['emoji', 'highlight'], { highlight: query })}
|
||||
{text}
|
||||
</div>
|
||||
</div>
|
||||
</ListItem>
|
||||
|
@ -1,6 +1,7 @@
|
||||
export * from './users';
|
||||
export * from './chats';
|
||||
export * from './messages';
|
||||
export * from './messageSummary';
|
||||
export * from './messageMedia';
|
||||
export * from './localSearch';
|
||||
export * from './payments';
|
||||
|
141
src/modules/helpers/messageSummary.ts
Normal file
141
src/modules/helpers/messageSummary.ts
Normal file
@ -0,0 +1,141 @@
|
||||
import { LangFn } from '../../hooks/useLang';
|
||||
import { ApiMessage, ApiMessageEntityTypes } from '../../api/types';
|
||||
import type { TextPart } from '../../components/common/helpers/renderMessageText';
|
||||
import { CONTENT_NOT_SUPPORTED } from '../../config';
|
||||
import { getMessageText } from './messages';
|
||||
|
||||
const SPOILER_CHARS = ['⠺', '⠵', '⠞', '⠟'];
|
||||
export const TRUNCATED_SUMMARY_LENGTH = 80;
|
||||
|
||||
export function getMessageSummaryText(
|
||||
lang: LangFn,
|
||||
message: ApiMessage,
|
||||
noEmoji = false,
|
||||
truncateLength = TRUNCATED_SUMMARY_LENGTH,
|
||||
) {
|
||||
const emoji = !noEmoji && getMessageSummaryEmoji(message);
|
||||
const emojiWithSpace = emoji ? `${emoji} ` : '';
|
||||
|
||||
let text = getMessageText(message);
|
||||
if (text) {
|
||||
const { entities } = message.content.text || {};
|
||||
if (entities?.length) {
|
||||
text = entities.reduce((accText, { type, offset, length }) => {
|
||||
if (type !== ApiMessageEntityTypes.Spoiler) {
|
||||
return accText;
|
||||
}
|
||||
|
||||
const spoiler = generateBrailleSpoiler(length);
|
||||
|
||||
return `${accText.substr(0, offset)}${spoiler}${accText.substr(offset + length, accText.length)}`;
|
||||
}, text);
|
||||
}
|
||||
|
||||
text = text.substr(0, truncateLength);
|
||||
}
|
||||
|
||||
const description = getMessageSummaryDescription(lang, message, text);
|
||||
|
||||
return `${emojiWithSpace}${description}`;
|
||||
}
|
||||
|
||||
export function getMessageSummaryEmoji(message: ApiMessage) {
|
||||
const {
|
||||
photo, video, audio, voice, document, sticker, poll,
|
||||
} = message.content;
|
||||
|
||||
if (message.groupedId || photo) {
|
||||
return '🖼';
|
||||
}
|
||||
|
||||
if (video) {
|
||||
return '📹';
|
||||
}
|
||||
|
||||
if (sticker) {
|
||||
return sticker.emoji;
|
||||
}
|
||||
|
||||
if (audio) {
|
||||
return '🎧';
|
||||
}
|
||||
|
||||
if (voice) {
|
||||
return '🎤';
|
||||
}
|
||||
|
||||
if (document) {
|
||||
return '📎';
|
||||
}
|
||||
|
||||
if (poll) {
|
||||
return '📊';
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
||||
|
||||
export function getMessageSummaryDescription(lang: LangFn, message: ApiMessage, truncatedText?: string | TextPart[]) {
|
||||
const {
|
||||
text, photo, video, audio, voice, document, sticker, contact, poll, invoice,
|
||||
} = message.content;
|
||||
|
||||
if (message.groupedId) {
|
||||
return truncatedText || lang('lng_in_dlg_album');
|
||||
}
|
||||
|
||||
if (photo) {
|
||||
return truncatedText || lang('AttachPhoto');
|
||||
}
|
||||
|
||||
if (video) {
|
||||
return truncatedText || lang(video.isGif ? 'AttachGif' : 'AttachVideo');
|
||||
}
|
||||
|
||||
if (sticker) {
|
||||
return lang('AttachSticker').trim();
|
||||
}
|
||||
|
||||
if (audio) {
|
||||
return getMessageAudioCaption(message) || lang('AttachMusic');
|
||||
}
|
||||
|
||||
if (voice) {
|
||||
return truncatedText || lang('AttachAudio');
|
||||
}
|
||||
|
||||
if (document) {
|
||||
return truncatedText || document.fileName;
|
||||
}
|
||||
|
||||
if (contact) {
|
||||
return lang('AttachContact');
|
||||
}
|
||||
|
||||
if (poll) {
|
||||
return poll.summary.question;
|
||||
}
|
||||
|
||||
if (invoice) {
|
||||
return 'Invoice';
|
||||
}
|
||||
|
||||
if (text) {
|
||||
return truncatedText;
|
||||
}
|
||||
|
||||
return CONTENT_NOT_SUPPORTED;
|
||||
}
|
||||
|
||||
export function generateBrailleSpoiler(length: number) {
|
||||
return new Array(length)
|
||||
.fill(undefined)
|
||||
.map(() => SPOILER_CHARS[Math.floor(Math.random() * SPOILER_CHARS.length)])
|
||||
.join('');
|
||||
}
|
||||
|
||||
function getMessageAudioCaption(message: ApiMessage) {
|
||||
const { audio, text } = message.content;
|
||||
|
||||
return (audio && [audio.title, audio.performer].filter(Boolean).join(' — ')) || (text?.text);
|
||||
}
|
@ -4,18 +4,17 @@ import {
|
||||
import { LangFn } from '../../hooks/useLang';
|
||||
|
||||
import {
|
||||
LOCAL_MESSAGE_ID_BASE,
|
||||
SERVICE_NOTIFICATIONS_USER_ID,
|
||||
RE_LINK_TEMPLATE,
|
||||
CONTENT_NOT_SUPPORTED,
|
||||
LOCAL_MESSAGE_ID_BASE,
|
||||
RE_LINK_TEMPLATE,
|
||||
SERVICE_NOTIFICATIONS_USER_ID,
|
||||
} from '../../config';
|
||||
import { getUserFullName } from './users';
|
||||
import { isWebpSupported, IS_OPUS_SUPPORTED } from '../../util/environment';
|
||||
import { IS_OPUS_SUPPORTED, isWebpSupported } from '../../util/environment';
|
||||
import { getChatTitle, isUserId } from './chats';
|
||||
import parseEmojiOnlyString from '../../components/common/helpers/parseEmojiOnlyString';
|
||||
|
||||
const RE_LINK = new RegExp(RE_LINK_TEMPLATE, 'i');
|
||||
const TRUNCATED_SUMMARY_LENGTH = 80;
|
||||
|
||||
export type MessageKey = `msg${string}-${number}`;
|
||||
|
||||
@ -39,60 +38,6 @@ export function getMessageOriginalId(message: ApiMessage) {
|
||||
return message.previousLocalId || message.id;
|
||||
}
|
||||
|
||||
export function getMessageSummaryText(lang: LangFn, message: ApiMessage, noEmoji = false) {
|
||||
const {
|
||||
text, photo, video, audio, voice, document, sticker, contact, poll, invoice,
|
||||
} = message.content;
|
||||
|
||||
const truncatedText = text && text.text.substr(0, TRUNCATED_SUMMARY_LENGTH);
|
||||
|
||||
if (message.groupedId) {
|
||||
return `${noEmoji ? '' : '🖼 '}${truncatedText || lang('lng_in_dlg_album')}`;
|
||||
}
|
||||
|
||||
if (photo) {
|
||||
return `${noEmoji ? '' : '🖼 '}${truncatedText || lang('AttachPhoto')}`;
|
||||
}
|
||||
|
||||
if (video) {
|
||||
return `${noEmoji ? '' : '📹 '}${truncatedText || lang(video.isGif ? 'AttachGif' : 'AttachVideo')}`;
|
||||
}
|
||||
|
||||
if (sticker) {
|
||||
return `${sticker.emoji || ''} ${lang('AttachSticker')}`.trim();
|
||||
}
|
||||
|
||||
if (audio) {
|
||||
return `${noEmoji ? '' : '🎧 '}${getMessageAudioCaption(message) || lang('AttachMusic')}`;
|
||||
}
|
||||
|
||||
if (voice) {
|
||||
return `${noEmoji ? '' : '🎤 '}${truncatedText || lang('AttachAudio')}`;
|
||||
}
|
||||
|
||||
if (document) {
|
||||
return `${noEmoji ? '' : '📎 '}${truncatedText || document.fileName}`;
|
||||
}
|
||||
|
||||
if (contact) {
|
||||
return lang('AttachContact');
|
||||
}
|
||||
|
||||
if (poll) {
|
||||
return `${noEmoji ? '' : '📊 '}${poll.summary.question}`;
|
||||
}
|
||||
|
||||
if (invoice) {
|
||||
return 'Invoice';
|
||||
}
|
||||
|
||||
if (text) {
|
||||
return truncatedText;
|
||||
}
|
||||
|
||||
return CONTENT_NOT_SUPPORTED;
|
||||
}
|
||||
|
||||
export function getMessageText(message: ApiMessage) {
|
||||
const {
|
||||
text, sticker, photo, video, audio, voice, document, poll, webPage, contact, invoice,
|
||||
@ -230,12 +175,6 @@ export function isHistoryClearMessage(message: ApiMessage) {
|
||||
return message.content.action && message.content.action.type === 'historyClear';
|
||||
}
|
||||
|
||||
export function getMessageAudioCaption(message: ApiMessage) {
|
||||
const { audio, text } = message.content;
|
||||
|
||||
return (audio && [audio.title, audio.performer].filter(Boolean).join(' — ')) || (text?.text);
|
||||
}
|
||||
|
||||
export function getMessageContentFilename(message: ApiMessage) {
|
||||
const { content } = message;
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user