Message: Support quote highlight with entities (#5842)

This commit is contained in:
zubiden 2025-04-23 18:59:11 +02:00 committed by Alexander Zinchuk
parent 185a42be5d
commit 58e79daddb
15 changed files with 149 additions and 45 deletions

View File

@ -286,6 +286,7 @@ export function buildMessageDraft(draft: GramJs.TypeDraftMessage): ApiDraft | un
replyToTopId: replyTo.topMsgId, replyToTopId: replyTo.topMsgId,
replyToPeerId: replyTo.replyToPeerId && getApiChatIdFromMtpPeer(replyTo.replyToPeerId), replyToPeerId: replyTo.replyToPeerId && getApiChatIdFromMtpPeer(replyTo.replyToPeerId),
quoteText: replyTo.quoteText ? buildMessageTextContent(replyTo.quoteText, replyTo.quoteEntities) : undefined, quoteText: replyTo.quoteText ? buildMessageTextContent(replyTo.quoteText, replyTo.quoteEntities) : undefined,
quoteOffset: replyTo.quoteOffset,
} satisfies ApiInputMessageReplyInfo : undefined; } satisfies ApiInputMessageReplyInfo : undefined;
return { return {
@ -340,6 +341,7 @@ function buildApiReplyInfo(
quote, quote,
quoteText, quoteText,
quoteEntities, quoteEntities,
quoteOffset,
} = replyHeader; } = replyHeader;
return { return {
@ -352,6 +354,7 @@ function buildApiReplyInfo(
replyMedia: replyMedia && buildMessageMediaContent(replyMedia, context), replyMedia: replyMedia && buildMessageMediaContent(replyMedia, context),
isQuote: quote, isQuote: quote,
quoteText: quoteText ? buildMessageTextContent(quoteText, quoteEntities) : undefined, quoteText: quoteText ? buildMessageTextContent(quoteText, quoteEntities) : undefined,
quoteOffset,
}; };
} }
@ -554,6 +557,7 @@ function buildReplyInfo(inputInfo: ApiInputReplyInfo, isForum?: boolean): ApiRep
replyToTopId: inputInfo.replyToTopId, replyToTopId: inputInfo.replyToTopId,
replyToPeerId: inputInfo.replyToPeerId, replyToPeerId: inputInfo.replyToPeerId,
quoteText: inputInfo.quoteText, quoteText: inputInfo.quoteText,
quoteOffset: inputInfo.quoteOffset,
isForumTopic: isForum && inputInfo.replyToTopId ? true : undefined, isForumTopic: isForum && inputInfo.replyToTopId ? true : undefined,
...(Boolean(inputInfo.quoteText) && { isQuote: true }), ...(Boolean(inputInfo.quoteText) && { isQuote: true }),
}; };

View File

