Action Message: Refactoring and fixes
This commit is contained in:
parent
8baad38a37
commit
c6672612bf
@ -46,7 +46,7 @@ const WebLink: FC<OwnProps> = ({
|
|||||||
siteName: domain.replace(/^www./, ''),
|
siteName: domain.replace(/^www./, ''),
|
||||||
url: url.includes('://') ? url : url.includes('@') ? `mailto:${url}` : `http://${url}`,
|
url: url.includes('://') ? url : url.includes('@') ? `mailto:${url}` : `http://${url}`,
|
||||||
formattedDescription: getMessageText(message) !== url
|
formattedDescription: getMessageText(message) !== url
|
||||||
? renderMessageSummary(lang, message, undefined, undefined, MAX_TEXT_LENGTH, true)
|
? renderMessageSummary(lang, message, undefined, undefined, MAX_TEXT_LENGTH)
|
||||||
: undefined,
|
: undefined,
|
||||||
} as ApiWebPageWithFormatted;
|
} as ApiWebPageWithFormatted;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -6,12 +6,12 @@ import {
|
|||||||
import { LangFn } from '../../../hooks/useLang';
|
import { LangFn } from '../../../hooks/useLang';
|
||||||
import {
|
import {
|
||||||
getChatTitle,
|
getChatTitle,
|
||||||
getMessageContent, getMessageSummaryText,
|
getMessageSummaryText,
|
||||||
getUserFullName,
|
getUserFullName,
|
||||||
} from '../../../modules/helpers';
|
} from '../../../modules/helpers';
|
||||||
import trimText from '../../../util/trimText';
|
import trimText from '../../../util/trimText';
|
||||||
import { formatCurrency } from '../../../util/formatCurrency';
|
import { formatCurrency } from '../../../util/formatCurrency';
|
||||||
import { renderMessageSummary, TextPart } from './renderMessageText';
|
import { TextPart, renderMessageSummary } from './renderMessageText';
|
||||||
import renderText from './renderText';
|
import renderText from './renderText';
|
||||||
|
|
||||||
import UserLink from '../UserLink';
|
import UserLink from '../UserLink';
|
||||||
@ -19,12 +19,12 @@ import MessageLink from '../MessageLink';
|
|||||||
import ChatLink from '../ChatLink';
|
import ChatLink from '../ChatLink';
|
||||||
import GroupCallLink from '../GroupCallLink';
|
import GroupCallLink from '../GroupCallLink';
|
||||||
|
|
||||||
interface ActionMessageTextOptions {
|
interface RenderOptions {
|
||||||
maxTextLength?: number;
|
asPlainText?: boolean;
|
||||||
asPlain?: boolean;
|
asTextWithSpoilers?: boolean;
|
||||||
isEmbedded?: boolean;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const MAX_LENGTH = 32;
|
||||||
const NBSP = '\u00A0';
|
const NBSP = '\u00A0';
|
||||||
|
|
||||||
export function renderActionMessageText(
|
export function renderActionMessageText(
|
||||||
@ -35,21 +35,22 @@ export function renderActionMessageText(
|
|||||||
targetUsers?: ApiUser[],
|
targetUsers?: ApiUser[],
|
||||||
targetMessage?: ApiMessage,
|
targetMessage?: ApiMessage,
|
||||||
targetChatId?: string,
|
targetChatId?: string,
|
||||||
options: ActionMessageTextOptions = {},
|
options: RenderOptions = {},
|
||||||
) {
|
) {
|
||||||
if (!message.content.action) {
|
if (!message.content.action) {
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
const {
|
const {
|
||||||
text, translationValues, amount, currency, call,
|
text, translationValues, amount, currency, call,
|
||||||
} = message.content.action;
|
} = message.content.action;
|
||||||
const content: TextPart[] = [];
|
const content: TextPart[] = [];
|
||||||
const textOptions: ActionMessageTextOptions = { ...options, maxTextLength: 32 };
|
const noLinks = options.asPlainText || options.asTextWithSpoilers;
|
||||||
const translationKey = text === 'Chat.Service.Group.UpdatedPinnedMessage1' && !targetMessage
|
const translationKey = text === 'Chat.Service.Group.UpdatedPinnedMessage1' && !targetMessage
|
||||||
? 'Message.PinnedGenericMessage'
|
? 'Message.PinnedGenericMessage'
|
||||||
: text;
|
: text;
|
||||||
|
|
||||||
let unprocessed = lang(translationKey, translationValues && translationValues.length ? translationValues : undefined);
|
let unprocessed = lang(translationKey, translationValues?.length ? translationValues : undefined);
|
||||||
let processed: TextPart[];
|
let processed: TextPart[];
|
||||||
|
|
||||||
if (unprocessed.includes('%payment_amount%')) {
|
if (unprocessed.includes('%payment_amount%')) {
|
||||||
@ -66,10 +67,9 @@ export function renderActionMessageText(
|
|||||||
unprocessed,
|
unprocessed,
|
||||||
'%action_origin%',
|
'%action_origin%',
|
||||||
actionOriginUser ? (
|
actionOriginUser ? (
|
||||||
(!options.isEmbedded && renderUserContent(actionOriginUser, options.asPlain)) || NBSP
|
renderUserContent(actionOriginUser, noLinks) || NBSP
|
||||||
|
|
||||||
) : actionOriginChat ? (
|
) : actionOriginChat ? (
|
||||||
(!options.isEmbedded && renderChatContent(lang, actionOriginChat, options.asPlain)) || NBSP
|
renderChatContent(lang, actionOriginChat, noLinks) || NBSP
|
||||||
) : 'User',
|
) : 'User',
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -80,7 +80,7 @@ export function renderActionMessageText(
|
|||||||
unprocessed,
|
unprocessed,
|
||||||
'%target_user%',
|
'%target_user%',
|
||||||
targetUsers
|
targetUsers
|
||||||
? targetUsers.map((user) => renderUserContent(user, options.asPlain)).filter<TextPart>(Boolean as any)
|
? targetUsers.map((user) => renderUserContent(user, noLinks)).filter<TextPart>(Boolean as any)
|
||||||
: 'User',
|
: 'User',
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -91,7 +91,7 @@ export function renderActionMessageText(
|
|||||||
unprocessed,
|
unprocessed,
|
||||||
'%message%',
|
'%message%',
|
||||||
targetMessage
|
targetMessage
|
||||||
? renderMessageContent(lang, targetMessage, textOptions)
|
? renderMessageContent(lang, targetMessage, options)
|
||||||
: 'a message',
|
: 'a message',
|
||||||
);
|
);
|
||||||
unprocessed = processed.pop() as string;
|
unprocessed = processed.pop() as string;
|
||||||
@ -111,12 +111,12 @@ export function renderActionMessageText(
|
|||||||
unprocessed,
|
unprocessed,
|
||||||
'%target_chat%',
|
'%target_chat%',
|
||||||
targetChatId
|
targetChatId
|
||||||
? renderMigratedContent(targetChatId, options.asPlain)
|
? renderMigratedContent(targetChatId, noLinks)
|
||||||
: 'another chat',
|
: 'another chat',
|
||||||
);
|
);
|
||||||
content.push(...processed);
|
content.push(...processed);
|
||||||
|
|
||||||
if (options.asPlain) {
|
if (options.asPlainText) {
|
||||||
return content.join('').trim();
|
return content.join('').trim();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -133,47 +133,23 @@ function renderProductContent(message: ApiMessage) {
|
|||||||
: 'a product';
|
: 'a product';
|
||||||
}
|
}
|
||||||
|
|
||||||
function renderMessageContent(lang: LangFn, message: ApiMessage, options: ActionMessageTextOptions = {}) {
|
function renderMessageContent(lang: LangFn, message: ApiMessage, options: RenderOptions = {}) {
|
||||||
const { maxTextLength, isEmbedded, asPlain } = options;
|
const { asPlainText, asTextWithSpoilers } = options;
|
||||||
|
|
||||||
const text = asPlain
|
if (asPlainText) {
|
||||||
? [trimText(getMessageSummaryText(lang, message), maxTextLength)]
|
return getMessageSummaryText(lang, message, undefined, MAX_LENGTH);
|
||||||
: renderMessageSummary(lang, message, undefined, undefined, maxTextLength, true);
|
|
||||||
const {
|
|
||||||
photo, video, document, sticker,
|
|
||||||
} = getMessageContent(message);
|
|
||||||
|
|
||||||
const showQuotes = isEmbedded && text && !photo && !video && !document && !sticker;
|
|
||||||
let messageText = text;
|
|
||||||
|
|
||||||
if (isEmbedded) {
|
|
||||||
if (photo) {
|
|
||||||
messageText = ['a photo'];
|
|
||||||
} else if (video) {
|
|
||||||
messageText = [video.isGif ? 'a GIF' : 'a video'];
|
|
||||||
} else if (document) {
|
|
||||||
messageText = ['a document'];
|
|
||||||
} else if (sticker) {
|
|
||||||
messageText = text;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (asPlain && messageText) {
|
const messageSummary = renderMessageSummary(lang, message, undefined, undefined, MAX_LENGTH);
|
||||||
return (showQuotes ? ['«', ...messageText, '»'] : messageText).join('');
|
|
||||||
}
|
|
||||||
|
|
||||||
if (showQuotes) {
|
if (asTextWithSpoilers) {
|
||||||
return (
|
return (
|
||||||
<span>
|
<span>{messageSummary}</span>
|
||||||
«
|
|
||||||
<MessageLink className="action-link" message={message}>{messageText}</MessageLink>
|
|
||||||
»
|
|
||||||
</span>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<MessageLink className="action-link" message={message}>{messageText}</MessageLink>
|
<MessageLink className="action-link" message={message}>{messageSummary}</MessageLink>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -185,30 +161,30 @@ function renderGroupCallContent(groupCall: Partial<ApiGroupCall>, text: TextPart
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function renderUserContent(sender: ApiUser, asPlain?: boolean): string | TextPart | undefined {
|
function renderUserContent(sender: ApiUser, noLinks?: boolean): string | TextPart | undefined {
|
||||||
const text = trimText(getUserFullName(sender));
|
const text = trimText(getUserFullName(sender), MAX_LENGTH);
|
||||||
|
|
||||||
if (asPlain) {
|
if (noLinks) {
|
||||||
return text;
|
return text;
|
||||||
}
|
}
|
||||||
|
|
||||||
return <UserLink className="action-link" sender={sender}>{sender && renderText(text!)}</UserLink>;
|
return <UserLink className="action-link" sender={sender}>{sender && renderText(text!)}</UserLink>;
|
||||||
}
|
}
|
||||||
|
|
||||||
function renderChatContent(lang: LangFn, chat: ApiChat, asPlain?: boolean): string | TextPart | undefined {
|
function renderChatContent(lang: LangFn, chat: ApiChat, noLinks?: boolean): string | TextPart | undefined {
|
||||||
const text = trimText(getChatTitle(lang, chat));
|
const text = trimText(getChatTitle(lang, chat), MAX_LENGTH);
|
||||||
|
|
||||||
if (asPlain) {
|
if (noLinks) {
|
||||||
return text;
|
return text;
|
||||||
}
|
}
|
||||||
|
|
||||||
return <ChatLink className="action-link" chatId={chat.id}>{chat && renderText(text!)}</ChatLink>;
|
return <ChatLink className="action-link" chatId={chat.id}>{chat && renderText(text!)}</ChatLink>;
|
||||||
}
|
}
|
||||||
|
|
||||||
function renderMigratedContent(chatId: string, asPlain?: boolean): string | TextPart | undefined {
|
function renderMigratedContent(chatId: string, noLinks?: boolean): string | TextPart | undefined {
|
||||||
const text = 'another chat';
|
const text = 'another chat';
|
||||||
|
|
||||||
if (asPlain) {
|
if (noLinks) {
|
||||||
return text;
|
return text;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
71
src/components/common/helpers/renderMessageText.ts
Normal file
71
src/components/common/helpers/renderMessageText.ts
Normal file
@ -0,0 +1,71 @@
|
|||||||
|
import { ApiMessage, ApiMessageEntityTypes } from '../../../api/types';
|
||||||
|
import {
|
||||||
|
getMessageSummaryDescription,
|
||||||
|
getMessageSummaryEmoji,
|
||||||
|
getMessageSummaryText,
|
||||||
|
getMessageText,
|
||||||
|
TRUNCATED_SUMMARY_LENGTH,
|
||||||
|
} from '../../../modules/helpers';
|
||||||
|
import { LangFn } from '../../../hooks/useLang';
|
||||||
|
import renderText from './renderText';
|
||||||
|
import { renderTextWithEntities, TextPart } from './renderTextWithEntities';
|
||||||
|
import trimText from '../../../util/trimText';
|
||||||
|
|
||||||
|
export type { TextPart };
|
||||||
|
|
||||||
|
export function renderMessageText(
|
||||||
|
message: ApiMessage,
|
||||||
|
highlight?: string,
|
||||||
|
shouldRenderHqEmoji?: boolean,
|
||||||
|
isSimple?: boolean,
|
||||||
|
truncateLength?: number,
|
||||||
|
) {
|
||||||
|
const { text, entities } = message.content.text || {};
|
||||||
|
|
||||||
|
if (!text) {
|
||||||
|
const contentNotSupportedText = getMessageText(message);
|
||||||
|
return contentNotSupportedText ? [trimText(contentNotSupportedText, truncateLength)] : undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
return renderTextWithEntities(
|
||||||
|
trimText(text, truncateLength),
|
||||||
|
entities,
|
||||||
|
highlight,
|
||||||
|
shouldRenderHqEmoji,
|
||||||
|
undefined,
|
||||||
|
message.id,
|
||||||
|
isSimple,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function renderMessageSummary(
|
||||||
|
lang: LangFn,
|
||||||
|
message: ApiMessage,
|
||||||
|
noEmoji = false,
|
||||||
|
highlight?: string,
|
||||||
|
truncateLength = TRUNCATED_SUMMARY_LENGTH,
|
||||||
|
): TextPart[] {
|
||||||
|
let { entities } = message.content.text || {};
|
||||||
|
|
||||||
|
const hasSpoilers = entities?.some((e) => e.type === ApiMessageEntityTypes.Spoiler);
|
||||||
|
if (!hasSpoilers) {
|
||||||
|
const text = trimText(getMessageSummaryText(lang, message, noEmoji), truncateLength);
|
||||||
|
|
||||||
|
if (highlight) {
|
||||||
|
return renderText(text, ['emoji', 'highlight'], { highlight });
|
||||||
|
} else {
|
||||||
|
return renderText(text);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const emoji = !noEmoji && getMessageSummaryEmoji(message);
|
||||||
|
const emojiWithSpace = emoji ? `${emoji} ` : '';
|
||||||
|
|
||||||
|
const text = renderMessageText(message, highlight, undefined, true, truncateLength);
|
||||||
|
const description = getMessageSummaryDescription(lang, message, text);
|
||||||
|
|
||||||
|
return [
|
||||||
|
emojiWithSpace,
|
||||||
|
...(Array.isArray(description) ? description : [description]),
|
||||||
|
].filter<TextPart>(Boolean);
|
||||||
|
}
|
||||||
@ -2,159 +2,23 @@ import { MouseEvent } from 'react';
|
|||||||
import React from '../../../lib/teact/teact';
|
import React from '../../../lib/teact/teact';
|
||||||
import { getDispatch } from '../../../lib/teact/teactn';
|
import { getDispatch } from '../../../lib/teact/teactn';
|
||||||
|
|
||||||
import { ApiMessageEntity, ApiMessageEntityTypes, ApiMessage } from '../../../api/types';
|
import { ApiFormattedText, ApiMessageEntity, ApiMessageEntityTypes } from '../../../api/types';
|
||||||
|
|
||||||
import {
|
|
||||||
getMessageSummaryText,
|
|
||||||
getMessageSummaryDescription,
|
|
||||||
getMessageSummaryEmoji,
|
|
||||||
getMessageText,
|
|
||||||
TRUNCATED_SUMMARY_LENGTH,
|
|
||||||
} from '../../../modules/helpers';
|
|
||||||
import renderText, { TextFilter } from './renderText';
|
import renderText, { TextFilter } from './renderText';
|
||||||
|
|
||||||
import MentionLink from '../../middle/message/MentionLink';
|
import MentionLink from '../../middle/message/MentionLink';
|
||||||
import SafeLink from '../SafeLink';
|
import SafeLink from '../SafeLink';
|
||||||
import Spoiler from '../spoiler/Spoiler';
|
import Spoiler from '../spoiler/Spoiler';
|
||||||
import { LangFn } from '../../../hooks/useLang';
|
|
||||||
|
|
||||||
export type TextPart =
|
export type TextPart =
|
||||||
string
|
string
|
||||||
| Element;
|
| Element;
|
||||||
|
|
||||||
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<TextPart>(Boolean);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function renderMessageText(
|
|
||||||
message: ApiMessage,
|
|
||||||
highlight?: string,
|
|
||||||
shouldRenderHqEmoji?: boolean,
|
|
||||||
isSimple?: boolean,
|
|
||||||
truncateLength?: number,
|
|
||||||
) {
|
|
||||||
const formattedText = message.content.text;
|
|
||||||
|
|
||||||
if (!formattedText || !formattedText.text) {
|
|
||||||
const rawText = getMessageText(message);
|
|
||||||
return rawText ? [rawText] : undefined;
|
|
||||||
}
|
|
||||||
const { text, entities } = formattedText;
|
|
||||||
|
|
||||||
return renderTextWithEntities(
|
|
||||||
truncateLength ? text.substr(0, truncateLength) : text,
|
|
||||||
entities,
|
|
||||||
highlight,
|
|
||||||
shouldRenderHqEmoji,
|
|
||||||
undefined,
|
|
||||||
message.id,
|
|
||||||
isSimple,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
interface IOrganizedEntity {
|
interface IOrganizedEntity {
|
||||||
entity: ApiMessageEntity;
|
entity: ApiMessageEntity;
|
||||||
organizedIndexes: Set<number>;
|
organizedIndexes: Set<number>;
|
||||||
nestedEntities: IOrganizedEntity[];
|
nestedEntities: IOrganizedEntity[];
|
||||||
}
|
}
|
||||||
|
|
||||||
function organizeEntity(
|
|
||||||
entity: ApiMessageEntity,
|
|
||||||
index: number,
|
|
||||||
entities: ApiMessageEntity[],
|
|
||||||
organizedEntityIndexes: Set<number>,
|
|
||||||
): IOrganizedEntity | undefined {
|
|
||||||
const { offset, length } = entity;
|
|
||||||
const organizedIndexes = new Set([index]);
|
|
||||||
|
|
||||||
if (organizedEntityIndexes.has(index)) {
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Determine any nested entities inside current entity
|
|
||||||
const nestedEntities: IOrganizedEntity[] = [];
|
|
||||||
const parsedNestedEntities = entities
|
|
||||||
.filter((e, i) => i > index && e.offset >= offset && e.offset < offset + length)
|
|
||||||
.map((e) => organizeEntity(e, entities.indexOf(e), entities, organizedEntityIndexes))
|
|
||||||
.filter<IOrganizedEntity>(Boolean as any);
|
|
||||||
|
|
||||||
parsedNestedEntities.forEach((parsedEntity) => {
|
|
||||||
let isChanged = false;
|
|
||||||
|
|
||||||
parsedEntity.organizedIndexes.forEach((organizedIndex) => {
|
|
||||||
if (!isChanged && !organizedIndexes.has(organizedIndex)) {
|
|
||||||
isChanged = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
organizedIndexes.add(organizedIndex);
|
|
||||||
});
|
|
||||||
|
|
||||||
if (isChanged) {
|
|
||||||
nestedEntities.push(parsedEntity);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
return {
|
|
||||||
entity,
|
|
||||||
organizedIndexes,
|
|
||||||
nestedEntities,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
// Organize entities in a tree-like structure to better represent how the text will be displayed
|
|
||||||
function organizeEntities(entities: ApiMessageEntity[]) {
|
|
||||||
const organizedEntityIndexes: Set<number> = new Set();
|
|
||||||
const organizedEntities: IOrganizedEntity[] = [];
|
|
||||||
|
|
||||||
entities.forEach((entity, index) => {
|
|
||||||
if (organizedEntityIndexes.has(index)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const organizedEntity = organizeEntity(entity, index, entities, organizedEntityIndexes);
|
|
||||||
if (organizedEntity) {
|
|
||||||
organizedEntity.organizedIndexes.forEach((organizedIndex) => {
|
|
||||||
organizedEntityIndexes.add(organizedIndex);
|
|
||||||
});
|
|
||||||
|
|
||||||
organizedEntities.push(organizedEntity);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
return organizedEntities;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function renderTextWithEntities(
|
export function renderTextWithEntities(
|
||||||
text: string,
|
text: string,
|
||||||
entities?: ApiMessageEntity[],
|
entities?: ApiMessageEntity[],
|
||||||
@ -283,6 +147,128 @@ export function renderTextWithEntities(
|
|||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function getTextWithEntitiesAsHtml(formattedText?: ApiFormattedText) {
|
||||||
|
const { text, entities } = formattedText || {};
|
||||||
|
if (!text) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = renderTextWithEntities(
|
||||||
|
text,
|
||||||
|
entities,
|
||||||
|
undefined,
|
||||||
|
undefined,
|
||||||
|
true,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (Array.isArray(result)) {
|
||||||
|
return result.join('');
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderMessagePart(
|
||||||
|
content: TextPart | TextPart[],
|
||||||
|
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, isSimple));
|
||||||
|
});
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (shouldRenderAsHtml) {
|
||||||
|
return renderText(content, ['escape_html', 'emoji_html', 'br_html']);
|
||||||
|
}
|
||||||
|
|
||||||
|
const emojiFilter = shouldRenderHqEmoji ? 'hq_emoji' : 'emoji';
|
||||||
|
|
||||||
|
const filters: TextFilter[] = [emojiFilter];
|
||||||
|
if (!isSimple) {
|
||||||
|
filters.push('br');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (highlight) {
|
||||||
|
return renderText(content, filters.concat('highlight'), { highlight });
|
||||||
|
} else {
|
||||||
|
return renderText(content, filters);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Organize entities in a tree-like structure to better represent how the text will be displayed
|
||||||
|
function organizeEntities(entities: ApiMessageEntity[]) {
|
||||||
|
const organizedEntityIndexes: Set<number> = new Set();
|
||||||
|
const organizedEntities: IOrganizedEntity[] = [];
|
||||||
|
|
||||||
|
entities.forEach((entity, index) => {
|
||||||
|
if (organizedEntityIndexes.has(index)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const organizedEntity = organizeEntity(entity, index, entities, organizedEntityIndexes);
|
||||||
|
if (organizedEntity) {
|
||||||
|
organizedEntity.organizedIndexes.forEach((organizedIndex) => {
|
||||||
|
organizedEntityIndexes.add(organizedIndex);
|
||||||
|
});
|
||||||
|
|
||||||
|
organizedEntities.push(organizedEntity);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return organizedEntities;
|
||||||
|
}
|
||||||
|
|
||||||
|
function organizeEntity(
|
||||||
|
entity: ApiMessageEntity,
|
||||||
|
index: number,
|
||||||
|
entities: ApiMessageEntity[],
|
||||||
|
organizedEntityIndexes: Set<number>,
|
||||||
|
): IOrganizedEntity | undefined {
|
||||||
|
const { offset, length } = entity;
|
||||||
|
const organizedIndexes = new Set([index]);
|
||||||
|
|
||||||
|
if (organizedEntityIndexes.has(index)) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Determine any nested entities inside current entity
|
||||||
|
const nestedEntities: IOrganizedEntity[] = [];
|
||||||
|
const parsedNestedEntities = entities
|
||||||
|
.filter((e, i) => i > index && e.offset >= offset && e.offset < offset + length)
|
||||||
|
.map((e) => organizeEntity(e, entities.indexOf(e), entities, organizedEntityIndexes))
|
||||||
|
.filter<IOrganizedEntity>(Boolean as any);
|
||||||
|
|
||||||
|
parsedNestedEntities.forEach((parsedEntity) => {
|
||||||
|
let isChanged = false;
|
||||||
|
|
||||||
|
parsedEntity.organizedIndexes.forEach((organizedIndex) => {
|
||||||
|
if (!isChanged && !organizedIndexes.has(organizedIndex)) {
|
||||||
|
isChanged = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
organizedIndexes.add(organizedIndex);
|
||||||
|
});
|
||||||
|
|
||||||
|
if (isChanged) {
|
||||||
|
nestedEntities.push(parsedEntity);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
entity,
|
||||||
|
organizedIndexes,
|
||||||
|
nestedEntities,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
function processEntity(
|
function processEntity(
|
||||||
entity: ApiMessageEntity,
|
entity: ApiMessageEntity,
|
||||||
entityContent: TextPart,
|
entityContent: TextPart,
|
||||||
@ -408,55 +394,6 @@ function processEntity(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function renderMessagePart(
|
|
||||||
content: TextPart | TextPart[],
|
|
||||||
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, isSimple));
|
|
||||||
});
|
|
||||||
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (shouldRenderAsHtml) {
|
|
||||||
return renderText(content, ['escape_html', 'emoji_html', 'br_html']);
|
|
||||||
}
|
|
||||||
|
|
||||||
const emojiFilter = shouldRenderHqEmoji ? 'hq_emoji' : 'emoji';
|
|
||||||
|
|
||||||
const filters: TextFilter[] = [emojiFilter];
|
|
||||||
if (!isSimple) {
|
|
||||||
filters.push('br');
|
|
||||||
}
|
|
||||||
|
|
||||||
if (highlight) {
|
|
||||||
return renderText(content, filters.concat('highlight'), { highlight });
|
|
||||||
} else {
|
|
||||||
return renderText(content, filters);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function getLinkUrl(entityContent: string, entity: ApiMessageEntity) {
|
|
||||||
const { type, url } = entity;
|
|
||||||
return type === ApiMessageEntityTypes.TextUrl && url ? url : entityContent;
|
|
||||||
}
|
|
||||||
|
|
||||||
function handleBotCommandClick(e: MouseEvent<HTMLAnchorElement>) {
|
|
||||||
getDispatch().sendBotCommand({ command: e.currentTarget.innerText });
|
|
||||||
}
|
|
||||||
|
|
||||||
function handleHashtagClick(e: MouseEvent<HTMLAnchorElement>) {
|
|
||||||
getDispatch().setLocalTextSearchQuery({ query: e.currentTarget.innerText });
|
|
||||||
getDispatch().searchTextMessagesLocal();
|
|
||||||
}
|
|
||||||
|
|
||||||
function processEntityAsHtml(
|
function processEntityAsHtml(
|
||||||
entity: ApiMessageEntity,
|
entity: ApiMessageEntity,
|
||||||
entityContent: TextPart,
|
entityContent: TextPart,
|
||||||
@ -510,3 +447,17 @@ function processEntityAsHtml(
|
|||||||
return renderedContent;
|
return renderedContent;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getLinkUrl(entityContent: string, entity: ApiMessageEntity) {
|
||||||
|
const { type, url } = entity;
|
||||||
|
return type === ApiMessageEntityTypes.TextUrl && url ? url : entityContent;
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleBotCommandClick(e: MouseEvent<HTMLAnchorElement>) {
|
||||||
|
getDispatch().sendBotCommand({ command: e.currentTarget.innerText });
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleHashtagClick(e: MouseEvent<HTMLAnchorElement>) {
|
||||||
|
getDispatch().setLocalTextSearchQuery({ query: e.currentTarget.innerText });
|
||||||
|
getDispatch().searchTextMessagesLocal();
|
||||||
|
}
|
||||||
@ -247,6 +247,7 @@ const Chat: FC<OwnProps & StateProps> = ({
|
|||||||
actionTargetUsers,
|
actionTargetUsers,
|
||||||
actionTargetMessage,
|
actionTargetMessage,
|
||||||
actionTargetChatId,
|
actionTargetChatId,
|
||||||
|
{ asTextWithSpoilers: true },
|
||||||
)}
|
)}
|
||||||
</p>
|
</p>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -1,10 +1,10 @@
|
|||||||
import React, { FC, useEffect, useState } from '../../lib/teact/teact';
|
import React, { FC, useEffect, useState } from '../../lib/teact/teact';
|
||||||
|
|
||||||
import { throttle } from '../../util/schedulers';
|
|
||||||
import { TextPart } from '../common/helpers/renderMessageText';
|
|
||||||
import buildClassName from '../../util/buildClassName';
|
|
||||||
import { REM } from '../common/helpers/mediaDimensions';
|
|
||||||
import { IS_SINGLE_COLUMN_LAYOUT } from '../../util/environment';
|
import { IS_SINGLE_COLUMN_LAYOUT } from '../../util/environment';
|
||||||
|
import { throttle } from '../../util/schedulers';
|
||||||
|
import buildClassName from '../../util/buildClassName';
|
||||||
|
import { TextPart } from '../common/helpers/renderMessageText';
|
||||||
|
import { REM } from '../common/helpers/mediaDimensions';
|
||||||
|
|
||||||
import './MediaViewerFooter.scss';
|
import './MediaViewerFooter.scss';
|
||||||
|
|
||||||
|
|||||||
@ -98,7 +98,7 @@ const ActionMessage: FC<OwnProps & StateProps> = ({
|
|||||||
targetUsers,
|
targetUsers,
|
||||||
targetMessage,
|
targetMessage,
|
||||||
targetChatId,
|
targetChatId,
|
||||||
isEmbedded ? { isEmbedded: true } : undefined,
|
{ asTextWithSpoilers: isEmbedded },
|
||||||
);
|
);
|
||||||
const {
|
const {
|
||||||
isContextMenuOpen, contextMenuPosition,
|
isContextMenuOpen, contextMenuPosition,
|
||||||
|
|||||||
@ -1,23 +0,0 @@
|
|||||||
import { ApiFormattedText } from '../../../../api/types';
|
|
||||||
import { renderTextWithEntities } from '../../../common/helpers/renderMessageText';
|
|
||||||
|
|
||||||
export default function getMessageTextAsHtml(formattedText?: ApiFormattedText) {
|
|
||||||
const { text, entities } = formattedText || {};
|
|
||||||
if (!text) {
|
|
||||||
return '';
|
|
||||||
}
|
|
||||||
|
|
||||||
const result = renderTextWithEntities(
|
|
||||||
text,
|
|
||||||
entities,
|
|
||||||
undefined,
|
|
||||||
undefined,
|
|
||||||
true,
|
|
||||||
);
|
|
||||||
|
|
||||||
if (Array.isArray(result)) {
|
|
||||||
return result.join('');
|
|
||||||
}
|
|
||||||
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
@ -8,10 +8,10 @@ import usePrevious from '../../../../hooks/usePrevious';
|
|||||||
import { debounce } from '../../../../util/schedulers';
|
import { debounce } from '../../../../util/schedulers';
|
||||||
import focusEditableElement from '../../../../util/focusEditableElement';
|
import focusEditableElement from '../../../../util/focusEditableElement';
|
||||||
import parseMessageInput from '../../../../util/parseMessageInput';
|
import parseMessageInput from '../../../../util/parseMessageInput';
|
||||||
import getMessageTextAsHtml from '../helpers/getMessageTextAsHtml';
|
|
||||||
import useBackgroundMode from '../../../../hooks/useBackgroundMode';
|
import useBackgroundMode from '../../../../hooks/useBackgroundMode';
|
||||||
import useBeforeUnload from '../../../../hooks/useBeforeUnload';
|
import useBeforeUnload from '../../../../hooks/useBeforeUnload';
|
||||||
import { IS_TOUCH_ENV } from '../../../../util/environment';
|
import { IS_TOUCH_ENV } from '../../../../util/environment';
|
||||||
|
import { getTextWithEntitiesAsHtml } from '../../../common/helpers/renderTextWithEntities';
|
||||||
|
|
||||||
// Used to avoid running debounced callbacks when chat changes.
|
// Used to avoid running debounced callbacks when chat changes.
|
||||||
let currentChatId: string | undefined;
|
let currentChatId: string | undefined;
|
||||||
@ -65,7 +65,7 @@ const useDraft = (
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
setHtml(getMessageTextAsHtml(draft));
|
setHtml(getTextWithEntitiesAsHtml(draft));
|
||||||
|
|
||||||
if (!IS_TOUCH_ENV) {
|
if (!IS_TOUCH_ENV) {
|
||||||
requestAnimationFrame(() => {
|
requestAnimationFrame(() => {
|
||||||
|
|||||||
@ -5,9 +5,9 @@ import { ApiMessage } from '../../../../api/types';
|
|||||||
|
|
||||||
import { EDITABLE_INPUT_ID } from '../../../../config';
|
import { EDITABLE_INPUT_ID } from '../../../../config';
|
||||||
import parseMessageInput from '../../../../util/parseMessageInput';
|
import parseMessageInput from '../../../../util/parseMessageInput';
|
||||||
import getMessageTextAsHtml from '../helpers/getMessageTextAsHtml';
|
|
||||||
import focusEditableElement from '../../../../util/focusEditableElement';
|
import focusEditableElement from '../../../../util/focusEditableElement';
|
||||||
import { hasMessageMedia } from '../../../../modules/helpers';
|
import { hasMessageMedia } from '../../../../modules/helpers';
|
||||||
|
import { getTextWithEntitiesAsHtml } from '../../../common/helpers/renderTextWithEntities';
|
||||||
|
|
||||||
const useEditing = (
|
const useEditing = (
|
||||||
htmlRef: { current: string },
|
htmlRef: { current: string },
|
||||||
@ -26,7 +26,7 @@ const useEditing = (
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
setHtml(getMessageTextAsHtml(editedMessage.content.text));
|
setHtml(getTextWithEntitiesAsHtml(editedMessage.content.text));
|
||||||
|
|
||||||
requestAnimationFrame(() => {
|
requestAnimationFrame(() => {
|
||||||
const messageInput = document.getElementById(EDITABLE_INPUT_ID)!;
|
const messageInput = document.getElementById(EDITABLE_INPUT_ID)!;
|
||||||
|
|||||||
@ -14,7 +14,7 @@ import {
|
|||||||
} from '../../../api/types';
|
} from '../../../api/types';
|
||||||
|
|
||||||
import renderText from '../../common/helpers/renderText';
|
import renderText from '../../common/helpers/renderText';
|
||||||
import { renderTextWithEntities } from '../../common/helpers/renderMessageText';
|
import { renderTextWithEntities } from '../../common/helpers/renderTextWithEntities';
|
||||||
import { formatMediaDuration } from '../../../util/dateFormat';
|
import { formatMediaDuration } from '../../../util/dateFormat';
|
||||||
import useLang, { LangFn } from '../../../hooks/useLang';
|
import useLang, { LangFn } from '../../../hooks/useLang';
|
||||||
|
|
||||||
|
|||||||
@ -6,7 +6,7 @@ import { getDispatch, withGlobal } from '../../../lib/teact/teactn';
|
|||||||
|
|
||||||
import { ApiChat, ApiSponsoredMessage, ApiUser } from '../../../api/types';
|
import { ApiChat, ApiSponsoredMessage, ApiUser } from '../../../api/types';
|
||||||
|
|
||||||
import { renderTextWithEntities } from '../../common/helpers/renderMessageText';
|
import { renderTextWithEntities } from '../../common/helpers/renderTextWithEntities';
|
||||||
import { selectChat, selectSponsoredMessage, selectUser } from '../../../modules/selectors';
|
import { selectChat, selectSponsoredMessage, selectUser } from '../../../modules/selectors';
|
||||||
import { getChatTitle, getUserFullName } from '../../../modules/helpers';
|
import { getChatTitle, getUserFullName } from '../../../modules/helpers';
|
||||||
import renderText from '../../common/helpers/renderText';
|
import renderText from '../../common/helpers/renderText';
|
||||||
|
|||||||
@ -8,9 +8,9 @@ import React, {
|
|||||||
|
|
||||||
import { ANIMATION_END_DELAY } from '../../config';
|
import { ANIMATION_END_DELAY } from '../../config';
|
||||||
import useShowTransition from '../../hooks/useShowTransition';
|
import useShowTransition from '../../hooks/useShowTransition';
|
||||||
import { TextPart } from '../common/helpers/renderMessageText';
|
|
||||||
import buildClassName from '../../util/buildClassName';
|
import buildClassName from '../../util/buildClassName';
|
||||||
import captureEscKeyListener from '../../util/captureEscKeyListener';
|
import captureEscKeyListener from '../../util/captureEscKeyListener';
|
||||||
|
import { TextPart } from '../common/helpers/renderMessageText';
|
||||||
|
|
||||||
import Portal from './Portal';
|
import Portal from './Portal';
|
||||||
|
|
||||||
|
|||||||
@ -1,8 +1,10 @@
|
|||||||
|
import type { TextPart } from '../../components/common/helpers/renderTextWithEntities';
|
||||||
|
|
||||||
import { LangFn } from '../../hooks/useLang';
|
import { LangFn } from '../../hooks/useLang';
|
||||||
import { ApiMessage, ApiMessageEntityTypes } from '../../api/types';
|
import { ApiMessage, ApiMessageEntityTypes } from '../../api/types';
|
||||||
import type { TextPart } from '../../components/common/helpers/renderMessageText';
|
|
||||||
import { CONTENT_NOT_SUPPORTED } from '../../config';
|
import { CONTENT_NOT_SUPPORTED } from '../../config';
|
||||||
import { getMessageText } from './messages';
|
import { getMessageText } from './messages';
|
||||||
|
import trimText from '../../util/trimText';
|
||||||
|
|
||||||
const SPOILER_CHARS = ['⠺', '⠵', '⠞', '⠟'];
|
const SPOILER_CHARS = ['⠺', '⠵', '⠞', '⠟'];
|
||||||
export const TRUNCATED_SUMMARY_LENGTH = 80;
|
export const TRUNCATED_SUMMARY_LENGTH = 80;
|
||||||
@ -15,7 +17,7 @@ export function getMessageSummaryText(
|
|||||||
) {
|
) {
|
||||||
const emoji = !noEmoji && getMessageSummaryEmoji(message);
|
const emoji = !noEmoji && getMessageSummaryEmoji(message);
|
||||||
const emojiWithSpace = emoji ? `${emoji} ` : '';
|
const emojiWithSpace = emoji ? `${emoji} ` : '';
|
||||||
const text = getMessageTextWithSpoilers(message)?.substr(0, truncateLength);
|
const text = trimText(getMessageTextWithSpoilers(message), truncateLength);
|
||||||
const description = getMessageSummaryDescription(lang, message, text);
|
const description = getMessageSummaryDescription(lang, message, text);
|
||||||
|
|
||||||
return `${emojiWithSpace}${description}`;
|
return `${emojiWithSpace}${description}`;
|
||||||
@ -39,6 +41,7 @@ export function getMessageTextWithSpoilers(message: ApiMessage) {
|
|||||||
|
|
||||||
const spoiler = generateBrailleSpoiler(length);
|
const spoiler = generateBrailleSpoiler(length);
|
||||||
|
|
||||||
|
|
||||||
return `${accText.substr(0, offset)}${spoiler}${accText.substr(offset + length, accText.length)}`;
|
return `${accText.substr(0, offset)}${spoiler}${accText.substr(offset + length, accText.length)}`;
|
||||||
}, text);
|
}, text);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -287,7 +287,7 @@ function getNotificationContent(chat: ApiChat, message: ApiMessage) {
|
|||||||
actionTargetUsers,
|
actionTargetUsers,
|
||||||
actionTargetMessage,
|
actionTargetMessage,
|
||||||
actionTargetChatId,
|
actionTargetChatId,
|
||||||
{ asPlain: true },
|
{ asPlainText: true },
|
||||||
) as string;
|
) as string;
|
||||||
} else {
|
} else {
|
||||||
const senderName = getMessageSenderName(getTranslation, chat.id, messageSender);
|
const senderName = getMessageSenderName(getTranslation, chat.id, messageSender);
|
||||||
|
|||||||
@ -1,9 +1,7 @@
|
|||||||
const DEFAULT_MAX_TEXT_LENGTH = 30;
|
export default function trimText<T extends string | undefined>(text: T, length?: number) {
|
||||||
|
if (!text || !length || text.length <= length) {
|
||||||
export default function trimText(text: string | undefined, length = DEFAULT_MAX_TEXT_LENGTH) {
|
|
||||||
if (!text || text.length <= length) {
|
|
||||||
return text;
|
return text;
|
||||||
}
|
}
|
||||||
|
|
||||||
return `${text.substr(0, length)}...`;
|
return `${text.substring(0, length)}...`;
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user