import { MouseEvent } from 'react'; import React from '../../../lib/teact/teact'; import { getDispatch } from '../../../lib/teact/teactn'; import { ApiMessageEntity, ApiMessageEntityTypes, ApiMessage } from '../../../api/types'; 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 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) { 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 { entity: ApiMessageEntity; organizedIndexes: Set; nestedEntities: IOrganizedEntity[]; } function organizeEntity( entity: ApiMessageEntity, index: number, entities: ApiMessageEntity[], organizedEntityIndexes: Set, ): 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(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 = 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( text: string, entities?: ApiMessageEntity[], highlight?: string, shouldRenderHqEmoji?: boolean, shouldRenderAsHtml?: boolean, messageId?: number, isSimple?: boolean, ) { if (!entities || !entities.length) { return renderMessagePart(text, highlight, shouldRenderHqEmoji, shouldRenderAsHtml, isSimple); } const result: TextPart[] = []; let deleteLineBreakAfterPre = false; const organizedEntities = organizeEntities(entities); // Recursive function to render regular and nested entities function renderEntity( textPartStart: number, textPartEnd: number, organizedEntity: IOrganizedEntity, isLastEntity: boolean, ) { const renderResult: TextPart[] = []; const { entity, nestedEntities } = organizedEntity; const { offset, length, type } = entity; // Render text before the entity let textBefore = text.substring(textPartStart, offset); const textBeforeLength = textBefore.length; if (textBefore) { if (deleteLineBreakAfterPre && textBefore.length > 0 && textBefore[0] === '\n') { textBefore = textBefore.substr(1); deleteLineBreakAfterPre = false; } if (textBefore) { renderResult.push(...renderMessagePart( textBefore, highlight, shouldRenderHqEmoji, shouldRenderAsHtml, isSimple, ) as TextPart[]); } } const entityStartIndex = textPartStart + textBeforeLength; const entityEndIndex = entityStartIndex + length; let entityContent: TextPart = text.substring(offset, offset + length); const nestedEntityContent: TextPart[] = []; if (deleteLineBreakAfterPre && entityContent.length > 0 && entityContent[0] === '\n') { entityContent = entityContent.substr(1); deleteLineBreakAfterPre = false; } if (type === ApiMessageEntityTypes.Pre) { deleteLineBreakAfterPre = true; } // Render nested entities, if any if (nestedEntities.length) { let nestedIndex = entityStartIndex; nestedEntities.forEach((nestedEntity, nestedEntityIndex) => { const { renderResult: nestedResult, entityEndIndex: nestedEntityEndIndex, } = renderEntity( nestedIndex, entityEndIndex, nestedEntity, nestedEntityIndex === nestedEntities.length - 1, ); nestedEntityContent.push(...nestedResult); nestedIndex = nestedEntityEndIndex; }); } // Render the entity itself const newEntity = shouldRenderAsHtml ? processEntityAsHtml(entity, entityContent, nestedEntityContent) : processEntity(entity, entityContent, nestedEntityContent, highlight, messageId, isSimple); if (Array.isArray(newEntity)) { renderResult.push(...newEntity); } else { renderResult.push(newEntity); } // Render text after the entity, if it is the last entity in the text, // or the last nested entity inside of another entity if (isLastEntity && entityEndIndex < textPartEnd) { let textAfter = text.substring(entityEndIndex, textPartEnd); if (deleteLineBreakAfterPre && textAfter.length > 0 && textAfter[0] === '\n') { textAfter = textAfter.substring(1); } if (textAfter) { renderResult.push(...renderMessagePart( textAfter, highlight, shouldRenderHqEmoji, shouldRenderAsHtml, isSimple, ) as TextPart[]); } } return { renderResult, entityEndIndex, }; } // Process highest-level entities let index = 0; organizedEntities.forEach((entity, arrayIndex) => { const { renderResult, entityEndIndex } = renderEntity( index, text.length, entity, arrayIndex === organizedEntities.length - 1, ); result.push(...renderResult); index = entityEndIndex; }); return result; } 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 renderNestedMessagePart(); } if (isSimple) { const text = renderNestedMessagePart(); if (entity.type === ApiMessageEntityTypes.Spoiler) { return {text}; } return text; } switch (entity.type) { case ApiMessageEntityTypes.Bold: return {renderNestedMessagePart()}; case ApiMessageEntityTypes.Blockquote: return
{renderNestedMessagePart()}
; case ApiMessageEntityTypes.BotCommand: return ( {renderNestedMessagePart()} ); case ApiMessageEntityTypes.Hashtag: return ( {renderNestedMessagePart()} ); case ApiMessageEntityTypes.Cashtag: return ( {renderNestedMessagePart()} ); case ApiMessageEntityTypes.Code: return {renderNestedMessagePart()}; case ApiMessageEntityTypes.Email: return ( {renderNestedMessagePart()} ); case ApiMessageEntityTypes.Italic: return {renderNestedMessagePart()}; case ApiMessageEntityTypes.MentionName: return ( {renderNestedMessagePart()} ); case ApiMessageEntityTypes.Mention: return ( {renderNestedMessagePart()} ); case ApiMessageEntityTypes.Phone: return ( {renderNestedMessagePart()} ); case ApiMessageEntityTypes.Pre: return
{renderNestedMessagePart()}
; case ApiMessageEntityTypes.Strike: return {renderNestedMessagePart()}; case ApiMessageEntityTypes.TextUrl: case ApiMessageEntityTypes.Url: return ( {renderNestedMessagePart()} ); case ApiMessageEntityTypes.Underline: return {renderNestedMessagePart()}; case ApiMessageEntityTypes.Spoiler: return {renderNestedMessagePart()}; default: return renderNestedMessagePart(); } } 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) { getDispatch().sendBotCommand({ command: e.currentTarget.innerText }); } function handleHashtagClick(e: MouseEvent) { getDispatch().setLocalTextSearchQuery({ query: e.currentTarget.innerText }); getDispatch().searchTextMessagesLocal(); } function processEntityAsHtml( entity: ApiMessageEntity, entityContent: TextPart, nestedEntityContent: TextPart[], ) { const rawEntityText = typeof entityContent === 'string' && entityContent; const renderedContent = nestedEntityContent.length ? nestedEntityContent.join('') : renderText(entityContent, ['escape_html', 'emoji_html', 'br_html']).join(''); if (!rawEntityText) { return renderedContent; } switch (entity.type) { case ApiMessageEntityTypes.Bold: return `${renderedContent}`; case ApiMessageEntityTypes.Italic: return `${renderedContent}`; case ApiMessageEntityTypes.Underline: return `${renderedContent}`; case ApiMessageEntityTypes.Code: return `${renderedContent}`; case ApiMessageEntityTypes.Pre: return `\`\`\`
${renderedContent}
\`\`\``; case ApiMessageEntityTypes.Strike: return `${renderedContent}`; case ApiMessageEntityTypes.MentionName: return `${renderedContent}`; case ApiMessageEntityTypes.Url: case ApiMessageEntityTypes.TextUrl: return `${renderedContent}`; case ApiMessageEntityTypes.Spoiler: return `||${renderedContent}||`; default: return renderedContent; } }