Message: Allow quote message (#4067)

This commit is contained in:
Alexander Zinchuk 2023-12-12 12:34:23 +01:00
parent df84448372
commit 89df119add
28 changed files with 345 additions and 136 deletions

View File

@ -886,6 +886,7 @@ function buildReplyInfo(inputInfo: ApiInputReplyInfo, isForum?: boolean): ApiRep
replyToPeerId: inputInfo.replyToPeerId, replyToPeerId: inputInfo.replyToPeerId,
quoteText: inputInfo.quoteText, quoteText: inputInfo.quoteText,
isForumTopic: isForum && inputInfo.replyToTopId ? true : undefined, isForumTopic: isForum && inputInfo.replyToTopId ? true : undefined,
...(Boolean(inputInfo.quoteText) && { isQuote: true }),
}; };
} }

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" fill="none"><path fill="#000" fill-rule="evenodd" d="M20 2.667a2.667 2.667 0 0 0-.306 5.316c-.141.498-.329.984-.562 1.45l-.32.64a1.328 1.328 0 0 0 2.376 1.188l.32-.64a10.928 10.928 0 0 0 1.153-4.887v-.23A2.667 2.667 0 0 0 20 2.666Zm6.667 0a2.667 2.667 0 0 0-.307 5.316c-.14.498-.328.984-.561 1.45l-.32.64a1.328 1.328 0 0 0 2.376 1.188l.32-.64a10.928 10.928 0 0 0 1.153-4.887v-.23a2.667 2.667 0 0 0-2.661-2.837ZM5.333 6.672a1.328 1.328 0 1 0 0 2.656H14a1.328 1.328 0 1 0 0-2.656H5.333Zm0 8a1.328 1.328 0 1 0 0 2.656h21.334a1.328 1.328 0 0 0 0-2.656H5.333ZM4.005 24c0-.733.595-1.328 1.328-1.328h21.334a1.328 1.328 0 1 1 0 2.656H5.333A1.328 1.328 0 0 1 4.005 24Z" clip-rule="evenodd"/></svg>

After

Width:  |  Height:  |  Size: 752 B

View File

