From 58e79daddba4d699e695b83347c6f4f6d92bc591 Mon Sep 17 00:00:00 2001
From: zubiden <19638254+zubiden@users.noreply.github.com>
Date: Wed, 23 Apr 2025 18:59:11 +0200
Subject: [PATCH] Message: Support quote highlight with entities (#5842)
---
src/api/gramjs/apiBuilders/messages.ts | 4 ++
src/api/gramjs/gramjsBuilders/index.ts | 3 +-
src/api/types/messages.ts | 13 +++-
src/components/common/MessageText.tsx | 32 +++++++--
src/components/common/helpers/renderText.tsx | 14 ++--
.../common/helpers/renderTextWithEntities.tsx | 69 +++++++++++++++----
.../middle/message/ContextMenuContainer.tsx | 29 +++++---
src/components/middle/message/Message.tsx | 6 +-
.../middle/message/hooks/useInnerHandlers.ts | 4 +-
.../middle/message/hooks/useOuterHandlers.ts | 4 +-
src/global/actions/api/messages.ts | 1 +
src/global/actions/ui/messages.ts | 8 ++-
src/global/reducers/messages.ts | 3 +
src/global/types/actions.ts | 2 +
src/global/types/tabState.ts | 2 +
15 files changed, 149 insertions(+), 45 deletions(-)
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;
};