diff --git a/src/api/gramjs/apiBuilders/messages.ts b/src/api/gramjs/apiBuilders/messages.ts index 00b51b10a..13571dfeb 100644 --- a/src/api/gramjs/apiBuilders/messages.ts +++ b/src/api/gramjs/apiBuilders/messages.ts @@ -286,6 +286,7 @@ export function buildMessageDraft(draft: GramJs.TypeDraftMessage): ApiDraft | un replyToTopId: replyTo.topMsgId, replyToPeerId: replyTo.replyToPeerId && getApiChatIdFromMtpPeer(replyTo.replyToPeerId), quoteText: replyTo.quoteText ? buildMessageTextContent(replyTo.quoteText, replyTo.quoteEntities) : undefined, + quoteOffset: replyTo.quoteOffset, } satisfies ApiInputMessageReplyInfo : undefined; return { @@ -340,6 +341,7 @@ function buildApiReplyInfo( quote, quoteText, quoteEntities, + quoteOffset, } = replyHeader; return { @@ -352,6 +354,7 @@ function buildApiReplyInfo( replyMedia: replyMedia && buildMessageMediaContent(replyMedia, context), isQuote: quote, quoteText: quoteText ? buildMessageTextContent(quoteText, quoteEntities) : undefined, + quoteOffset, }; } @@ -554,6 +557,7 @@ function buildReplyInfo(inputInfo: ApiInputReplyInfo, isForum?: boolean): ApiRep replyToTopId: inputInfo.replyToTopId, replyToPeerId: inputInfo.replyToPeerId, quoteText: inputInfo.quoteText, + quoteOffset: inputInfo.quoteOffset, isForumTopic: isForum && inputInfo.replyToTopId ? true : undefined, ...(Boolean(inputInfo.quoteText) && { isQuote: true }), }; diff --git a/src/api/gramjs/gramjsBuilders/index.ts b/src/api/gramjs/gramjsBuilders/index.ts index e47cd80a7..40b47ff06 100644 --- a/src/api/gramjs/gramjsBuilders/index.ts +++ b/src/api/gramjs/gramjsBuilders/index.ts @@ -805,7 +805,7 @@ export function buildInputReplyTo(replyInfo: ApiInputReplyInfo) { if (replyInfo.type === 'message') { const { - replyToMsgId, replyToTopId, replyToPeerId, quoteText, + replyToMsgId, replyToTopId, replyToPeerId, quoteText, quoteOffset, } = replyInfo; return new GramJs.InputReplyToMessage({ replyToMsgId, @@ -813,6 +813,7 @@ export function buildInputReplyTo(replyInfo: ApiInputReplyInfo) { replyToPeerId: replyToPeerId ? buildInputPeerFromLocalDb(replyToPeerId)! : undefined, quoteText: quoteText?.text, quoteEntities: quoteText?.entities?.map(buildMtpMessageEntity), + quoteOffset, }); } diff --git a/src/api/types/messages.ts b/src/api/types/messages.ts index deccb8126..c5d468ab2 100644 --- a/src/api/types/messages.ts +++ b/src/api/types/messages.ts @@ -365,6 +365,7 @@ export interface ApiMessageReplyInfo { isForumTopic?: true; isQuote?: true; quoteText?: ApiFormattedText; + quoteOffset?: number; } export interface ApiStoryReplyInfo { @@ -379,6 +380,7 @@ export interface ApiInputMessageReplyInfo { replyToTopId?: number; replyToPeerId?: string; quoteText?: ApiFormattedText; + quoteOffset?: number; } export interface ApiInputStoryReplyInfo { @@ -457,6 +459,7 @@ export type ApiMessageEntityCustomEmoji = { documentId: string; }; +// Local entities export type ApiMessageEntityTimestamp = { type: ApiMessageEntityTypes.Timestamp; offset: number; @@ -464,8 +467,15 @@ export type ApiMessageEntityTimestamp = { timestamp: number; }; +export type ApiMessageEntityQuoteFocus = { + type: 'quoteFocus'; + offset: number; + length: number; +}; + export type ApiMessageEntity = ApiMessageEntityDefault | ApiMessageEntityPre | ApiMessageEntityTextUrl | -ApiMessageEntityMentionName | ApiMessageEntityCustomEmoji | ApiMessageEntityBlockquote | ApiMessageEntityTimestamp; +ApiMessageEntityMentionName | ApiMessageEntityCustomEmoji | ApiMessageEntityBlockquote | ApiMessageEntityTimestamp | +ApiMessageEntityQuoteFocus; export enum ApiMessageEntityTypes { Bold = 'MessageEntityBold', @@ -487,6 +497,7 @@ export enum ApiMessageEntityTypes { Spoiler = 'MessageEntitySpoiler', CustomEmoji = 'MessageEntityCustomEmoji', Timestamp = 'MessageEntityTimestamp', + QuoteFocus = 'MessageEntityQuoteFocus', Unknown = 'MessageEntityUnknown', } diff --git a/src/components/common/MessageText.tsx b/src/components/common/MessageText.tsx index 824a96189..6322043a7 100644 --- a/src/components/common/MessageText.tsx +++ b/src/components/common/MessageText.tsx @@ -10,7 +10,7 @@ import { ApiMessageEntityTypes } from '../../api/types'; import { CONTENT_NOT_SUPPORTED } from '../../config'; import { extractMessageText, stripCustomEmoji } from '../../global/helpers'; import trimText from '../../util/trimText'; -import { renderTextWithEntities } from './helpers/renderTextWithEntities'; +import { insertTextEntity, renderTextWithEntities } from './helpers/renderTextWithEntities'; import useSyncEffect from '../../hooks/useSyncEffect'; import useUniqueId from '../../hooks/useUniqueId'; @@ -32,6 +32,7 @@ interface OwnProps { inChatList?: boolean; forcePlayback?: boolean; focusedQuote?: string; + focusedQuoteOffset?: number; isInSelectMode?: boolean; canBeEmpty?: boolean; maxTimestamp?: number; @@ -55,6 +56,7 @@ function MessageText({ inChatList, forcePlayback, focusedQuote, + focusedQuoteOffset, isInSelectMode, canBeEmpty, maxTimestamp, @@ -71,21 +73,38 @@ function MessageText({ const adaptedFormattedText = isForAnimation && formattedText ? stripCustomEmoji(formattedText) : formattedText; 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(); useSyncEffect(() => { textCacheBusterRef.current += 1; - }, [text, entities]); + }, [text, entitiesWithFocusedQuote]); const withSharedCanvas = useMemo(() => { - const hasSpoilers = entities?.some((e) => e.type === ApiMessageEntityTypes.Spoiler); + const hasSpoilers = entitiesWithFocusedQuote?.some((e) => e.type === ApiMessageEntityTypes.Spoiler); if (hasSpoilers) { 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; - }, [entities]) || 0; + }, [entitiesWithFocusedQuote]) || 0; if (!text && !canBeEmpty) { return {CONTENT_NOT_SUPPORTED}; @@ -98,7 +117,7 @@ function MessageText({ withSharedCanvas && , renderTextWithEntities({ text: trimText(text!, truncateLength), - entities, + entities: entitiesWithFocusedQuote, highlight, emojiSize, shouldRenderAsHtml, @@ -112,7 +131,6 @@ function MessageText({ sharedCanvasHqRef, cacheBuster: textCacheBusterRef.current.toString(), forcePlayback, - focusedQuote, isInSelectMode, maxTimestamp, chatId: 'chatId' in messageOrStory ? messageOrStory.chatId : undefined, diff --git a/src/components/common/helpers/renderText.tsx b/src/components/common/helpers/renderText.tsx index 54190bff0..3b7c1ad96 100644 --- a/src/components/common/helpers/renderText.tsx +++ b/src/components/common/helpers/renderText.tsx @@ -23,7 +23,7 @@ import SafeLink from '../SafeLink'; export type TextFilter = ( '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; @@ -31,7 +31,10 @@ const SIMPLE_MARKDOWN_REGEX = /(\*\*|__).+?\1/g; export default function renderText( part: TextPart, filters: Array = ['emoji'], - params?: { highlight?: string; quote?: string; markdownPostProcessor?: (part: string) => TeactNode }, + params?: { + highlight?: string; + markdownPostProcessor?: (part: string) => TeactNode; + }, ): TeactNode[] { if (typeof part !== 'string') { return [part]; @@ -63,9 +66,6 @@ export default function renderText( case 'highlight': return addHighlight(text, params!.highlight); - case 'quote': - return addHighlight(text, params!.quote, true); - case 'links': 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((result, part) => { if (typeof part !== 'string' || !highlight) { result.push(part); @@ -207,7 +207,7 @@ function addHighlight(textParts: TextPart[], highlight: string | undefined, isQu const newParts: TextPart[] = []; newParts.push(part.substring(0, queryPosition)); newParts.push( - + {part.substring(queryPosition, queryPosition + highlight.length)} , ); diff --git a/src/components/common/helpers/renderTextWithEntities.tsx b/src/components/common/helpers/renderTextWithEntities.tsx index b10b39dab..977b20fe5 100644 --- a/src/components/common/helpers/renderTextWithEntities.tsx +++ b/src/components/common/helpers/renderTextWithEntities.tsx @@ -46,7 +46,6 @@ export function renderTextWithEntities({ cacheBuster, forcePlayback, noCustomEmojiPlayback, - focusedQuote, isInSelectMode, chatId, messageId, @@ -69,7 +68,6 @@ export function renderTextWithEntities({ cacheBuster?: string; forcePlayback?: boolean; noCustomEmojiPlayback?: boolean; - focusedQuote?: string; isInSelectMode?: boolean; chatId?: string; messageId?: number; @@ -80,7 +78,6 @@ export function renderTextWithEntities({ return renderMessagePart({ content: text, highlight, - focusedQuote, emojiSize, shouldRenderAsHtml, asPreview, @@ -115,7 +112,6 @@ export function renderTextWithEntities({ renderResult.push(...renderMessagePart({ content: textBefore, highlight, - focusedQuote, emojiSize, shouldRenderAsHtml, asPreview, @@ -166,7 +162,6 @@ export function renderTextWithEntities({ entityContent, nestedEntityContent, highlight, - focusedQuote, containerId, asPreview, isProtected, @@ -203,7 +198,6 @@ export function renderTextWithEntities({ renderResult.push(...renderMessagePart({ content: textAfter, highlight, - focusedQuote, emojiSize, shouldRenderAsHtml, asPreview, @@ -302,14 +296,63 @@ function renderMessagePart({ filters.push('highlight'); params.highlight = highlight; } - if (focusedQuote) { - filters.push('quote'); - params.quote = focusedQuote; - } 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 function organizeEntities(entities: ApiMessageEntity[]) { const organizedEntityIndexes: Set = new Set(); @@ -381,7 +424,6 @@ function processEntity({ entityContent, nestedEntityContent, highlight, - focusedQuote, containerId, asPreview, isProtected, @@ -430,7 +472,6 @@ function processEntity({ return renderMessagePart({ content: renderedContent, highlight, - focusedQuote, emojiSize, asPreview, }); @@ -613,6 +654,10 @@ function processEntity({ noPlay={noCustomEmojiPlayback} /> ); + case ApiMessageEntityTypes.QuoteFocus: + return ( + {renderNestedMessagePart()} + ); default: return renderNestedMessagePart(); } diff --git a/src/components/middle/message/ContextMenuContainer.tsx b/src/components/middle/message/ContextMenuContainer.tsx index 75d0d5f14..9229eacf6 100644 --- a/src/components/middle/message/ContextMenuContainer.tsx +++ b/src/components/middle/message/ContextMenuContainer.tsx @@ -159,6 +159,7 @@ type StateProps = { }; const selection = window.getSelection(); +const UNQUOTABLE_OFFSET = -1; const ContextMenuContainer: FC = ({ threadId, @@ -272,7 +273,7 @@ const ContextMenuContainer: FC = ({ const [isMenuOpen, setIsMenuOpen] = useState(true); const [isPinModalOpen, setIsPinModalOpen] = useState(false); const [isClosePollDialogOpen, openClosePollDialog, closeClosePollDialog] = useFlag(); - const [canQuoteSelection, setCanQuoteSelection] = useState(false); + const [selectionQuoteOffset, setSelectionQuoteOffset] = useState(UNQUOTABLE_OFFSET); const [requestCalendar, calendar] = useSchedule(canScheduleUntilOnline, onClose, message.date); // `undefined` indicates that emoji are present and loading @@ -349,7 +350,7 @@ const ContextMenuContainer: FC = ({ useEffect(() => { if (isMessageTranslated) { - setCanQuoteSelection(false); + setSelectionQuoteOffset(UNQUOTABLE_OFFSET); return; } @@ -359,7 +360,7 @@ const ContextMenuContainer: FC = ({ && isSelectionRangeInsideMessage(selectionRange); if (!isMessageTextSelected) { - setCanQuoteSelection(false); + setSelectionQuoteOffset(UNQUOTABLE_OFFSET); return; } @@ -367,10 +368,14 @@ const ContextMenuContainer: FC = ({ const messageText = message.content.text!.text!.replace(/\u00A0/g, ' '); - setCanQuoteSelection( - selectionText.text.trim().length > 0 - && messageText.includes(selectionText.text), - ); + const canQuote = selectionText.text.trim().length > 0 + && messageText.includes(selectionText.text); + if (!canQuote) { + setSelectionQuoteOffset(UNQUOTABLE_OFFSET); + return; + } + + setSelectionQuoteOffset(selectionRange.startOffset); }, [ selectionRange, selectionRange?.collapsed, selectionRange?.startOffset, selectionRange?.endOffset, isMessageTranslated, message.content.text, @@ -400,13 +405,17 @@ const ContextMenuContainer: FC = ({ }); const handleReply = useLastCallback(() => { - const quoteText = canQuoteSelection && selectionRange ? getSelectionAsFormattedText(selectionRange) : undefined; + const quoteText = selectionQuoteOffset !== UNQUOTABLE_OFFSET && selectionRange + ? getSelectionAsFormattedText(selectionRange) : undefined; if (!canReplyInChat) { - openReplyMenu({ fromChatId: message.chatId, messageId: message.id, quoteText }); + openReplyMenu({ + fromChatId: message.chatId, messageId: message.id, quoteText, quoteOffset: selectionQuoteOffset, + }); } else { updateDraftReplyInfo({ replyToMsgId: message.id, quoteText, + quoteOffset: selectionQuoteOffset, replyToPeerId: undefined, }); } @@ -647,7 +656,7 @@ const ContextMenuContainer: FC = ({ canSendNow={canSendNow} canReschedule={canReschedule} canReply={canReply} - canQuote={canQuoteSelection} + canQuote={selectionQuoteOffset !== UNQUOTABLE_OFFSET} canDelete={canDelete} canPin={canPin} canReport={canReport} diff --git a/src/components/middle/message/Message.tsx b/src/components/middle/message/Message.tsx index 6574f2efa..d01282e86 100644 --- a/src/components/middle/message/Message.tsx +++ b/src/components/middle/message/Message.tsx @@ -245,6 +245,7 @@ type StateProps = { isFocused?: boolean; focusDirection?: FocusDirection; focusedQuote?: string; + focusedQuoteOffset?: number; noFocusHighlight?: boolean; scrollTargetPosition?: ScrollTargetPosition; isResizingContainer?: boolean; @@ -368,6 +369,7 @@ const Message: FC = ({ isFocused, focusDirection, focusedQuote, + focusedQuoteOffset, noFocusHighlight, scrollTargetPosition, isResizingContainer, @@ -971,6 +973,7 @@ const Message: FC = ({ translatedText={requestedTranslationLanguage ? currentTranslatedText : undefined} isForAnimation={isForAnimation} focusedQuote={focusedQuote} + focusedQuoteOffset={focusedQuoteOffset} emojiSize={emojiSize} highlight={highlight} isProtected={isProtected} @@ -1788,7 +1791,7 @@ export default memo(withGlobal( const { direction: focusDirection, noHighlight: noFocusHighlight, isResizingContainer, - quote: focusedQuote, scrollTargetPosition, + quote: focusedQuote, quoteOffset: focusedQuoteOffset, scrollTargetPosition, } = (isFocused && focusedMessage) || {}; const middleSearch = selectCurrentMiddleSearch(global); @@ -1935,6 +1938,7 @@ export default memo(withGlobal( noFocusHighlight, isResizingContainer, focusedQuote, + focusedQuoteOffset, scrollTargetPosition, }), senderBoosts, diff --git a/src/components/middle/message/hooks/useInnerHandlers.ts b/src/components/middle/message/hooks/useInnerHandlers.ts index b53022714..a9159db6f 100644 --- a/src/components/middle/message/hooks/useInnerHandlers.ts +++ b/src/components/middle/message/hooks/useInnerHandlers.ts @@ -65,7 +65,7 @@ export default function useInnerHandlers({ } = message; const { - replyToMsgId, replyToPeerId, replyToTopId, isQuote, quoteText, + replyToMsgId, replyToPeerId, replyToTopId, isQuote, quoteText, quoteOffset, } = getMessageReplyInfo(message) || {}; const handleSenderClick = useLastCallback(() => { @@ -114,7 +114,7 @@ export default function useInnerHandlers({ messageId: replyToMsgId, replyMessageId: replyToPeerId ? undefined : messageId, noForumTopicPanel: !replyToPeerId, // Open topic panel for cross-chat replies - ...(isQuote && { quote: quoteText?.text }), + ...(isQuote && { quote: quoteText?.text, quoteOffset }), }); }); diff --git a/src/components/middle/message/hooks/useOuterHandlers.ts b/src/components/middle/message/hooks/useOuterHandlers.ts index 7b689bb25..1aa7749ae 100644 --- a/src/components/middle/message/hooks/useOuterHandlers.ts +++ b/src/components/middle/message/hooks/useOuterHandlers.ts @@ -138,7 +138,9 @@ export default function useOuterHandlers( function handleContainerDoubleClick() { 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) { diff --git a/src/global/actions/api/messages.ts b/src/global/actions/api/messages.ts index 05e4eeabd..969de0723 100644 --- a/src/global/actions/api/messages.ts +++ b/src/global/actions/api/messages.ts @@ -2145,6 +2145,7 @@ addActionHandler('openChatOrTopicWithReplyInDraft', (global, actions, payload): replyToTopId: replyingInfo.toThreadId, replyToPeerId: currentChatId, quoteText: replyingInfo.quoteText, + quoteOffset: replyingInfo.quoteOffset, } as ApiInputMessageReplyInfo; const currentReplyInfo = replyingInfo.messageId diff --git a/src/global/actions/ui/messages.ts b/src/global/actions/ui/messages.ts index 00ed7d73e..df461cc0c 100644 --- a/src/global/actions/ui/messages.ts +++ b/src/global/actions/ui/messages.ts @@ -405,8 +405,8 @@ addActionHandler('focusNextReply', (global, actions, payload): ActionReturnType addActionHandler('focusMessage', (global, actions, payload): ActionReturnType => { const { chatId, threadId = MAIN_THREAD_ID, messageListType = 'thread', noHighlight, groupedId, groupedChatId, - replyMessageId, isResizingContainer, shouldReplaceHistory, noForumTopicPanel, quote, scrollTargetPosition, - timestamp, tabId = getCurrentTabId(), + replyMessageId, isResizingContainer, shouldReplaceHistory, noForumTopicPanel, quote, quoteOffset, + scrollTargetPosition, timestamp, tabId = getCurrentTabId(), } = payload; let { messageId } = payload; @@ -455,6 +455,7 @@ addActionHandler('focusMessage', (global, actions, payload): ActionReturnType => noHighlight, isResizingContainer, quote, + quoteOffset, scrollTargetPosition, }, tabId); global = updateFocusDirection(global, undefined, tabId); @@ -525,13 +526,14 @@ addActionHandler('setShouldPreventComposerAnimation', (global, actions, payload) addActionHandler('openReplyMenu', (global, actions, payload): ActionReturnType => { const { - fromChatId, messageId, quoteText, tabId = getCurrentTabId(), + fromChatId, messageId, quoteText, quoteOffset, tabId = getCurrentTabId(), } = payload; return updateTabState(global, { replyingMessage: { fromChatId, messageId, quoteText, + quoteOffset, }, isShareMessageModalShown: true, }, tabId); diff --git a/src/global/reducers/messages.ts b/src/global/reducers/messages.ts index 9b139f78a..48977dd81 100644 --- a/src/global/reducers/messages.ts +++ b/src/global/reducers/messages.ts @@ -726,6 +726,7 @@ export function updateFocusedMessage( noHighlight = false, isResizingContainer = false, quote, + quoteOffset, scrollTargetPosition, }: { global: T; @@ -735,6 +736,7 @@ export function updateFocusedMessage( noHighlight?: boolean; isResizingContainer?: boolean; quote?: string; + quoteOffset?: number; scrollTargetPosition?: ScrollTargetPosition; }, ...[tabId = getCurrentTabId()]: TabArgs @@ -748,6 +750,7 @@ export function updateFocusedMessage( noHighlight, isResizingContainer, quote, + quoteOffset, scrollTargetPosition, }, }, tabId); diff --git a/src/global/types/actions.ts b/src/global/types/actions.ts index 326dd8614..b2e32eaa4 100644 --- a/src/global/types/actions.ts +++ b/src/global/types/actions.ts @@ -956,6 +956,7 @@ export interface ActionPayloads { shouldReplaceHistory?: boolean; noForumTopicPanel?: boolean; quote?: string; + quoteOffset?: number; scrollTargetPosition?: ScrollTargetPosition; timestamp?: number; } & WithTabId; @@ -1769,6 +1770,7 @@ export interface ActionPayloads { fromChatId: string; messageId?: number; quoteText?: ApiFormattedText; + quoteOffset?: number; } & WithTabId; // Forwards diff --git a/src/global/types/tabState.ts b/src/global/types/tabState.ts index 874561541..0fab521b5 100644 --- a/src/global/types/tabState.ts +++ b/src/global/types/tabState.ts @@ -156,6 +156,7 @@ export type TabState = { noHighlight?: boolean; isResizingContainer?: boolean; quote?: string; + quoteOffset?: number; scrollTargetPosition?: ScrollTargetPosition; }; @@ -352,6 +353,7 @@ export type TabState = { fromChatId?: string; messageId?: number; quoteText?: ApiFormattedText; + quoteOffset?: number; toChatId?: string; toThreadId?: ThreadId; };