@ -85,7 +85,7 @@ import { formatMediaDuration, formatVoiceRecordDuration } from '../../util/dateF
import deleteLastCharacterOutsideSelection from '../../util/deleteLastCharacterOutsideSelection'; import deleteLastCharacterOutsideSelection from '../../util/deleteLastCharacterOutsideSelection';
import focusEditableElement from '../../util/focusEditableElement'; import focusEditableElement from '../../util/focusEditableElement';
import { MEMO_EMPTY_ARRAY } from '../../util/memo'; import { MEMO_EMPTY_ARRAY } from '../../util/memo';
import parseMessageInput from '../../util/parseMessageInput'; import parseHtmlAsFormattedText from '../../util/parseHtmlAsFormattedText';
import { insertHtmlInSelection } from '../../util/selection'; import { insertHtmlInSelection } from '../../util/selection';
import { getServerTime } from '../../util/serverTime'; import { getServerTime } from '../../util/serverTime';
import { IS_IOS, IS_VOICE_RECORDING_SUPPORTED } from '../../util/windowEnvironment'; import { IS_IOS, IS_VOICE_RECORDING_SUPPORTED } from '../../util/windowEnvironment';
@ -869,7 +869,7 @@ const Composer: FC<OwnProps & StateProps> = ({
return; return;
} }
const { text, entities } = parseMessageInput(getHtml()); const { text, entities } = parseHtmlAsFormattedText(getHtml());
if (!text && !attachmentsToSend.length) { if (!text && !attachmentsToSend.length) {
return; return;
} }
@ -931,7 +931,7 @@ const Composer: FC<OwnProps & StateProps> = ({
} }
} }
const { text, entities } = parseMessageInput(getHtml()); const { text, entities } = parseHtmlAsFormattedText(getHtml());
if (currentAttachments.length) { if (currentAttachments.length) {
sendAttachments({ sendAttachments({
@ -1398,7 +1398,7 @@ const Composer: FC<OwnProps & StateProps> = ({
showCustomEmojiPremiumNotification(); showCustomEmojiPremiumNotification();
return; return;
} }
const customEmojiMessage = parseMessageInput(buildCustomEmojiHtml(sticker)); const customEmojiMessage = parseHtmlAsFormattedText(buildCustomEmojiHtml(sticker));
text = customEmojiMessage.text; text = customEmojiMessage.text;
entities = customEmojiMessage.entities; entities = customEmojiMessage.entities;
} }

View File

@ -127,7 +127,14 @@ const CustomEmoji: FC<OwnProps> = ({
data-alt={customEmoji?.emoji} data-alt={customEmoji?.emoji}
style={style} style={style}
> >
<img className={styles.highlightCatch} src={blankImg} alt={customEmoji?.emoji} draggable={false} /> <img
className={styles.highlightCatch}
src={blankImg}
alt={customEmoji?.emoji}
data-entity-type={ApiMessageEntityTypes.CustomEmoji}
data-document-id={documentId}
draggable={false}
/>
{!customEmoji ? ( {!customEmoji ? (
<img className={styles.thumb} src={svgPlaceholder} alt="Emoji" draggable={false} /> <img className={styles.thumb} src={svgPlaceholder} alt="Emoji" draggable={false} />
) : ( ) : (

View File

@ -51,6 +51,7 @@ type OwnProps = {
customText?: string; customText?: string;
noUserColors?: boolean; noUserColors?: boolean;
isProtected?: boolean; isProtected?: boolean;
isInComposer?: boolean;
chatTranslations?: ChatTranslatedMessages; chatTranslations?: ChatTranslatedMessages;
requestedChatTranslationLanguage?: string; requestedChatTranslationLanguage?: string;
observeIntersectionForLoading?: ObserveFn; observeIntersectionForLoading?: ObserveFn;
@ -71,6 +72,7 @@ const EmbeddedMessage: FC<OwnProps> = ({
title, title,
customText, customText,
isProtected, isProtected,
isInComposer,
noUserColors, noUserColors,
chatTranslations, chatTranslations,
requestedChatTranslationLanguage, requestedChatTranslationLanguage,
@ -118,6 +120,7 @@ const EmbeddedMessage: FC<OwnProps> = ({
return renderTextWithEntities({ return renderTextWithEntities({
text: replyInfo.quoteText.text, text: replyInfo.quoteText.text,
entities: replyInfo.quoteText.entities, entities: replyInfo.quoteText.entities,
noLineBreaks: isInComposer,
}); });
} }
@ -173,7 +176,11 @@ const EmbeddedMessage: FC<OwnProps> = ({
return ( return (
<> <>
{!isChatSender && <span className="embedded-sender">{renderText(senderTitle)}</span>} {!isChatSender && (
<span className="embedded-sender">
{renderText(isInComposer ? lang('ReplyToQuote', senderTitle) : senderTitle)}
</span>
)}
{icon && <Icon name={icon} className="embedded-chat-icon" />} {icon && <Icon name={icon} className="embedded-chat-icon" />}
{icon && senderChatTitle && renderText(senderChatTitle)} {icon && senderChatTitle && renderText(senderChatTitle)}
</> </>

View File

@ -37,6 +37,7 @@ export function renderTextWithEntities({
containerId, containerId,
isSimple, isSimple,
isProtected, isProtected,
noLineBreaks,
observeIntersectionForLoading, observeIntersectionForLoading,
observeIntersectionForPlaying, observeIntersectionForPlaying,
withTranslucentThumbs, withTranslucentThumbs,
@ -54,6 +55,7 @@ export function renderTextWithEntities({
containerId?: string; containerId?: string;
isSimple?: boolean; isSimple?: boolean;
isProtected?: boolean; isProtected?: boolean;
noLineBreaks?: boolean;
observeIntersectionForLoading?: ObserveFn; observeIntersectionForLoading?: ObserveFn;
observeIntersectionForPlaying?: ObserveFn; observeIntersectionForPlaying?: ObserveFn;
withTranslucentThumbs?: boolean; withTranslucentThumbs?: boolean;
@ -64,7 +66,15 @@ export function renderTextWithEntities({
focusedQuote?: string; focusedQuote?: string;
}) { }) {
if (!entities?.length) { if (!entities?.length) {
return renderMessagePart(text, highlight, focusedQuote, emojiSize, shouldRenderAsHtml, isSimple); return renderMessagePart({
content: text,
highlight,
focusedQuote,
emojiSize,
shouldRenderAsHtml,
isSimple,
noLineBreaks,
});
} }
const result: TextPart[] = []; const result: TextPart[] = [];
@ -92,9 +102,15 @@ export function renderTextWithEntities({
deleteLineBreakAfterPre = false; deleteLineBreakAfterPre = false;
} }
if (textBefore) { if (textBefore) {
renderResult.push(...renderMessagePart( renderResult.push(...renderMessagePart({
textBefore, highlight, focusedQuote, emojiSize, shouldRenderAsHtml, isSimple, content: textBefore,
) as TextPart[]); highlight,
focusedQuote,
emojiSize,
shouldRenderAsHtml,
isSimple,
noLineBreaks,
}) as TextPart[]);
} }
} }
@ -141,8 +157,10 @@ export function renderTextWithEntities({
entityContent, entityContent,
nestedEntityContent, nestedEntityContent,
highlight, highlight,
focusedQuote,
containerId, containerId,
isSimple, isSimple,
noLineBreaks,
isProtected, isProtected,
observeIntersectionForLoading, observeIntersectionForLoading,
observeIntersectionForPlaying, observeIntersectionForPlaying,
@ -168,9 +186,15 @@ export function renderTextWithEntities({
textAfter = textAfter.substring(1); textAfter = textAfter.substring(1);
} }
if (textAfter) { if (textAfter) {
renderResult.push(...renderMessagePart( renderResult.push(...renderMessagePart({
textAfter, highlight, focusedQuote, emojiSize, shouldRenderAsHtml, isSimple, content: textAfter,
) as TextPart[]); highlight,
focusedQuote,
emojiSize,
shouldRenderAsHtml,
isSimple,
noLineBreaks,
}) as TextPart[]);
} }
} }
@ -217,19 +241,36 @@ export function getTextWithEntitiesAsHtml(formattedText?: ApiFormattedText) {
return result; return result;
} }
function renderMessagePart( function renderMessagePart({
content: TextPart | TextPart[], content,
highlight?: string, highlight,
focusedQuote?: string, focusedQuote,
emojiSize?: number, emojiSize,
shouldRenderAsHtml?: boolean, shouldRenderAsHtml,
isSimple?: boolean, isSimple,
) { noLineBreaks,
} : {
content: TextPart | TextPart[];
highlight?: string;
focusedQuote?: string;
emojiSize?: number;
shouldRenderAsHtml?: boolean;
isSimple?: boolean;
noLineBreaks?: boolean;
}) {
if (Array.isArray(content)) { if (Array.isArray(content)) {
const result: TextPart[] = []; const result: TextPart[] = [];
content.forEach((c) => { content.forEach((c) => {
result.push(...renderMessagePart(c, highlight, focusedQuote, emojiSize, shouldRenderAsHtml, isSimple)); result.push(...renderMessagePart({
content: c,
highlight,
focusedQuote,
emojiSize,
shouldRenderAsHtml,
isSimple,
noLineBreaks,
}));
}); });
return result; return result;
@ -243,7 +284,7 @@ function renderMessagePart(
const filters: TextFilter[] = [emojiFilter]; const filters: TextFilter[] = [emojiFilter];
const params: RenderTextParams = {}; const params: RenderTextParams = {};
if (!isSimple) { if (!isSimple && !noLineBreaks) {
filters.push('br'); filters.push('br');
} }
@ -330,8 +371,10 @@ function processEntity({
entityContent, entityContent,
nestedEntityContent, nestedEntityContent,
highlight, highlight,
focusedQuote,
containerId, containerId,
isSimple, isSimple,
noLineBreaks,
isProtected, isProtected,
observeIntersectionForLoading, observeIntersectionForLoading,
observeIntersectionForPlaying, observeIntersectionForPlaying,
@ -346,8 +389,10 @@ function processEntity({
entityContent: TextPart; entityContent: TextPart;
nestedEntityContent: TextPart[]; nestedEntityContent: TextPart[];
highlight?: string; highlight?: string;
focusedQuote?: string;
containerId?: string; containerId?: string;
isSimple?: boolean; isSimple?: boolean;
noLineBreaks?: boolean;
isProtected?: boolean; isProtected?: boolean;
observeIntersectionForLoading?: ObserveFn; observeIntersectionForLoading?: ObserveFn;
observeIntersectionForPlaying?: ObserveFn; observeIntersectionForPlaying?: ObserveFn;
@ -362,9 +407,14 @@ function processEntity({
const renderedContent = nestedEntityContent.length ? nestedEntityContent : entityContent; const renderedContent = nestedEntityContent.length ? nestedEntityContent : entityContent;
function renderNestedMessagePart() { function renderNestedMessagePart() {
return renderMessagePart( return renderMessagePart({
renderedContent, highlight, undefined, emojiSize, undefined, isSimple, content: renderedContent,
); highlight,
focusedQuote,
emojiSize,
isSimple,
noLineBreaks,
});
} }
if (!entityText) { if (!entityText) {
@ -402,11 +452,11 @@ function processEntity({
return <strong data-entity-type={entity.type}>{renderNestedMessagePart()}</strong>; return <strong data-entity-type={entity.type}>{renderNestedMessagePart()}</strong>;
case ApiMessageEntityTypes.Blockquote: case ApiMessageEntityTypes.Blockquote:
return ( return (
<div className="text-entity-blockquote-wrapper"> <span className="text-entity-blockquote-wrapper">
<blockquote data-entity-type={entity.type}> <blockquote data-entity-type={entity.type}>
{renderNestedMessagePart()} {renderNestedMessagePart()}
</blockquote> </blockquote>
</div> </span>
); );
case ApiMessageEntityTypes.BotCommand: case ApiMessageEntityTypes.BotCommand:
return ( return (
@ -585,10 +635,10 @@ function processEntityAsHtml(
case ApiMessageEntityTypes.CustomEmoji: case ApiMessageEntityTypes.CustomEmoji:
return buildCustomEmojiHtmlFromEntity(rawEntityText, entity); return buildCustomEmojiHtmlFromEntity(rawEntityText, entity);
case ApiMessageEntityTypes.Blockquote: case ApiMessageEntityTypes.Blockquote:
return `<span return `<blockquote
class="blockquote" class="blockquote"
data-entity-type="${ApiMessageEntityTypes.Blockquote}" data-entity-type="${ApiMessageEntityTypes.Blockquote}"
>${renderedContent}</span>`; >${renderedContent}</blockquote>`;
default: default:
return renderedContent; return renderedContent;
} }

View File

@ -23,6 +23,7 @@
} }
& .embedded-left-icon { & .embedded-left-icon {
position: relative;
flex-shrink: 0; flex-shrink: 0;
background: none !important; background: none !important;
height: 2.625rem; height: 2.625rem;
@ -63,4 +64,15 @@
width: 1.5rem; width: 1.5rem;
} }
} }
.quote-reply {
position: absolute;
right: 0.75rem;
top: 0.5rem;
font-size: 0.5rem;
@media (max-width: 600px) {
right: 0.625rem;
}
}
} }

View File

@ -34,6 +34,7 @@ import useShowTransition from '../../../hooks/useShowTransition';
import useAsyncRendering from '../../right/hooks/useAsyncRendering'; import useAsyncRendering from '../../right/hooks/useAsyncRendering';
import EmbeddedMessage from '../../common/embedded/EmbeddedMessage'; import EmbeddedMessage from '../../common/embedded/EmbeddedMessage';
import Icon from '../../common/Icon';
import Button from '../../ui/Button'; import Button from '../../ui/Button';
import Menu from '../../ui/Menu'; import Menu from '../../ui/Menu';
import MenuItem from '../../ui/MenuItem'; import MenuItem from '../../ui/MenuItem';
@ -173,13 +174,13 @@ const ComposerEmbeddedMessage: FC<OwnProps & StateProps> = ({
const leftIcon = useMemo(() => { const leftIcon = useMemo(() => {
if (isShowingReply) { if (isShowingReply) {
return 'icon-reply'; return 'reply';
} }
if (editingId) { if (editingId) {
return 'icon-edit'; return 'edit';
} }
if (isForwarding) { if (isForwarding) {
return 'icon-forward'; return 'forward';
} }
return undefined; return undefined;
@ -210,11 +211,15 @@ const ComposerEmbeddedMessage: FC<OwnProps & StateProps> = ({
<div className={className} ref={ref} onContextMenu={handleContextMenu} onClick={handleContextMenu}> <div className={className} ref={ref} onContextMenu={handleContextMenu} onClick={handleContextMenu}>
<div className={innerClassName}> <div className={innerClassName}>
<div className="embedded-left-icon"> <div className="embedded-left-icon">
<i className={buildClassName('icon', leftIcon)} /> {leftIcon && <Icon name={leftIcon} />}
{Boolean(replyInfo?.quoteText) && (
<Icon name="quote" className="quote-reply" />
)}
</div> </div>
<EmbeddedMessage <EmbeddedMessage
className="inside-input" className="inside-input"
replyInfo={replyInfo} replyInfo={replyInfo}
isInComposer
message={strippedMessage} message={strippedMessage}
sender={!noAuthors ? sender : undefined} sender={!noAuthors ? sender : undefined}
customText={customText} customText={customText}
@ -339,7 +344,7 @@ export default memo(withGlobal<OwnProps>(
sender = selectForwardedSender(global, message); sender = selectForwardedSender(global, message);
} }
if (!sender && !forwardInfo?.hiddenUserName) { if (!sender && (!forwardInfo?.hiddenUserName || Boolean(replyInfo.quoteText))) {
sender = selectSender(global, message); sender = selectSender(global, message);
} }
} else if (isForwarding) { } else if (isForwarding) {
@ -352,8 +357,8 @@ export default memo(withGlobal<OwnProps>(
if (!sender) { if (!sender) {
sender = selectPeer(global, fromChatId!); sender = selectPeer(global, fromChatId!);
} }
} else if (editingId) { } else if (editingId && message) {
sender = selectSender(global, message!); sender = selectSender(global, message);
} }
const forwardsHaveCaptions = forwardedMessages?.some((forward) => ( const forwardsHaveCaptions = forwardedMessages?.some((forward) => (

View File

@ -8,7 +8,7 @@ import type { ApiNewPoll } from '../../../api/types';
import { requestNextMutation } from '../../../lib/fasterdom/fasterdom'; import { requestNextMutation } from '../../../lib/fasterdom/fasterdom';
import captureEscKeyListener from '../../../util/captureEscKeyListener'; import captureEscKeyListener from '../../../util/captureEscKeyListener';
import parseMessageInput from '../../../util/parseMessageInput'; import parseHtmlAsFormattedText from '../../../util/parseHtmlAsFormattedText';
import useLang from '../../../hooks/useLang'; import useLang from '../../../hooks/useLang';
import useLastCallback from '../../../hooks/useLastCallback'; import useLastCallback from '../../../hooks/useLastCallback';
@ -142,7 +142,8 @@ const PollModal: FC<OwnProps> = ({
}; };
if (isQuizMode) { if (isQuizMode) {
const { text, entities } = (solution && parseMessageInput(solution.substring(0, MAX_SOLUTION_LENGTH))) || {}; const { text, entities } = (solution && parseHtmlAsFormattedText(solution.substring(0, MAX_SOLUTION_LENGTH)))
|| {};
payload.quiz = { payload.quiz = {
correctAnswers: [String(correctOption)], correctAnswers: [String(correctOption)],

View File

@ -12,7 +12,7 @@ import { ApiMessageEntityTypes } from '../../../api/types';
import { RE_LINK_TEMPLATE } from '../../../config'; import { RE_LINK_TEMPLATE } from '../../../config';
import { selectNoWebPage, selectTabState, selectTheme } from '../../../global/selectors'; import { selectNoWebPage, selectTabState, selectTheme } from '../../../global/selectors';
import buildClassName from '../../../util/buildClassName'; import buildClassName from '../../../util/buildClassName';
import parseMessageInput from '../../../util/parseMessageInput'; import parseHtmlAsFormattedText from '../../../util/parseHtmlAsFormattedText';
import { useDebouncedResolver } from '../../../hooks/useAsyncResolvers'; import { useDebouncedResolver } from '../../../hooks/useAsyncResolvers';
import useCurrentOrPrev from '../../../hooks/useCurrentOrPrev'; import useCurrentOrPrev from '../../../hooks/useCurrentOrPrev';
@ -61,7 +61,7 @@ const WebPagePreview: FC<OwnProps & StateProps> = ({
const formattedTextWithLinkRef = useRef<ApiFormattedText>(); const formattedTextWithLinkRef = useRef<ApiFormattedText>();
const detectLinkDebounced = useDebouncedResolver(() => { const detectLinkDebounced = useDebouncedResolver(() => {
const formattedText = parseMessageInput(getHtml()); const formattedText = parseHtmlAsFormattedText(getHtml());
const linkEntity = formattedText.entities?.find((entity): entity is ApiMessageEntityTextUrl => ( const linkEntity = formattedText.entities?.find((entity): entity is ApiMessageEntityTextUrl => (
entity.type === ApiMessageEntityTypes.TextUrl entity.type === ApiMessageEntityTypes.TextUrl
)); ));

View File

@ -2,7 +2,7 @@ import { ApiMessageEntityTypes } from '../../../../api/types';
import { DEBUG } from '../../../../config'; import { DEBUG } from '../../../../config';
import cleanDocsHtml from '../../../../lib/cleanDocsHtml'; import cleanDocsHtml from '../../../../lib/cleanDocsHtml';
import { ENTITY_CLASS_BY_NODE_NAME } from '../../../../util/parseMessageInput'; import { ENTITY_CLASS_BY_NODE_NAME } from '../../../../util/parseHtmlAsFormattedText';
const STYLE_TAG_REGEX = /<style>(.*?)<\/style>/gs; const STYLE_TAG_REGEX = /<style>(.*?)<\/style>/gs;

View File

@ -1,4 +1,4 @@
import { fixImageContent } from '../../../../util/parseMessageInput'; import { fixImageContent } from '../../../../util/parseHtmlAsFormattedText';
const div = document.createElement('div'); const div = document.createElement('div');

View File

@ -7,7 +7,7 @@ import {
EDITABLE_INPUT_ID, EDITABLE_INPUT_MODAL_ID, EDITABLE_STORY_INPUT_ID, EDITABLE_INPUT_ID, EDITABLE_INPUT_MODAL_ID, EDITABLE_STORY_INPUT_ID,
} from '../../../../config'; } from '../../../../config';
import { containsCustomEmoji, stripCustomEmoji } from '../../../../global/helpers/symbols'; import { containsCustomEmoji, stripCustomEmoji } from '../../../../global/helpers/symbols';
import parseMessageInput from '../../../../util/parseMessageInput'; import parseHtmlAsFormattedText from '../../../../util/parseHtmlAsFormattedText';
import buildAttachment from '../helpers/buildAttachment'; import buildAttachment from '../helpers/buildAttachment';
import { preparePastedHtml } from '../helpers/cleanHtml'; import { preparePastedHtml } from '../helpers/cleanHtml';
import getFilesFromDataTransferItems from '../helpers/getFilesFromDataTransferItems'; import getFilesFromDataTransferItems from '../helpers/getFilesFromDataTransferItems';
@ -45,7 +45,7 @@ const useClipboardPaste = (
const pastedText = e.clipboardData.getData('text').substring(0, MAX_MESSAGE_LENGTH); const pastedText = e.clipboardData.getData('text').substring(0, MAX_MESSAGE_LENGTH);
const html = e.clipboardData.getData('text/html'); const html = e.clipboardData.getData('text/html');
let pastedFormattedText = html ? parseMessageInput( let pastedFormattedText = html ? parseHtmlAsFormattedText(
preparePastedHtml(html), undefined, true, preparePastedHtml(html), undefined, true,
) : undefined; ) : undefined;

View File

@ -11,7 +11,7 @@ import {
requestMeasure, requestNextMutation, requestMeasure, requestNextMutation,
} from '../../../../lib/fasterdom/fasterdom'; } from '../../../../lib/fasterdom/fasterdom';
import focusEditableElement from '../../../../util/focusEditableElement'; import focusEditableElement from '../../../../util/focusEditableElement';
import parseMessageInput from '../../../../util/parseMessageInput'; import parseHtmlAsFormattedText from '../../../../util/parseHtmlAsFormattedText';
import { IS_TOUCH_ENV } from '../../../../util/windowEnvironment'; import { IS_TOUCH_ENV } from '../../../../util/windowEnvironment';
import { getTextWithEntitiesAsHtml } from '../../../common/helpers/renderTextWithEntities'; import { getTextWithEntitiesAsHtml } from '../../../common/helpers/renderTextWithEntities';
@ -77,7 +77,7 @@ const useDraft = ({
saveDraft({ saveDraft({
chatId: prevState.chatId ?? chatId, chatId: prevState.chatId ?? chatId,
threadId: prevState.threadId ?? threadId, threadId: prevState.threadId ?? threadId,
text: parseMessageInput(html), text: parseHtmlAsFormattedText(html),
}); });
} else { } else {
clearDraft({ clearDraft({

View File

@ -10,7 +10,7 @@ import { EDITABLE_INPUT_CSS_SELECTOR } from '../../../../config';
import { requestMeasure, requestNextMutation } from '../../../../lib/fasterdom/fasterdom'; import { requestMeasure, requestNextMutation } from '../../../../lib/fasterdom/fasterdom';
import { hasMessageMedia } from '../../../../global/helpers'; import { hasMessageMedia } from '../../../../global/helpers';
import focusEditableElement from '../../../../util/focusEditableElement'; import focusEditableElement from '../../../../util/focusEditableElement';
import parseMessageInput from '../../../../util/parseMessageInput'; import parseHtmlAsFormattedText from '../../../../util/parseHtmlAsFormattedText';
import { getTextWithEntitiesAsHtml } from '../../../common/helpers/renderTextWithEntities'; import { getTextWithEntitiesAsHtml } from '../../../common/helpers/renderTextWithEntities';
import { useDebouncedResolver } from '../../../../hooks/useAsyncResolvers'; import { useDebouncedResolver } from '../../../../hooks/useAsyncResolvers';
@ -87,7 +87,7 @@ const useEditing = (
useEffect(() => { useEffect(() => {
if (!editedMessage) return undefined; if (!editedMessage) return undefined;
return () => { return () => {
const edited = parseMessageInput(getHtml()); const edited = parseHtmlAsFormattedText(getHtml());
const update = edited.text.length ? edited : undefined; const update = edited.text.length ? edited : undefined;
setEditingDraft({ setEditingDraft({
@ -99,7 +99,7 @@ const useEditing = (
const detectLinkDebounced = useDebouncedResolver(() => { const detectLinkDebounced = useDebouncedResolver(() => {
if (!editedMessage) return false; if (!editedMessage) return false;
const edited = parseMessageInput(getHtml()); const edited = parseHtmlAsFormattedText(getHtml());
return !('webPage' in editedMessage.content) return !('webPage' in editedMessage.content)
&& editedMessage.content.text?.entities?.some((entity) => URL_ENTITIES.has(entity.type)) && editedMessage.content.text?.entities?.some((entity) => URL_ENTITIES.has(entity.type))
&& !(edited.entities?.some((entity) => URL_ENTITIES.has(entity.type))); && !(edited.entities?.some((entity) => URL_ENTITIES.has(entity.type)));
@ -144,7 +144,7 @@ const useEditing = (
}); });
const handleEditComplete = useLastCallback(() => { const handleEditComplete = useLastCallback(() => {
const { text, entities } = parseMessageInput(getHtml()); const { text, entities } = parseHtmlAsFormattedText(getHtml());
if (!editedMessage) { if (!editedMessage) {
return; return;
@ -167,7 +167,7 @@ const useEditing = (
const handleBlur = useLastCallback(() => { const handleBlur = useLastCallback(() => {
if (!editedMessage) return; if (!editedMessage) return;
const edited = parseMessageInput(getHtml()); const edited = parseHtmlAsFormattedText(getHtml());
const update = edited.text.length ? edited : undefined; const update = edited.text.length ? edited : undefined;
setEditingDraft({ setEditingDraft({

View File

@ -43,6 +43,8 @@ import {
} from '../../../global/selectors'; } from '../../../global/selectors';
import buildClassName from '../../../util/buildClassName'; import buildClassName from '../../../util/buildClassName';
import { copyTextToClipboard } from '../../../util/clipboard'; import { copyTextToClipboard } from '../../../util/clipboard';
import { getSelectionAsFormattedText } from './helpers/getSelectionAsFormattedText';
import { isSelectionRangeInsideMessage } from './helpers/isSelectionRangeInsideMessage';
import useFlag from '../../../hooks/useFlag'; import useFlag from '../../../hooks/useFlag';
import useLang from '../../../hooks/useLang'; import useLang from '../../../hooks/useLang';
@ -93,6 +95,7 @@ type StateProps = {
canCopy?: boolean; canCopy?: boolean;
canTranslate?: boolean; canTranslate?: boolean;
canShowOriginal?: boolean; canShowOriginal?: boolean;
isMessageTranslated?: boolean;
canSelectLanguage?: boolean; canSelectLanguage?: boolean;
isPrivate?: boolean; isPrivate?: boolean;
isCurrentUserPremium?: boolean; isCurrentUserPremium?: boolean;
@ -113,6 +116,8 @@ type StateProps = {
messageLink?: string; messageLink?: string;
}; };
const selection = window.getSelection();
const ContextMenuContainer: FC<OwnProps & StateProps> = ({ const ContextMenuContainer: FC<OwnProps & StateProps> = ({
availableReactions, availableReactions,
topReactions, topReactions,
@ -158,6 +163,7 @@ const ContextMenuContainer: FC<OwnProps & StateProps> = ({
canShowSeenBy, canShowSeenBy,
canScheduleUntilOnline, canScheduleUntilOnline,
canTranslate, canTranslate,
isMessageTranslated,
canShowOriginal, canShowOriginal,
canSelectLanguage, canSelectLanguage,
isReactionPickerOpen, isReactionPickerOpen,
@ -202,7 +208,7 @@ const ContextMenuContainer: FC<OwnProps & StateProps> = ({
const [isReportModalOpen, setIsReportModalOpen] = useState(false); const [isReportModalOpen, setIsReportModalOpen] = useState(false);
const [isPinModalOpen, setIsPinModalOpen] = useState(false); const [isPinModalOpen, setIsPinModalOpen] = useState(false);
const [isClosePollDialogOpen, openClosePollDialog, closeClosePollDialog] = useFlag(); const [isClosePollDialogOpen, openClosePollDialog, closeClosePollDialog] = useFlag();
const [canQuoteSelection, setCanQuoteSelection] = useState(false);
const [requestCalendar, calendar] = useSchedule(canScheduleUntilOnline, onClose, message.date); const [requestCalendar, calendar] = useSchedule(canScheduleUntilOnline, onClose, message.date);
// `undefined` indicates that emoji are present and loading // `undefined` indicates that emoji are present and loading
@ -265,6 +271,35 @@ const ContextMenuContainer: FC<OwnProps & StateProps> = ({
return activeDownloads?.[message.isScheduled ? 'scheduledIds' : 'ids']?.includes(message.id); return activeDownloads?.[message.isScheduled ? 'scheduledIds' : 'ids']?.includes(message.id);
}, [activeDownloads, album, message]); }, [activeDownloads, album, message]);
const selectionRange = canReply && selection?.rangeCount ? selection.getRangeAt(0) : undefined;
useEffect(() => {
if (isMessageTranslated) {
setCanQuoteSelection(false);
return;
}
const isMessageTextSelected = selectionRange
&& !selectionRange.collapsed
&& Boolean(message.content.text?.text)
&& isSelectionRangeInsideMessage(selectionRange);
if (!isMessageTextSelected) {
setCanQuoteSelection(false);
return;
}
const selectionText = getSelectionAsFormattedText(selectionRange);
setCanQuoteSelection(
selectionText.text.trim().length > 0
&& message.content.text!.text!.includes(selectionText.text),
);
}, [
selectionRange, selectionRange?.collapsed, selectionRange?.startOffset, selectionRange?.endOffset,
isMessageTranslated, message.content.text,
]);
const handleDelete = useLastCallback(() => { const handleDelete = useLastCallback(() => {
setIsMenuOpen(false); setIsMenuOpen(false);
setIsDeleteModalOpen(true); setIsDeleteModalOpen(true);
@ -296,7 +331,10 @@ const ContextMenuContainer: FC<OwnProps & StateProps> = ({
}); });
const handleReply = useLastCallback(() => { const handleReply = useLastCallback(() => {
updateDraftReplyInfo({ replyToMsgId: message.id }); updateDraftReplyInfo({
replyToMsgId: message.id,
quoteText: canQuoteSelection && selectionRange ? getSelectionAsFormattedText(selectionRange) : undefined,
});
closeMenu(); closeMenu();
}); });
@ -493,6 +531,7 @@ const ContextMenuContainer: FC<OwnProps & StateProps> = ({
canSendNow={canSendNow} canSendNow={canSendNow}
canReschedule={canReschedule} canReschedule={canReschedule}
canReply={canReply} canReply={canReply}
canQuote={canQuoteSelection}
canDelete={canDelete} canDelete={canDelete}
canReport={canReport} canReport={canReport}
canPin={canPin} canPin={canPin}
@ -679,6 +718,7 @@ export default memo(withGlobal<OwnProps>(
canTranslate, canTranslate,
canShowOriginal: hasTranslation && !isChatTranslated, canShowOriginal: hasTranslation && !isChatTranslated,
canSelectLanguage: hasTranslation && !isChatTranslated, canSelectLanguage: hasTranslation && !isChatTranslated,
isMessageTranslated: hasTranslation,
canPlayAnimatedEmojis: selectCanPlayAnimatedEmojis(global), canPlayAnimatedEmojis: selectCanPlayAnimatedEmojis(global),
isReactionPickerOpen: selectIsReactionPickerOpen(global), isReactionPickerOpen: selectIsReactionPickerOpen(global),
messageLink, messageLink,

View File

@ -95,6 +95,7 @@ import {
import { isAnimatingScroll } from '../../../util/animateScroll'; import { isAnimatingScroll } from '../../../util/animateScroll';
import buildClassName from '../../../util/buildClassName'; import buildClassName from '../../../util/buildClassName';
import { isElementInViewport } from '../../../util/isElementInViewport'; import { isElementInViewport } from '../../../util/isElementInViewport';
import stopEvent from '../../../util/stopEvent';
import { IS_ANDROID, IS_ELECTRON, IS_TRANSLATION_SUPPORTED } from '../../../util/windowEnvironment'; import { IS_ANDROID, IS_ELECTRON, IS_TRANSLATION_SUPPORTED } from '../../../util/windowEnvironment';
import { import {
calculateDimensionsForMessageMedia, calculateDimensionsForMessageMedia,
@ -522,6 +523,7 @@ const Message: FC<OwnProps & StateProps> = ({
const avatarPeer = shouldPreferOriginSender ? originSender : messageSender; const avatarPeer = shouldPreferOriginSender ? originSender : messageSender;
const messageColorPeer = originSender || sender; const messageColorPeer = originSender || sender;
const senderPeer = (forwardInfo || message.content.storyData) ? originSender : messageSender; const senderPeer = (forwardInfo || message.content.storyData) ? originSender : messageSender;
const hasText = hasMessageText(message);
const { const {
handleMouseDown, handleMouseDown,
@ -605,7 +607,7 @@ const Message: FC<OwnProps & StateProps> = ({
const containerClassName = buildClassName( const containerClassName = buildClassName(
'Message message-list-item', 'Message message-list-item',
isFirstInGroup && 'first-in-group', isFirstInGroup && 'first-in-group',
isProtected ? 'is-protected' : 'allow-selection', isProtected && !hasText ? 'is-protected' : 'allow-selection',
isLastInGroup && 'last-in-group', isLastInGroup && 'last-in-group',
isFirstInDocumentGroup && 'first-in-document-group', isFirstInDocumentGroup && 'first-in-document-group',
isLastInDocumentGroup && 'last-in-document-group', isLastInDocumentGroup && 'last-in-document-group',
@ -686,7 +688,6 @@ const Message: FC<OwnProps & StateProps> = ({
}); });
const withAppendix = contentClassName.includes('has-appendix'); const withAppendix = contentClassName.includes('has-appendix');
const hasText = hasMessageText(message);
const emojiSize = getCustomEmojiSize(message.emojiOnlyCount); const emojiSize = getCustomEmojiSize(message.emojiOnlyCount);
let metaPosition!: MetaPosition; let metaPosition!: MetaPosition;
@ -1319,6 +1320,7 @@ const Message: FC<OwnProps & StateProps> = ({
id={getMessageHtmlId(message.id)} id={getMessageHtmlId(message.id)}
className={containerClassName} className={containerClassName}
data-message-id={messageId} data-message-id={messageId}
onCopy={isProtected ? stopEvent : undefined}
onMouseDown={handleMouseDown} onMouseDown={handleMouseDown}
onClick={handleClick} onClick={handleClick}
onContextMenu={handleContextMenu} onContextMenu={handleContextMenu}

View File

@ -53,6 +53,7 @@ type OwnProps = {
maxUniqueReactions?: number; maxUniqueReactions?: number;
canReschedule?: boolean; canReschedule?: boolean;
canReply?: boolean; canReply?: boolean;
canQuote?: boolean;
repliesThreadInfo?: ApiThreadInfo; repliesThreadInfo?: ApiThreadInfo;
canPin?: boolean; canPin?: boolean;
canUnpin?: boolean; canUnpin?: boolean;
@ -140,6 +141,7 @@ const MessageContextMenu: FC<OwnProps> = ({
canReschedule, canReschedule,
canBuyPremium, canBuyPremium,
canReply, canReply,
canQuote,
canEdit, canEdit,
noReplies, noReplies,
canPin, canPin,
@ -361,7 +363,11 @@ const MessageContextMenu: FC<OwnProps> = ({
{canReschedule && ( {canReschedule && (
<MenuItem icon="schedule" onClick={onReschedule}>{lang('MessageScheduleEditTime')}</MenuItem> <MenuItem icon="schedule" onClick={onReschedule}>{lang('MessageScheduleEditTime')}</MenuItem>
)} )}
{canReply && <MenuItem icon="reply" onClick={onReply}>{lang('Reply')}</MenuItem>} {canReply && (
<MenuItem icon="reply" onClick={onReply}>
{lang(canQuote ? 'lng_context_quote_and_reply' : 'Reply')}
</MenuItem>
)}
{!noReplies && Boolean(repliesThreadInfo?.messagesCount) && ( {!noReplies && Boolean(repliesThreadInfo?.messagesCount) && (
<MenuItem icon="replies" onClick={onOpenThread}> <MenuItem icon="replies" onClick={onOpenThread}>
{lang('Conversation.ContextViewReplies', repliesThreadInfo!.messagesCount, 'i')} {lang('Conversation.ContextViewReplies', repliesThreadInfo!.messagesCount, 'i')}

View File

@ -14,7 +14,7 @@ import {
selectPeerStory, selectTabState, selectPeerStory, selectTabState,
} from '../../../global/selectors'; } from '../../../global/selectors';
import buildClassName from '../../../util/buildClassName'; import buildClassName from '../../../util/buildClassName';
import parseMessageInput from '../../../util/parseMessageInput'; import parseHtmlAsFormattedText from '../../../util/parseHtmlAsFormattedText';
import { REM } from '../../common/helpers/mediaDimensions'; import { REM } from '../../common/helpers/mediaDimensions';
import { buildCustomEmojiHtml } from '../composer/helpers/customEmoji'; import { buildCustomEmojiHtml } from '../composer/helpers/customEmoji';
@ -162,7 +162,7 @@ const ReactionPicker: FC<OwnProps & StateProps> = ({
if ('emoticon' in item) { if ('emoticon' in item) {
text = item.emoticon; text = item.emoticon;
} else { } else {
const customEmojiMessage = parseMessageInput(buildCustomEmojiHtml(sticker!)); const customEmojiMessage = parseHtmlAsFormattedText(buildCustomEmojiHtml(sticker!));
text = customEmojiMessage.text; text = customEmojiMessage.text;
entities = customEmojiMessage.entities; entities = customEmojiMessage.entities;
} }

View File

@ -953,6 +953,12 @@
font-size: 0.875rem; font-size: 0.875rem;
} }
// Remove extra bottom padding from `blockquote`
.text-entity-blockquote-wrapper {
display: inline-block;
width: 100%
}
blockquote, .blockquote { blockquote, .blockquote {
display: inline-block; display: inline-block;
position: relative; position: relative;

View File

@ -0,0 +1,57 @@
import type { ApiFormattedText } from '../../../../api/types';
import { ApiMessageEntityTypes } from '../../../../api/types';
import parseHtmlAsFormattedText from '../../../../util/parseHtmlAsFormattedText';
const div = document.createElement('div');
const ALLOWED_QUOTE_ENTITIES = new Set([
ApiMessageEntityTypes.Bold,
ApiMessageEntityTypes.Italic,
ApiMessageEntityTypes.Underline,
ApiMessageEntityTypes.Strike,
ApiMessageEntityTypes.Spoiler,
ApiMessageEntityTypes.CustomEmoji,
]);
export function getSelectionAsFormattedText(range: Range) {
const html = getSelectionAsHtml(range);
const formattedText = parseHtmlAsFormattedText(html, false, true);
return stripEntitiesForQuote(formattedText);
}
function getSelectionAsHtml(range: Range) {
const clonedSelection = range.cloneContents();
div.appendChild(clonedSelection);
const html = wrapHtmlWithMarkupTags(range, div.innerHTML);
div.innerHTML = '';
return html
.replace(/<br\s*\/?>/gi, '\n')
.replace(/&nbsp;/gi, ' ') // Convert nbsp's to spaces
.replace(/\u00a0/gi, ' ');
}
function stripEntitiesForQuote(text: ApiFormattedText): ApiFormattedText {
if (!text.entities) return text;
const entities = text.entities.filter((entity) => ALLOWED_QUOTE_ENTITIES.has(entity.type as ApiMessageEntityTypes));
return { ...text, entities: entities.length ? entities : undefined };
}
function wrapHtmlWithMarkupTags(range: Range, html: string) {
const container = range.commonAncestorContainer;
if (container.nodeType === Node.ELEMENT_NODE && (container as Element).classList.contains('text-content')) {
return html;
}
let currentElement = range.commonAncestorContainer.parentElement;
while (currentElement && !currentElement.classList.contains('text-content')) {
const tag = currentElement.tagName.toLowerCase();
const entityType = currentElement.dataset.entityType;
html = `<${tag} ${entityType ? `data-entity-type="${entityType}"` : ''}>${html}</${tag}>`;
currentElement = currentElement.parentElement;
}
return html;
}

View File

@ -0,0 +1,9 @@
export function isSelectionRangeInsideMessage(range: Range) {
const ancestor = range.commonAncestorContainer;
const el = ancestor.nodeType === Node.TEXT_NODE
? ancestor.parentNode! as Element
: ancestor as Element;
return Boolean(el.closest('.message-content-wrapper .text-content'))
&& !(Boolean(el.closest('.EmbeddedMessage')) || Boolean(el.closest('.WebPage')));
}

View File

@ -16,7 +16,7 @@ import { copyHtmlToClipboard } from '../../../util/clipboard';
import { getCurrentTabId } from '../../../util/establishMultitabRole'; import { getCurrentTabId } from '../../../util/establishMultitabRole';
import { compact, findLast } from '../../../util/iteratees'; import { compact, findLast } from '../../../util/iteratees';
import * as langProvider from '../../../util/langProvider'; import * as langProvider from '../../../util/langProvider';
import parseMessageInput from '../../../util/parseMessageInput'; import parseHtmlAsFormattedText from '../../../util/parseHtmlAsFormattedText';
import { getServerTime } from '../../../util/serverTime'; import { getServerTime } from '../../../util/serverTime';
import { IS_TOUCH_ENV } from '../../../util/windowEnvironment'; import { IS_TOUCH_ENV } from '../../../util/windowEnvironment';
import versionNotification from '../../../versionNotification.txt'; import versionNotification from '../../../versionNotification.txt';
@ -689,7 +689,7 @@ addActionHandler('checkVersionNotification', (global, actions): ActionReturnType
chatId: SERVICE_NOTIFICATIONS_USER_ID, chatId: SERVICE_NOTIFICATIONS_USER_ID,
date: getServerTime(), date: getServerTime(),
content: { content: {
text: parseMessageInput(versionNotification, true), text: parseHtmlAsFormattedText(versionNotification, true),
}, },
isOutgoing: false, isOutgoing: false,
}; };

View File

@ -175,80 +175,81 @@ $icons-map: (
"premium": "\f190", "premium": "\f190",
"previous": "\f191", "previous": "\f191",
"privacy-policy": "\f192", "privacy-policy": "\f192",
"quote": "\f193", "quote-text": "\f193",
"readchats": "\f194", "quote": "\f194",
"recent": "\f195", "readchats": "\f195",
"reload": "\f196", "recent": "\f196",
"remove": "\f197", "reload": "\f197",
"reopen-topic": "\f198", "remove": "\f198",
"replace": "\f199", "reopen-topic": "\f199",
"replies": "\f19a", "replace": "\f19a",
"reply-filled": "\f19b", "replies": "\f19b",
"reply": "\f19c", "reply-filled": "\f19c",
"revote": "\f19d", "reply": "\f19d",
"save-story": "\f19e", "revote": "\f19e",
"saved-messages": "\f19f", "save-story": "\f19f",
"schedule": "\f1a0", "saved-messages": "\f1a0",
"search": "\f1a1", "schedule": "\f1a1",
"select": "\f1a2", "search": "\f1a2",
"send-outline": "\f1a3", "select": "\f1a3",
"send": "\f1a4", "send-outline": "\f1a4",
"settings-filled": "\f1a5", "send": "\f1a5",
"settings": "\f1a6", "settings-filled": "\f1a6",
"share-filled": "\f1a7", "settings": "\f1a7",
"share-screen-outlined": "\f1a8", "share-filled": "\f1a8",
"share-screen-stop": "\f1a9", "share-screen-outlined": "\f1a9",
"share-screen": "\f1aa", "share-screen-stop": "\f1aa",
"sidebar": "\f1ab", "share-screen": "\f1ab",
"skip-next": "\f1ac", "sidebar": "\f1ac",
"skip-previous": "\f1ad", "skip-next": "\f1ad",
"smallscreen": "\f1ae", "skip-previous": "\f1ae",
"smile": "\f1af", "smallscreen": "\f1af",
"sort": "\f1b0", "smile": "\f1b0",
"speaker-muted-story": "\f1b1", "sort": "\f1b1",
"speaker-outline": "\f1b2", "speaker-muted-story": "\f1b2",
"speaker-story": "\f1b3", "speaker-outline": "\f1b3",
"speaker": "\f1b4", "speaker-story": "\f1b4",
"spoiler-disable": "\f1b5", "speaker": "\f1b5",
"spoiler": "\f1b6", "spoiler-disable": "\f1b6",
"sport": "\f1b7", "spoiler": "\f1b7",
"stats": "\f1b8", "sport": "\f1b8",
"stealth-future": "\f1b9", "stats": "\f1b9",
"stealth-past": "\f1ba", "stealth-future": "\f1ba",
"stickers": "\f1bb", "stealth-past": "\f1bb",
"stop-raising-hand": "\f1bc", "stickers": "\f1bc",
"stop": "\f1bd", "stop-raising-hand": "\f1bd",
"story-caption": "\f1be", "stop": "\f1be",
"story-expired": "\f1bf", "story-caption": "\f1bf",
"story-priority": "\f1c0", "story-expired": "\f1c0",
"story-reply": "\f1c1", "story-priority": "\f1c1",
"strikethrough": "\f1c2", "story-reply": "\f1c2",
"timer": "\f1c3", "strikethrough": "\f1c3",
"transcribe": "\f1c4", "timer": "\f1c4",
"truck": "\f1c5", "transcribe": "\f1c5",
"unarchive": "\f1c6", "truck": "\f1c6",
"underlined": "\f1c7", "unarchive": "\f1c7",
"unlock-badge": "\f1c8", "underlined": "\f1c8",
"unlock": "\f1c9", "unlock-badge": "\f1c9",
"unmute": "\f1ca", "unlock": "\f1ca",
"unpin": "\f1cb", "unmute": "\f1cb",
"unread": "\f1cc", "unpin": "\f1cc",
"up": "\f1cd", "unread": "\f1cd",
"user-filled": "\f1ce", "up": "\f1ce",
"user-online": "\f1cf", "user-filled": "\f1cf",
"user": "\f1d0", "user-online": "\f1d0",
"video-outlined": "\f1d1", "user": "\f1d1",
"video-stop": "\f1d2", "video-outlined": "\f1d2",
"video": "\f1d3", "video-stop": "\f1d3",
"voice-chat": "\f1d4", "video": "\f1d4",
"volume-1": "\f1d5", "voice-chat": "\f1d5",
"volume-2": "\f1d6", "volume-1": "\f1d6",
"volume-3": "\f1d7", "volume-2": "\f1d7",
"web": "\f1d8", "volume-3": "\f1d8",
"webapp": "\f1d9", "web": "\f1d9",
"word-wrap": "\f1da", "webapp": "\f1da",
"zoom-in": "\f1db", "word-wrap": "\f1db",
"zoom-out": "\f1dc", "zoom-in": "\f1dc",
"zoom-out": "\f1dd",
); );
.icon-active-sessions::before { .icon-active-sessions::before {
@ -689,6 +690,9 @@ $icons-map: (
.icon-privacy-policy::before { .icon-privacy-policy::before {
content: map.get($icons-map, "privacy-policy"); content: map.get($icons-map, "privacy-policy");
} }
.icon-quote-text::before {
content: map.get($icons-map, "quote-text");
}
.icon-quote::before { .icon-quote::before {
content: map.get($icons-map, "quote"); content: map.get($icons-map, "quote");
} }

Binary file not shown.

Binary file not shown.

View File

@ -145,6 +145,7 @@ export type FontIconName =
| 'premium' | 'premium'
| 'previous' | 'previous'
| 'privacy-policy' | 'privacy-policy'
| 'quote-text'
| 'quote' | 'quote'
| 'readchats' | 'readchats'
| 'recent' | 'recent'

View File

@ -21,7 +21,7 @@ export const ENTITY_CLASS_BY_NODE_NAME: Record<string, ApiMessageEntityTypes> =
const MAX_TAG_DEEPNESS = 3; const MAX_TAG_DEEPNESS = 3;
export default function parseMessageInput( export default function parseHtmlAsFormattedText(
html: string, withMarkdownLinks = false, skipMarkdown = false, html: string, withMarkdownLinks = false, skipMarkdown = false,
): ApiFormattedText { ): ApiFormattedText {
const fragment = document.createElement('div'); const fragment = document.createElement('div');