From 17fe96dff397473b3cb92752151e4a15c1c30f8c Mon Sep 17 00:00:00 2001 From: Shahaf <10106174+kotevcode@users.noreply.github.com> Date: Fri, 5 Sep 2025 19:34:40 +0300 Subject: [PATCH] Message: Preserve focus stack for deeplinks within the same chat (#464) --- src/api/types/messages.ts | 7 +++++++ src/components/common/Composer.tsx | 1 + src/components/common/SafeLink.tsx | 15 ++++++++++++++- .../common/helpers/renderTextWithEntities.tsx | 3 +++ .../middle/composer/BotKeyboardMenu.tsx | 8 ++++++-- src/components/middle/message/Game.tsx | 4 ++++ src/components/middle/message/Message.tsx | 1 + src/global/actions/api/bots.ts | 14 +++++++------- src/global/actions/api/chats.ts | 16 +++++++++++++--- src/global/actions/api/messages.ts | 4 ++-- src/global/types/actions.ts | 7 +++++++ src/util/deeplink.ts | 6 ++++-- 12 files changed, 69 insertions(+), 17 deletions(-) diff --git a/src/api/types/messages.ts b/src/api/types/messages.ts index c8da1d487..70c900066 100644 --- a/src/api/types/messages.ts +++ b/src/api/types/messages.ts @@ -1038,6 +1038,13 @@ export type ApiSearchPostsFlood = { starsAmount: number; }; +export type LinkContext = { + type: 'message'; + threadId?: ThreadId; + chatId: string; + messageId: number; +}; + export const MAIN_THREAD_ID = -1; // `Symbol` can not be transferred from worker diff --git a/src/components/common/Composer.tsx b/src/components/common/Composer.tsx index c28804a41..50936bd10 100644 --- a/src/components/common/Composer.tsx +++ b/src/components/common/Composer.tsx @@ -2256,6 +2256,7 @@ const Composer: FC = ({ {isInMessageList && Boolean(botKeyboardMessageId) && ( diff --git a/src/components/common/SafeLink.tsx b/src/components/common/SafeLink.tsx index 739248d84..2397b5be8 100644 --- a/src/components/common/SafeLink.tsx +++ b/src/components/common/SafeLink.tsx @@ -2,6 +2,7 @@ import type { TeactNode } from '../../lib/teact/teact'; import type React from '../../lib/teact/teact'; import { getActions } from '../../global'; +import type { ThreadId } from '../../types'; import { ApiMessageEntityTypes } from '../../api/types'; import { ensureProtocol, getUnicodeUrl, isMixedScriptUrl } from '../../util/browser/url'; @@ -16,6 +17,9 @@ type OwnProps = { children?: TeactNode; isRtl?: boolean; shouldSkipModal?: boolean; + chatId?: string; + messageId?: number; + threadId?: ThreadId; }; const SafeLink = ({ @@ -25,6 +29,9 @@ const SafeLink = ({ children, isRtl, shouldSkipModal, + chatId, + messageId, + threadId, }: OwnProps) => { const { openUrl } = getActions(); @@ -37,7 +44,13 @@ const SafeLink = ({ e.preventDefault(); const isTrustedLink = isRegularLink && !isMixedScriptUrl(url); - openUrl({ url, shouldSkipModal: shouldSkipModal || isTrustedLink }); + openUrl({ + url, + shouldSkipModal: shouldSkipModal || isTrustedLink, + ...(chatId && messageId && { + linkContext: { type: 'message', chatId, threadId, messageId }, + }), + }); return false; }); diff --git a/src/components/common/helpers/renderTextWithEntities.tsx b/src/components/common/helpers/renderTextWithEntities.tsx index 893ce8b77..037691c3e 100644 --- a/src/components/common/helpers/renderTextWithEntities.tsx +++ b/src/components/common/helpers/renderTextWithEntities.tsx @@ -616,6 +616,9 @@ function processEntity({ {renderNestedMessagePart()} diff --git a/src/components/middle/composer/BotKeyboardMenu.tsx b/src/components/middle/composer/BotKeyboardMenu.tsx index 786469f21..745b5fb48 100644 --- a/src/components/middle/composer/BotKeyboardMenu.tsx +++ b/src/components/middle/composer/BotKeyboardMenu.tsx @@ -3,6 +3,7 @@ import { memo, useMemo } from '../../../lib/teact/teact'; import { getActions, withGlobal } from '../../../global'; import type { ApiMessage } from '../../../api/types'; +import type { ThreadId } from '../../../types'; import { selectChatMessage, selectCurrentMessageList } from '../../../global/selectors'; import { IS_TOUCH_ENV } from '../../../util/browser/windowEnvironment'; @@ -24,10 +25,11 @@ export type OwnProps = { type StateProps = { message?: ApiMessage; + threadId?: ThreadId; }; const BotKeyboardMenu: FC = ({ - isOpen, message, onClose, + isOpen, message, onClose, threadId, }) => { const { clickBotInlineButton } = getActions(); @@ -70,7 +72,9 @@ const BotKeyboardMenu: FC = ({ ripple disabled={button.type === 'unsupported'} - onClick={() => clickBotInlineButton({ chatId: message.chatId, messageId: message.id, button })} + onClick={() => clickBotInlineButton({ + chatId: message.chatId, messageId: message.id, threadId, button, + })} > {buttonTexts?.[i][j]} diff --git a/src/components/middle/message/Game.tsx b/src/components/middle/message/Game.tsx index 89e6bb8a4..92d26c7f5 100644 --- a/src/components/middle/message/Game.tsx +++ b/src/components/middle/message/Game.tsx @@ -3,6 +3,7 @@ import { memo } from '../../../lib/teact/teact'; import { getActions } from '../../../global'; import type { ApiMessage } from '../../../api/types'; +import type { ThreadId } from '../../../types'; import { getGamePreviewPhotoHash, getGamePreviewVideoHash, getMessageText } from '../../../global/helpers'; @@ -19,11 +20,13 @@ const DEFAULT_PREVIEW_DIMENSIONS = { type OwnProps = { message: ApiMessage; + threadId?: ThreadId; canAutoLoadMedia?: boolean; }; const Game: FC = ({ message, + threadId, canAutoLoadMedia, }) => { const { clickBotInlineButton } = getActions(); @@ -41,6 +44,7 @@ const Game: FC = ({ clickBotInlineButton({ chatId: message.chatId, messageId: message.id, + threadId, button: message.inlineButtons![0][0], }); }; diff --git a/src/components/middle/message/Message.tsx b/src/components/middle/message/Message.tsx index b2f13df99..2b8a130d6 100644 --- a/src/components/middle/message/Message.tsx +++ b/src/components/middle/message/Message.tsx @@ -1286,6 +1286,7 @@ const Message: FC = ({ {game && ( )} diff --git a/src/global/actions/api/bots.ts b/src/global/actions/api/bots.ts index c5e7cfbd9..9a2e21af5 100644 --- a/src/global/actions/api/bots.ts +++ b/src/global/actions/api/bots.ts @@ -1,4 +1,4 @@ -import type { InlineBotSettings } from '../../../types'; +import type { InlineBotSettings, ThreadId } from '../../../types'; import type { WebApp } from '../../../types/webapp'; import type { RequiredGlobalActions } from '../../index'; import type { @@ -95,7 +95,7 @@ addActionHandler('clickSuggestedMessageButton', (global, actions, payload): Acti addActionHandler('clickBotInlineButton', (global, actions, payload): ActionReturnType => { const { - chatId, messageId, button, tabId = getCurrentTabId(), + chatId, messageId, threadId, button, tabId = getCurrentTabId(), } = payload; const chat = selectChat(global, chatId); const message = selectChatMessage(global, chatId, messageId); @@ -109,7 +109,7 @@ addActionHandler('clickBotInlineButton', (global, actions, payload): ActionRetur break; case 'url': { const { url } = button; - actions.openUrl({ url, tabId }); + actions.openUrl({ url, tabId, linkContext: { type: 'message', chatId, messageId, threadId } }); break; } case 'copy': { @@ -118,7 +118,7 @@ addActionHandler('clickBotInlineButton', (global, actions, payload): ActionRetur break; } case 'callback': { - void answerCallbackButton(global, actions, chat, messageId, button.data, undefined, tabId); + void answerCallbackButton(global, actions, chat, messageId, threadId, button.data, undefined, tabId); break; } case 'requestPoll': @@ -157,7 +157,7 @@ addActionHandler('clickBotInlineButton', (global, actions, payload): ActionRetur break; } case 'game': { - void answerCallbackButton(global, actions, chat, messageId, undefined, true, tabId); + void answerCallbackButton(global, actions, chat, messageId, threadId, undefined, true, tabId); break; } case 'switchBotInline': { @@ -1287,7 +1287,7 @@ async function sendBotCommand( async function answerCallbackButton( global: T, - actions: RequiredGlobalActions, chat: ApiChat, messageId: number, data?: string, isGame = false, + actions: RequiredGlobalActions, chat: ApiChat, messageId: number, threadId?: ThreadId, data?: string, isGame = false, ...[tabId = getCurrentTabId()]: TabArgs ) { const { @@ -1317,7 +1317,7 @@ async function answerCallbackButton( url, chatId: chat.id, messageId, tabId, }); } else { - openUrl({ url, tabId }); + openUrl({ url, tabId, linkContext: { type: 'message', chatId: chat.id, messageId, threadId } }); } } } diff --git a/src/global/actions/api/chats.ts b/src/global/actions/api/chats.ts index 3bf033a29..af5cfd965 100644 --- a/src/global/actions/api/chats.ts +++ b/src/global/actions/api/chats.ts @@ -2,6 +2,7 @@ import type { ApiChat, ApiChatFolder, ApiChatlistExportedInvite, ApiChatMember, ApiError, ApiMissingInvitedUser, ApiTopic, + LinkContext, } from '../../../api/types'; import type { RequiredGlobalActions } from '../../index'; import type { @@ -1458,6 +1459,7 @@ addActionHandler('openTelegramLink', async (global, actions, payload): Promise { const { - id, commentId, messageId, threadId, timestamp, tabId = getCurrentTabId(), + id, commentId, messageId, threadId, timestamp, linkContext, tabId = getCurrentTabId(), } = payload; const chat = selectChat(global, id); if (!chat) { @@ -1827,6 +1830,7 @@ addActionHandler('openPrivateChannel', (global, actions, payload): ActionReturnT messageId, threadId, timestamp, + linkContext, }, tabId); }); @@ -3401,11 +3405,13 @@ async function openChatByUsername( attach?: string; text?: string; timestamp?: number; + linkContext?: LinkContext; }, ...[tabId = getCurrentTabId()]: TabArgs ) { const { username, threadId, channelPostId, startParam, ref, startAttach, attach, text, timestamp, + linkContext, } = params; const currentChat = selectCurrentChat(global, tabId); @@ -3461,6 +3467,7 @@ async function openChatByUsername( attach, text, timestamp, + linkContext, }, tabId); } @@ -3478,11 +3485,13 @@ async function openChatWithParams( attach?: string; text?: string; timestamp?: number; + linkContext?: LinkContext; }, ...[tabId = getCurrentTabId()]: TabArgs ) { const { isCurrentChat, threadId, messageId, startParam, referrer, startAttach, attach, text, timestamp, + linkContext, } = params; if (messageId) { @@ -3506,6 +3515,7 @@ async function openChatWithParams( if (!isTopicProcessed) { actions.focusMessage({ chatId: chat.id, threadId, messageId, timestamp, tabId, + replyMessageId: linkContext?.messageId, }); } } else if (!isCurrentChat) { diff --git a/src/global/actions/api/messages.ts b/src/global/actions/api/messages.ts index 007d93f77..c17f1c9bd 100644 --- a/src/global/actions/api/messages.ts +++ b/src/global/actions/api/messages.ts @@ -2372,7 +2372,7 @@ addActionHandler('readAllMentions', (global, actions, payload): ActionReturnType addActionHandler('openUrl', (global, actions, payload): ActionReturnType => { const { - url, shouldSkipModal, ignoreDeepLinks, tabId = getCurrentTabId(), + url, shouldSkipModal, ignoreDeepLinks, linkContext, tabId = getCurrentTabId(), } = payload; const urlWithProtocol = ensureProtocol(url); const parsedUrl = new URL(urlWithProtocol); @@ -2382,7 +2382,7 @@ addActionHandler('openUrl', (global, actions, payload): ActionReturnType => { actions.closeStoryViewer({ tabId }); actions.closePaymentModal({ tabId }); - actions.openTelegramLink({ url, tabId }); + actions.openTelegramLink({ url, linkContext, tabId }); return; } diff --git a/src/global/types/actions.ts b/src/global/types/actions.ts index 6891b3339..b9da16c7e 100644 --- a/src/global/types/actions.ts +++ b/src/global/types/actions.ts @@ -1,3 +1,4 @@ +import { LinkContext } from './../../api/types/messages'; import type { ApiAttachBot, ApiAttachment, @@ -55,6 +56,7 @@ import type { ApiUser, ApiVideo, BotsPrivacyType, + LinkContext, PrivacyVisibility, } from '../../api/types'; import type { ApiEmojiStatusCollectible, ApiEmojiStatusType } from '../../api/types/users'; @@ -652,6 +654,7 @@ export interface ActionPayloads { openTelegramLink: { url: string; shouldIgnoreCache?: boolean; + linkContext?: LinkContext; } & WithTabId; resolveBusinessChatLink: { slug: string; @@ -673,6 +676,7 @@ export interface ActionPayloads { originalParts?: (string | undefined)[]; timestamp?: number; onChatChanged?: CallbackAction; + linkContext?: LinkContext; } & WithTabId; processBoostParameters: { usernameOrId: string; @@ -1202,6 +1206,7 @@ export interface ActionPayloads { messageId?: number; commentId?: number; timestamp?: number; + linkContext?: LinkContext; } & WithTabId; loadFullChat: { chatId: string; @@ -2035,6 +2040,7 @@ export interface ActionPayloads { clickBotInlineButton: { chatId: string; messageId: number; + threadId?: ThreadId; button: ApiKeyboardButton; } & WithTabId; clickSuggestedMessageButton: { @@ -2290,6 +2296,7 @@ export interface ActionPayloads { url: string; shouldSkipModal?: boolean; ignoreDeepLinks?: boolean; + linkContext?: LinkContext; } & WithTabId; openMapModal: { geoPoint: ApiGeoPoint; diff --git a/src/util/deeplink.ts b/src/util/deeplink.ts index 791b4e89e..9f15c4ec9 100644 --- a/src/util/deeplink.ts +++ b/src/util/deeplink.ts @@ -1,6 +1,6 @@ import { getActions } from '../global'; -import type { ApiChatType, ApiFormattedText } from '../api/types'; +import type { ApiChatType, ApiFormattedText, LinkContext } from '../api/types'; import type { DeepLinkMethod } from './deepLinkParser'; import { LeftColumnContent, SettingsScreens } from '../types'; @@ -8,7 +8,7 @@ import { API_CHAT_TYPES, RE_TG_LINK, TON_CURRENCY_CODE } from '../config'; import { IS_BAD_URL_PARSER } from './browser/globalEnvironment'; import { tryParseDeepLink } from './deepLinkParser'; -export const processDeepLink = (url: string): boolean => { +export const processDeepLink = (url: string, linkContext?: LinkContext): boolean => { const actions = getActions(); const parsedLink = tryParseDeepLink(url); @@ -21,6 +21,7 @@ export const processDeepLink = (url: string): boolean => { messageId: parsedLink.messageId, commentId: parsedLink.commentId, timestamp: parsedLink.timestamp, + linkContext, }); return true; case 'publicMessageLink': { @@ -30,6 +31,7 @@ export const processDeepLink = (url: string): boolean => { messageId: parsedLink.messageId, commentId: parsedLink.commentId, timestamp: parsedLink.timestamp, + linkContext, }); return true; }