@ -805,7 +805,7 @@ export function buildInputReplyTo(replyInfo: ApiInputReplyInfo) {
if (replyInfo.type === 'message') { if (replyInfo.type === 'message') {
const { const {
replyToMsgId, replyToTopId, replyToPeerId, quoteText, replyToMsgId, replyToTopId, replyToPeerId, quoteText, quoteOffset,
} = replyInfo; } = replyInfo;
return new GramJs.InputReplyToMessage({ return new GramJs.InputReplyToMessage({
replyToMsgId, replyToMsgId,
@ -813,6 +813,7 @@ export function buildInputReplyTo(replyInfo: ApiInputReplyInfo) {
replyToPeerId: replyToPeerId ? buildInputPeerFromLocalDb(replyToPeerId)! : undefined, replyToPeerId: replyToPeerId ? buildInputPeerFromLocalDb(replyToPeerId)! : undefined,
quoteText: quoteText?.text, quoteText: quoteText?.text,
quoteEntities: quoteText?.entities?.map(buildMtpMessageEntity), quoteEntities: quoteText?.entities?.map(buildMtpMessageEntity),
quoteOffset,
}); });
} }

View File

@ -365,6 +365,7 @@ export interface ApiMessageReplyInfo {
isForumTopic?: true; isForumTopic?: true;
isQuote?: true; isQuote?: true;
quoteText?: ApiFormattedText; quoteText?: ApiFormattedText;
quoteOffset?: number;
} }
export interface ApiStoryReplyInfo { export interface ApiStoryReplyInfo {
@ -379,6 +380,7 @@ export interface ApiInputMessageReplyInfo {
replyToTopId?: number; replyToTopId?: number;
replyToPeerId?: string; replyToPeerId?: string;
quoteText?: ApiFormattedText; quoteText?: ApiFormattedText;
quoteOffset?: number;
} }
export interface ApiInputStoryReplyInfo { export interface ApiInputStoryReplyInfo {
@ -457,6 +459,7 @@ export type ApiMessageEntityCustomEmoji = {
documentId: string; documentId: string;
}; };
// Local entities
export type ApiMessageEntityTimestamp = { export type ApiMessageEntityTimestamp = {
type: ApiMessageEntityTypes.Timestamp; type: ApiMessageEntityTypes.Timestamp;
offset: number; offset: number;
@ -464,8 +467,15 @@ export type ApiMessageEntityTimestamp = {
timestamp: number; timestamp: number;
}; };
export type ApiMessageEntityQuoteFocus = {
type: 'quoteFocus';
offset: number;
length: number;
};
export type ApiMessageEntity = ApiMessageEntityDefault | ApiMessageEntityPre | ApiMessageEntityTextUrl | export type ApiMessageEntity = ApiMessageEntityDefault | ApiMessageEntityPre | ApiMessageEntityTextUrl |
ApiMessageEntityMentionName | ApiMessageEntityCustomEmoji | ApiMessageEntityBlockquote | ApiMessageEntityTimestamp; ApiMessageEntityMentionName | ApiMessageEntityCustomEmoji | ApiMessageEntityBlockquote | ApiMessageEntityTimestamp |
ApiMessageEntityQuoteFocus;
export enum ApiMessageEntityTypes { export enum ApiMessageEntityTypes {
Bold = 'MessageEntityBold', Bold = 'MessageEntityBold',
@ -487,6 +497,7 @@ export enum ApiMessageEntityTypes {
Spoiler = 'MessageEntitySpoiler', Spoiler = 'MessageEntitySpoiler',
CustomEmoji = 'MessageEntityCustomEmoji', CustomEmoji = 'MessageEntityCustomEmoji',
Timestamp = 'MessageEntityTimestamp', Timestamp = 'MessageEntityTimestamp',
QuoteFocus = 'MessageEntityQuoteFocus',
Unknown = 'MessageEntityUnknown', Unknown = 'MessageEntityUnknown',
} }

View File

@ -10,7 +10,7 @@ import { ApiMessageEntityTypes } from '../../api/types';
import { CONTENT_NOT_SUPPORTED } from '../../config'; import { CONTENT_NOT_SUPPORTED } from '../../config';
import { extractMessageText, stripCustomEmoji } from '../../global/helpers'; import { extractMessageText, stripCustomEmoji } from '../../global/helpers';
import trimText from '../../util/trimText'; import trimText from '../../util/trimText';
import { renderTextWithEntities } from './helpers/renderTextWithEntities'; import { insertTextEntity, renderTextWithEntities } from './helpers/renderTextWithEntities';
import useSyncEffect from '../../hooks/useSyncEffect'; import useSyncEffect from '../../hooks/useSyncEffect';
import useUniqueId from '../../hooks/useUniqueId'; import useUniqueId from '../../hooks/useUniqueId';
@ -32,6 +32,7 @@ interface OwnProps {
inChatList?: boolean; inChatList?: boolean;
forcePlayback?: boolean; forcePlayback?: boolean;
focusedQuote?: string; focusedQuote?: string;
focusedQuoteOffset?: number;
isInSelectMode?: boolean; isInSelectMode?: boolean;
canBeEmpty?: boolean; canBeEmpty?: boolean;
maxTimestamp?: number; maxTimestamp?: number;
@ -55,6 +56,7 @@ function MessageText({
inChatList, inChatList,
forcePlayback, forcePlayback,
focusedQuote, focusedQuote,
focusedQuoteOffset,
isInSelectMode, isInSelectMode,
canBeEmpty, canBeEmpty,
maxTimestamp, maxTimestamp,
@ -71,21 +73,38 @@ function MessageText({
const adaptedFormattedText = isForAnimation && formattedText ? stripCustomEmoji(formattedText) : formattedText; const adaptedFormattedText = isForAnimation && formattedText ? stripCustomEmoji(formattedText) : formattedText;
const { text, entities } = adaptedFormattedText || {}; const { text, entities } = adaptedFormattedText || {};
const entitiesWithFocusedQuote = useMemo(() => {
if (!text || !focusedQuote) return entities;
const index = text.indexOf(focusedQuote, focusedQuoteOffset);
const lendth = focusedQuote.length;
if (index >= 0) {
return insertTextEntity(entities || [], {
offset: index,
length: lendth,
type: ApiMessageEntityTypes.QuoteFocus,
});
}
return entities;
}, [text, entities, focusedQuote, focusedQuoteOffset]);
const containerId = useUniqueId(); const containerId = useUniqueId();
useSyncEffect(() => { useSyncEffect(() => {
textCacheBusterRef.current += 1; textCacheBusterRef.current += 1;
}, [text, entities]); }, [text, entitiesWithFocusedQuote]);
const withSharedCanvas = useMemo(() => { const withSharedCanvas = useMemo(() => {
const hasSpoilers = entities?.some((e) => e.type === ApiMessageEntityTypes.Spoiler); const hasSpoilers = entitiesWithFocusedQuote?.some((e) => e.type === ApiMessageEntityTypes.Spoiler);
if (hasSpoilers) { if (hasSpoilers) {
return false; return false;
} }
const customEmojisCount = entities?.filter((e) => e.type === ApiMessageEntityTypes.CustomEmoji).length || 0; const customEmojisCount = entitiesWithFocusedQuote
?.filter((e) => e.type === ApiMessageEntityTypes.CustomEmoji).length || 0;
return customEmojisCount >= MIN_CUSTOM_EMOJIS_FOR_SHARED_CANVAS; return customEmojisCount >= MIN_CUSTOM_EMOJIS_FOR_SHARED_CANVAS;
}, [entities]) || 0; }, [entitiesWithFocusedQuote]) || 0;
if (!text && !canBeEmpty) { if (!text && !canBeEmpty) {
return <span className="content-unsupported">{CONTENT_NOT_SUPPORTED}</span>; return <span className="content-unsupported">{CONTENT_NOT_SUPPORTED}</span>;
@ -98,7 +117,7 @@ function MessageText({
withSharedCanvas && <canvas ref={sharedCanvasHqRef} className="shared-canvas" />, withSharedCanvas && <canvas ref={sharedCanvasHqRef} className="shared-canvas" />,
renderTextWithEntities({ renderTextWithEntities({
text: trimText(text!, truncateLength), text: trimText(text!, truncateLength),
entities, entities: entitiesWithFocusedQuote,
highlight, highlight,
emojiSize, emojiSize,
shouldRenderAsHtml, shouldRenderAsHtml,
@ -112,7 +131,6 @@ function MessageText({
sharedCanvasHqRef, sharedCanvasHqRef,
cacheBuster: textCacheBusterRef.current.toString(), cacheBuster: textCacheBusterRef.current.toString(),
forcePlayback, forcePlayback,
focusedQuote,
isInSelectMode, isInSelectMode,
maxTimestamp, maxTimestamp,
chatId: 'chatId' in messageOrStory ? messageOrStory.chatId : undefined, chatId: 'chatId' in messageOrStory ? messageOrStory.chatId : undefined,

View File

@ -23,7 +23,7 @@ import SafeLink from '../SafeLink';
export type TextFilter = ( export type TextFilter = (
'escape_html' | 'hq_emoji' | 'emoji' | 'emoji_html' | 'br' | 'br_html' | 'highlight' | 'links' | 'escape_html' | 'hq_emoji' | 'emoji' | 'emoji_html' | 'br' | 'br_html' | 'highlight' | 'links' |
'simple_markdown' | 'simple_markdown_html' | 'quote' | 'tg_links' 'simple_markdown' | 'simple_markdown_html' | 'tg_links'
); );
const SIMPLE_MARKDOWN_REGEX = /(\*\*|__).+?\1/g; const SIMPLE_MARKDOWN_REGEX = /(\*\*|__).+?\1/g;
@ -31,7 +31,10 @@ const SIMPLE_MARKDOWN_REGEX = /(\*\*|__).+?\1/g;
export default function renderText( export default function renderText(
part: TextPart, part: TextPart,
filters: Array<TextFilter> = ['emoji'], filters: Array<TextFilter> = ['emoji'],
params?: { highlight?: string; quote?: string; markdownPostProcessor?: (part: string) => TeactNode }, params?: {
highlight?: string;
markdownPostProcessor?: (part: string) => TeactNode;
},
): TeactNode[] { ): TeactNode[] {
if (typeof part !== 'string') { if (typeof part !== 'string') {
return [part]; return [part];
@ -63,9 +66,6 @@ export default function renderText(
case 'highlight': case 'highlight':
return addHighlight(text, params!.highlight); return addHighlight(text, params!.highlight);
case 'quote':
return addHighlight(text, params!.quote, true);
case 'links': case 'links':
return addLinks(text); return addLinks(text);
@ -190,7 +190,7 @@ function addLineBreaks(textParts: TextPart[], type: 'jsx' | 'html'): TextPart[]
}, []); }, []);
} }
function addHighlight(textParts: TextPart[], highlight: string | undefined, isQuote?: true): TextPart[] { function addHighlight(textParts: TextPart[], highlight: string | undefined): TextPart[] {
return textParts.reduce<TextPart[]>((result, part) => { return textParts.reduce<TextPart[]>((result, part) => {
if (typeof part !== 'string' || !highlight) { if (typeof part !== 'string' || !highlight) {
result.push(part); result.push(part);
@ -207,7 +207,7 @@ function addHighlight(textParts: TextPart[], highlight: string | undefined, isQu
const newParts: TextPart[] = []; const newParts: TextPart[] = [];
newParts.push(part.substring(0, queryPosition)); newParts.push(part.substring(0, queryPosition));
newParts.push( newParts.push(
<span className={buildClassName('matching-text-highlight', isQuote && 'is-quote')}> <span className="matching-text-highlight">
{part.substring(queryPosition, queryPosition + highlight.length)} {part.substring(queryPosition, queryPosition + highlight.length)}
</span>, </span>,
); );

View File

@ -46,7 +46,6 @@ export function renderTextWithEntities({
cacheBuster, cacheBuster,
forcePlayback, forcePlayback,
noCustomEmojiPlayback, noCustomEmojiPlayback,
focusedQuote,
isInSelectMode, isInSelectMode,
chatId, chatId,
messageId, messageId,
@ -69,7 +68,6 @@ export function renderTextWithEntities({
cacheBuster?: string; cacheBuster?: string;
forcePlayback?: boolean; forcePlayback?: boolean;
noCustomEmojiPlayback?: boolean; noCustomEmojiPlayback?: boolean;
focusedQuote?: string;
isInSelectMode?: boolean; isInSelectMode?: boolean;
chatId?: string; chatId?: string;
messageId?: number; messageId?: number;
@ -80,7 +78,6 @@ export function renderTextWithEntities({
return renderMessagePart({ return renderMessagePart({
content: text, content: text,
highlight, highlight,
focusedQuote,
emojiSize, emojiSize,
shouldRenderAsHtml, shouldRenderAsHtml,
asPreview, asPreview,
@ -115,7 +112,6 @@ export function renderTextWithEntities({
renderResult.push(...renderMessagePart({ renderResult.push(...renderMessagePart({
content: textBefore, content: textBefore,
highlight, highlight,
focusedQuote,
emojiSize, emojiSize,
shouldRenderAsHtml, shouldRenderAsHtml,
asPreview, asPreview,
@ -166,7 +162,6 @@ export function renderTextWithEntities({
entityContent, entityContent,
nestedEntityContent, nestedEntityContent,
highlight, highlight,
focusedQuote,
containerId, containerId,
asPreview, asPreview,
isProtected, isProtected,
@ -203,7 +198,6 @@ export function renderTextWithEntities({
renderResult.push(...renderMessagePart({ renderResult.push(...renderMessagePart({
content: textAfter, content: textAfter,
highlight, highlight,
focusedQuote,
emojiSize, emojiSize,
shouldRenderAsHtml, shouldRenderAsHtml,
asPreview, asPreview,
@ -302,14 +296,63 @@ function renderMessagePart({
filters.push('highlight'); filters.push('highlight');
params.highlight = highlight; params.highlight = highlight;
} }
if (focusedQuote) {
filters.push('quote');
params.quote = focusedQuote;
}
return renderText(content, filters, params); return renderText(content, filters, params);
} }
export function insertTextEntity(entities: ApiMessageEntity[], newEntity: ApiMessageEntity) {
const resultEntities: ApiMessageEntity[] = [];
const newEntityStart = newEntity.offset;
const newEntityEnd = newEntity.offset + newEntity.length;
for (const existingEntity of entities) {
const existingEntityStart = existingEntity.offset;
const existingEntityEnd = existingEntity.offset + existingEntity.length;
// Push as is if edges do not overlap
if (existingEntityEnd <= newEntityStart
|| existingEntityStart > newEntityEnd
|| (existingEntityStart > newEntityStart
&& existingEntityEnd < newEntityEnd)) {
resultEntities.push(existingEntity);
continue;
}
// If start edge overlaps
if (existingEntityStart < newEntityStart && existingEntityEnd > newEntityStart) {
// Split entity in two
resultEntities.push({
...existingEntity,
length: newEntityStart - existingEntityStart,
});
resultEntities.push({
...existingEntity,
offset: newEntityStart,
length: existingEntityEnd - newEntityStart,
});
}
// If end edge overlaps
if (existingEntityStart < newEntityEnd
&& existingEntityEnd > newEntityEnd) {
// Split entity in two
resultEntities.push({
...existingEntity,
offset: newEntityEnd,
length: existingEntityEnd - newEntityStart - newEntity.length,
});
resultEntities.push({
...existingEntity,
length: newEntityEnd - existingEntityStart,
});
}
}
resultEntities.push(newEntity);
// Sort entities by offset, longer entities first
return resultEntities.sort((a, b) => a.offset - b.offset || b.length - a.length);
}
// Organize entities in a tree-like structure to better represent how the text will be displayed // Organize entities in a tree-like structure to better represent how the text will be displayed
function organizeEntities(entities: ApiMessageEntity[]) { function organizeEntities(entities: ApiMessageEntity[]) {
const organizedEntityIndexes: Set<number> = new Set(); const organizedEntityIndexes: Set<number> = new Set();
@ -381,7 +424,6 @@ function processEntity({
entityContent, entityContent,
nestedEntityContent, nestedEntityContent,
highlight, highlight,
focusedQuote,
containerId, containerId,
asPreview, asPreview,
isProtected, isProtected,
@ -430,7 +472,6 @@ function processEntity({
return renderMessagePart({ return renderMessagePart({
content: renderedContent, content: renderedContent,
highlight, highlight,
focusedQuote,
emojiSize, emojiSize,
asPreview, asPreview,
}); });
@ -613,6 +654,10 @@ function processEntity({
noPlay={noCustomEmojiPlayback} noPlay={noCustomEmojiPlayback}
/> />
); );
case ApiMessageEntityTypes.QuoteFocus:
return (
<span className="matching-text-highlight is-quote">{renderNestedMessagePart()}</span>
);
default: default:
return renderNestedMessagePart(); return renderNestedMessagePart();
} }

View File

@ -159,6 +159,7 @@ type StateProps = {
}; };
const selection = window.getSelection(); const selection = window.getSelection();
const UNQUOTABLE_OFFSET = -1;
const ContextMenuContainer: FC<OwnProps & StateProps> = ({ const ContextMenuContainer: FC<OwnProps & StateProps> = ({
threadId, threadId,
@ -272,7 +273,7 @@ const ContextMenuContainer: FC<OwnProps & StateProps> = ({
const [isMenuOpen, setIsMenuOpen] = useState(true); const [isMenuOpen, setIsMenuOpen] = useState(true);
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 [selectionQuoteOffset, setSelectionQuoteOffset] = useState(UNQUOTABLE_OFFSET);
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
@ -349,7 +350,7 @@ const ContextMenuContainer: FC<OwnProps & StateProps> = ({
useEffect(() => { useEffect(() => {
if (isMessageTranslated) { if (isMessageTranslated) {
setCanQuoteSelection(false); setSelectionQuoteOffset(UNQUOTABLE_OFFSET);
return; return;
} }
@ -359,7 +360,7 @@ const ContextMenuContainer: FC<OwnProps & StateProps> = ({
&& isSelectionRangeInsideMessage(selectionRange); && isSelectionRangeInsideMessage(selectionRange);
if (!isMessageTextSelected) { if (!isMessageTextSelected) {
setCanQuoteSelection(false); setSelectionQuoteOffset(UNQUOTABLE_OFFSET);
return; return;
} }
@ -367,10 +368,14 @@ const ContextMenuContainer: FC<OwnProps & StateProps> = ({
const messageText = message.content.text!.text!.replace(/\u00A0/g, ' '); const messageText = message.content.text!.text!.replace(/\u00A0/g, ' ');
setCanQuoteSelection( const canQuote = selectionText.text.trim().length > 0
selectionText.text.trim().length > 0 && messageText.includes(selectionText.text);
&& messageText.includes(selectionText.text), if (!canQuote) {
); setSelectionQuoteOffset(UNQUOTABLE_OFFSET);
return;
}
setSelectionQuoteOffset(selectionRange.startOffset);
}, [ }, [
selectionRange, selectionRange?.collapsed, selectionRange?.startOffset, selectionRange?.endOffset, selectionRange, selectionRange?.collapsed, selectionRange?.startOffset, selectionRange?.endOffset,
isMessageTranslated, message.content.text, isMessageTranslated, message.content.text,
@ -400,13 +405,17 @@ const ContextMenuContainer: FC<OwnProps & StateProps> = ({
}); });
const handleReply = useLastCallback(() => { const handleReply = useLastCallback(() => {
const quoteText = canQuoteSelection && selectionRange ? getSelectionAsFormattedText(selectionRange) : undefined; const quoteText = selectionQuoteOffset !== UNQUOTABLE_OFFSET && selectionRange
? getSelectionAsFormattedText(selectionRange) : undefined;
if (!canReplyInChat) { if (!canReplyInChat) {
openReplyMenu({ fromChatId: message.chatId, messageId: message.id, quoteText }); openReplyMenu({
fromChatId: message.chatId, messageId: message.id, quoteText, quoteOffset: selectionQuoteOffset,
});
} else { } else {
updateDraftReplyInfo({ updateDraftReplyInfo({
replyToMsgId: message.id, replyToMsgId: message.id,
quoteText, quoteText,
quoteOffset: selectionQuoteOffset,
replyToPeerId: undefined, replyToPeerId: undefined,
}); });
} }
@ -647,7 +656,7 @@ const ContextMenuContainer: FC<OwnProps & StateProps> = ({
canSendNow={canSendNow} canSendNow={canSendNow}
canReschedule={canReschedule} canReschedule={canReschedule}
canReply={canReply} canReply={canReply}
canQuote={canQuoteSelection} canQuote={selectionQuoteOffset !== UNQUOTABLE_OFFSET}
canDelete={canDelete} canDelete={canDelete}
canPin={canPin} canPin={canPin}
canReport={canReport} canReport={canReport}

View File

@ -245,6 +245,7 @@ type StateProps = {
isFocused?: boolean; isFocused?: boolean;
focusDirection?: FocusDirection; focusDirection?: FocusDirection;
focusedQuote?: string; focusedQuote?: string;
focusedQuoteOffset?: number;
noFocusHighlight?: boolean; noFocusHighlight?: boolean;
scrollTargetPosition?: ScrollTargetPosition; scrollTargetPosition?: ScrollTargetPosition;
isResizingContainer?: boolean; isResizingContainer?: boolean;
@ -368,6 +369,7 @@ const Message: FC<OwnProps & StateProps> = ({
isFocused, isFocused,
focusDirection, focusDirection,
focusedQuote, focusedQuote,
focusedQuoteOffset,
noFocusHighlight, noFocusHighlight,
scrollTargetPosition, scrollTargetPosition,
isResizingContainer, isResizingContainer,
@ -971,6 +973,7 @@ const Message: FC<OwnProps & StateProps> = ({
translatedText={requestedTranslationLanguage ? currentTranslatedText : undefined} translatedText={requestedTranslationLanguage ? currentTranslatedText : undefined}
isForAnimation={isForAnimation} isForAnimation={isForAnimation}
focusedQuote={focusedQuote} focusedQuote={focusedQuote}
focusedQuoteOffset={focusedQuoteOffset}
emojiSize={emojiSize} emojiSize={emojiSize}
highlight={highlight} highlight={highlight}
isProtected={isProtected} isProtected={isProtected}
@ -1788,7 +1791,7 @@ export default memo(withGlobal<OwnProps>(
const { const {
direction: focusDirection, noHighlight: noFocusHighlight, isResizingContainer, direction: focusDirection, noHighlight: noFocusHighlight, isResizingContainer,
quote: focusedQuote, scrollTargetPosition, quote: focusedQuote, quoteOffset: focusedQuoteOffset, scrollTargetPosition,
} = (isFocused && focusedMessage) || {}; } = (isFocused && focusedMessage) || {};
const middleSearch = selectCurrentMiddleSearch(global); const middleSearch = selectCurrentMiddleSearch(global);
@ -1935,6 +1938,7 @@ export default memo(withGlobal<OwnProps>(
noFocusHighlight, noFocusHighlight,
isResizingContainer, isResizingContainer,
focusedQuote, focusedQuote,
focusedQuoteOffset,
scrollTargetPosition, scrollTargetPosition,
}), }),
senderBoosts, senderBoosts,

View File

@ -65,7 +65,7 @@ export default function useInnerHandlers({
} = message; } = message;
const { const {
replyToMsgId, replyToPeerId, replyToTopId, isQuote, quoteText, replyToMsgId, replyToPeerId, replyToTopId, isQuote, quoteText, quoteOffset,
} = getMessageReplyInfo(message) || {}; } = getMessageReplyInfo(message) || {};
const handleSenderClick = useLastCallback(() => { const handleSenderClick = useLastCallback(() => {
@ -114,7 +114,7 @@ export default function useInnerHandlers({
messageId: replyToMsgId, messageId: replyToMsgId,
replyMessageId: replyToPeerId ? undefined : messageId, replyMessageId: replyToPeerId ? undefined : messageId,
noForumTopicPanel: !replyToPeerId, // Open topic panel for cross-chat replies noForumTopicPanel: !replyToPeerId, // Open topic panel for cross-chat replies
...(isQuote && { quote: quoteText?.text }), ...(isQuote && { quote: quoteText?.text, quoteOffset }),
}); });
}); });

View File

@ -138,7 +138,9 @@ export default function useOuterHandlers(
function handleContainerDoubleClick() { function handleContainerDoubleClick() {
if (IS_TOUCH_ENV || !canReply) return; if (IS_TOUCH_ENV || !canReply) return;
updateDraftReplyInfo({ replyToMsgId: messageId, replyToPeerId: undefined, quoteText: undefined }); updateDraftReplyInfo({
replyToMsgId: messageId, replyToPeerId: undefined, quoteText: undefined, quoteOffset: undefined,
});
} }
function stopPropagation(e: React.MouseEvent<HTMLDivElement, MouseEvent>) { function stopPropagation(e: React.MouseEvent<HTMLDivElement, MouseEvent>) {

View File

@ -2145,6 +2145,7 @@ addActionHandler('openChatOrTopicWithReplyInDraft', (global, actions, payload):
replyToTopId: replyingInfo.toThreadId, replyToTopId: replyingInfo.toThreadId,
replyToPeerId: currentChatId, replyToPeerId: currentChatId,
quoteText: replyingInfo.quoteText, quoteText: replyingInfo.quoteText,
quoteOffset: replyingInfo.quoteOffset,
} as ApiInputMessageReplyInfo; } as ApiInputMessageReplyInfo;
const currentReplyInfo = replyingInfo.messageId const currentReplyInfo = replyingInfo.messageId

View File

@ -405,8 +405,8 @@ addActionHandler('focusNextReply', (global, actions, payload): ActionReturnType
addActionHandler('focusMessage', (global, actions, payload): ActionReturnType => { addActionHandler('focusMessage', (global, actions, payload): ActionReturnType => {
const { const {
chatId, threadId = MAIN_THREAD_ID, messageListType = 'thread', noHighlight, groupedId, groupedChatId, chatId, threadId = MAIN_THREAD_ID, messageListType = 'thread', noHighlight, groupedId, groupedChatId,
replyMessageId, isResizingContainer, shouldReplaceHistory, noForumTopicPanel, quote, scrollTargetPosition, replyMessageId, isResizingContainer, shouldReplaceHistory, noForumTopicPanel, quote, quoteOffset,
timestamp, tabId = getCurrentTabId(), scrollTargetPosition, timestamp, tabId = getCurrentTabId(),
} = payload; } = payload;
let { messageId } = payload; let { messageId } = payload;
@ -455,6 +455,7 @@ addActionHandler('focusMessage', (global, actions, payload): ActionReturnType =>
noHighlight, noHighlight,
isResizingContainer, isResizingContainer,
quote, quote,
quoteOffset,
scrollTargetPosition, scrollTargetPosition,
}, tabId); }, tabId);
global = updateFocusDirection(global, undefined, tabId); global = updateFocusDirection(global, undefined, tabId);
@ -525,13 +526,14 @@ addActionHandler('setShouldPreventComposerAnimation', (global, actions, payload)
addActionHandler('openReplyMenu', (global, actions, payload): ActionReturnType => { addActionHandler('openReplyMenu', (global, actions, payload): ActionReturnType => {
const { const {
fromChatId, messageId, quoteText, tabId = getCurrentTabId(), fromChatId, messageId, quoteText, quoteOffset, tabId = getCurrentTabId(),
} = payload; } = payload;
return updateTabState(global, { return updateTabState(global, {
replyingMessage: { replyingMessage: {
fromChatId, fromChatId,
messageId, messageId,
quoteText, quoteText,
quoteOffset,
}, },
isShareMessageModalShown: true, isShareMessageModalShown: true,
}, tabId); }, tabId);

View File

@ -726,6 +726,7 @@ export function updateFocusedMessage<T extends GlobalState>(
noHighlight = false, noHighlight = false,
isResizingContainer = false, isResizingContainer = false,
quote, quote,
quoteOffset,
scrollTargetPosition, scrollTargetPosition,
}: { }: {
global: T; global: T;
@ -735,6 +736,7 @@ export function updateFocusedMessage<T extends GlobalState>(
noHighlight?: boolean; noHighlight?: boolean;
isResizingContainer?: boolean; isResizingContainer?: boolean;
quote?: string; quote?: string;
quoteOffset?: number;
scrollTargetPosition?: ScrollTargetPosition; scrollTargetPosition?: ScrollTargetPosition;
}, },
...[tabId = getCurrentTabId()]: TabArgs<T> ...[tabId = getCurrentTabId()]: TabArgs<T>
@ -748,6 +750,7 @@ export function updateFocusedMessage<T extends GlobalState>(
noHighlight, noHighlight,
isResizingContainer, isResizingContainer,
quote, quote,
quoteOffset,
scrollTargetPosition, scrollTargetPosition,
}, },
}, tabId); }, tabId);

View File

@ -956,6 +956,7 @@ export interface ActionPayloads {
shouldReplaceHistory?: boolean; shouldReplaceHistory?: boolean;
noForumTopicPanel?: boolean; noForumTopicPanel?: boolean;
quote?: string; quote?: string;
quoteOffset?: number;
scrollTargetPosition?: ScrollTargetPosition; scrollTargetPosition?: ScrollTargetPosition;
timestamp?: number; timestamp?: number;
} & WithTabId; } & WithTabId;
@ -1769,6 +1770,7 @@ export interface ActionPayloads {
fromChatId: string; fromChatId: string;
messageId?: number; messageId?: number;
quoteText?: ApiFormattedText; quoteText?: ApiFormattedText;
quoteOffset?: number;
} & WithTabId; } & WithTabId;
// Forwards // Forwards

View File

@ -156,6 +156,7 @@ export type TabState = {
noHighlight?: boolean; noHighlight?: boolean;
isResizingContainer?: boolean; isResizingContainer?: boolean;
quote?: string; quote?: string;
quoteOffset?: number;
scrollTargetPosition?: ScrollTargetPosition; scrollTargetPosition?: ScrollTargetPosition;
}; };
@ -352,6 +353,7 @@ export type TabState = {
fromChatId?: string; fromChatId?: string;
messageId?: number; messageId?: number;
quoteText?: ApiFormattedText; quoteText?: ApiFormattedText;
quoteOffset?: number;
toChatId?: string; toChatId?: string;
toThreadId?: ThreadId; toThreadId?: ThreadId;
}; };