Message: Preserve focus stack for deeplinks within the same chat (#464)

This commit is contained in:
Shahaf 2025-09-05 19:34:40 +03:00 committed by GitHub
parent 08a4dd9117
commit 17fe96dff3
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
12 changed files with 69 additions and 17 deletions

View File

@ -1038,6 +1038,13 @@ export type ApiSearchPostsFlood = {
starsAmount: number; starsAmount: number;
}; };
export type LinkContext = {
type: 'message';
threadId?: ThreadId;
chatId: string;
messageId: number;
};
export const MAIN_THREAD_ID = -1; export const MAIN_THREAD_ID = -1;
// `Symbol` can not be transferred from worker // `Symbol` can not be transferred from worker

View File

@ -2256,6 +2256,7 @@ const Composer: FC<OwnProps & StateProps> = ({
{isInMessageList && Boolean(botKeyboardMessageId) && ( {isInMessageList && Boolean(botKeyboardMessageId) && (
<BotKeyboardMenu <BotKeyboardMenu
messageId={botKeyboardMessageId} messageId={botKeyboardMessageId}
threadId={threadId}
isOpen={isBotKeyboardOpen} isOpen={isBotKeyboardOpen}
onClose={closeBotKeyboard} onClose={closeBotKeyboard}
/> />

View File

@ -2,6 +2,7 @@ import type { TeactNode } from '../../lib/teact/teact';
import type React from '../../lib/teact/teact'; import type React from '../../lib/teact/teact';
import { getActions } from '../../global'; import { getActions } from '../../global';
import type { ThreadId } from '../../types';
import { ApiMessageEntityTypes } from '../../api/types'; import { ApiMessageEntityTypes } from '../../api/types';
import { ensureProtocol, getUnicodeUrl, isMixedScriptUrl } from '../../util/browser/url'; import { ensureProtocol, getUnicodeUrl, isMixedScriptUrl } from '../../util/browser/url';
@ -16,6 +17,9 @@ type OwnProps = {
children?: TeactNode; children?: TeactNode;
isRtl?: boolean; isRtl?: boolean;
shouldSkipModal?: boolean; shouldSkipModal?: boolean;
chatId?: string;
messageId?: number;
threadId?: ThreadId;
}; };
const SafeLink = ({ const SafeLink = ({
@ -25,6 +29,9 @@ const SafeLink = ({
children, children,
isRtl, isRtl,
shouldSkipModal, shouldSkipModal,
chatId,
messageId,
threadId,
}: OwnProps) => { }: OwnProps) => {
const { openUrl } = getActions(); const { openUrl } = getActions();
@ -37,7 +44,13 @@ const SafeLink = ({
e.preventDefault(); e.preventDefault();
const isTrustedLink = isRegularLink && !isMixedScriptUrl(url); 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; return false;
}); });

View File

@ -616,6 +616,9 @@ function processEntity({
<SafeLink <SafeLink
url={getLinkUrl(entityText, entity)} url={getLinkUrl(entityText, entity)}
text={entityText} text={entityText}
chatId={chatId}
messageId={messageId}
threadId={threadId}
> >
{renderNestedMessagePart()} {renderNestedMessagePart()}
</SafeLink> </SafeLink>

View File

@ -3,6 +3,7 @@ import { memo, useMemo } from '../../../lib/teact/teact';
import { getActions, withGlobal } from '../../../global'; import { getActions, withGlobal } from '../../../global';
import type { ApiMessage } from '../../../api/types'; import type { ApiMessage } from '../../../api/types';
import type { ThreadId } from '../../../types';
import { selectChatMessage, selectCurrentMessageList } from '../../../global/selectors'; import { selectChatMessage, selectCurrentMessageList } from '../../../global/selectors';
import { IS_TOUCH_ENV } from '../../../util/browser/windowEnvironment'; import { IS_TOUCH_ENV } from '../../../util/browser/windowEnvironment';
@ -24,10 +25,11 @@ export type OwnProps = {
type StateProps = { type StateProps = {
message?: ApiMessage; message?: ApiMessage;
threadId?: ThreadId;
}; };
const BotKeyboardMenu: FC<OwnProps & StateProps> = ({ const BotKeyboardMenu: FC<OwnProps & StateProps> = ({
isOpen, message, onClose, isOpen, message, onClose, threadId,
}) => { }) => {
const { clickBotInlineButton } = getActions(); const { clickBotInlineButton } = getActions();
@ -70,7 +72,9 @@ const BotKeyboardMenu: FC<OwnProps & StateProps> = ({
ripple ripple
disabled={button.type === 'unsupported'} 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]} {buttonTexts?.[i][j]}
</Button> </Button>

View File

@ -3,6 +3,7 @@ import { memo } from '../../../lib/teact/teact';
import { getActions } from '../../../global'; import { getActions } from '../../../global';
import type { ApiMessage } from '../../../api/types'; import type { ApiMessage } from '../../../api/types';
import type { ThreadId } from '../../../types';
import { getGamePreviewPhotoHash, getGamePreviewVideoHash, getMessageText } from '../../../global/helpers'; import { getGamePreviewPhotoHash, getGamePreviewVideoHash, getMessageText } from '../../../global/helpers';
@ -19,11 +20,13 @@ const DEFAULT_PREVIEW_DIMENSIONS = {
type OwnProps = { type OwnProps = {
message: ApiMessage; message: ApiMessage;
threadId?: ThreadId;
canAutoLoadMedia?: boolean; canAutoLoadMedia?: boolean;
}; };
const Game: FC<OwnProps> = ({ const Game: FC<OwnProps> = ({
message, message,
threadId,
canAutoLoadMedia, canAutoLoadMedia,
}) => { }) => {
const { clickBotInlineButton } = getActions(); const { clickBotInlineButton } = getActions();
@ -41,6 +44,7 @@ const Game: FC<OwnProps> = ({
clickBotInlineButton({ clickBotInlineButton({
chatId: message.chatId, chatId: message.chatId,
messageId: message.id, messageId: message.id,
threadId,
button: message.inlineButtons![0][0], button: message.inlineButtons![0][0],
}); });
}; };

View File

@ -1286,6 +1286,7 @@ const Message: FC<OwnProps & StateProps> = ({
{game && ( {game && (
<Game <Game
message={message} message={message}
threadId={threadId}
canAutoLoadMedia={canAutoLoadMedia} canAutoLoadMedia={canAutoLoadMedia}
/> />
)} )}

View File

@ -1,4 +1,4 @@
import type { InlineBotSettings } from '../../../types'; import type { InlineBotSettings, ThreadId } from '../../../types';
import type { WebApp } from '../../../types/webapp'; import type { WebApp } from '../../../types/webapp';
import type { RequiredGlobalActions } from '../../index'; import type { RequiredGlobalActions } from '../../index';
import type { import type {
@ -95,7 +95,7 @@ addActionHandler('clickSuggestedMessageButton', (global, actions, payload): Acti
addActionHandler('clickBotInlineButton', (global, actions, payload): ActionReturnType => { addActionHandler('clickBotInlineButton', (global, actions, payload): ActionReturnType => {
const { const {
chatId, messageId, button, tabId = getCurrentTabId(), chatId, messageId, threadId, button, tabId = getCurrentTabId(),
} = payload; } = payload;
const chat = selectChat(global, chatId); const chat = selectChat(global, chatId);
const message = selectChatMessage(global, chatId, messageId); const message = selectChatMessage(global, chatId, messageId);
@ -109,7 +109,7 @@ addActionHandler('clickBotInlineButton', (global, actions, payload): ActionRetur
break; break;
case 'url': { case 'url': {
const { url } = button; const { url } = button;
actions.openUrl({ url, tabId }); actions.openUrl({ url, tabId, linkContext: { type: 'message', chatId, messageId, threadId } });
break; break;
} }
case 'copy': { case 'copy': {
@ -118,7 +118,7 @@ addActionHandler('clickBotInlineButton', (global, actions, payload): ActionRetur
break; break;
} }
case 'callback': { case 'callback': {
void answerCallbackButton(global, actions, chat, messageId, button.data, undefined, tabId); void answerCallbackButton(global, actions, chat, messageId, threadId, button.data, undefined, tabId);
break; break;
} }
case 'requestPoll': case 'requestPoll':
@ -157,7 +157,7 @@ addActionHandler('clickBotInlineButton', (global, actions, payload): ActionRetur
break; break;
} }
case 'game': { case 'game': {
void answerCallbackButton(global, actions, chat, messageId, undefined, true, tabId); void answerCallbackButton(global, actions, chat, messageId, threadId, undefined, true, tabId);
break; break;
} }
case 'switchBotInline': { case 'switchBotInline': {
@ -1287,7 +1287,7 @@ async function sendBotCommand(
async function answerCallbackButton<T extends GlobalState>( async function answerCallbackButton<T extends GlobalState>(
global: T, 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<T> ...[tabId = getCurrentTabId()]: TabArgs<T>
) { ) {
const { const {
@ -1317,7 +1317,7 @@ async function answerCallbackButton<T extends GlobalState>(
url, chatId: chat.id, messageId, tabId, url, chatId: chat.id, messageId, tabId,
}); });
} else { } else {
openUrl({ url, tabId }); openUrl({ url, tabId, linkContext: { type: 'message', chatId: chat.id, messageId, threadId } });
} }
} }
} }

View File

@ -2,6 +2,7 @@ import type {
ApiChat, ApiChatFolder, ApiChatlistExportedInvite, ApiChat, ApiChatFolder, ApiChatlistExportedInvite,
ApiChatMember, ApiError, ApiMissingInvitedUser, ApiChatMember, ApiError, ApiMissingInvitedUser,
ApiTopic, ApiTopic,
LinkContext,
} from '../../../api/types'; } from '../../../api/types';
import type { RequiredGlobalActions } from '../../index'; import type { RequiredGlobalActions } from '../../index';
import type { import type {
@ -1458,6 +1459,7 @@ addActionHandler('openTelegramLink', async (global, actions, payload): Promise<v
const { const {
url, url,
shouldIgnoreCache, shouldIgnoreCache,
linkContext,
tabId = getCurrentTabId(), tabId = getCurrentTabId(),
} = payload; } = payload;
@ -1475,7 +1477,7 @@ addActionHandler('openTelegramLink', async (global, actions, payload): Promise<v
} = actions; } = actions;
if (isDeepLink(url)) { if (isDeepLink(url)) {
const isProcessed = processDeepLink(url); const isProcessed = processDeepLink(url, linkContext);
if (isProcessed || url.match(RE_TG_LINK)) { if (isProcessed || url.match(RE_TG_LINK)) {
return; return;
} }
@ -1653,7 +1655,7 @@ addActionHandler('openChatByUsername', async (global, actions, payload): Promise
const { const {
username, messageId, commentId, startParam, startAttach, attach, threadId, originalParts, username, messageId, commentId, startParam, startAttach, attach, threadId, originalParts,
startApp, shouldStartMainApp, mode, startApp, shouldStartMainApp, mode,
text, onChatChanged, choose, ref, timestamp, text, onChatChanged, choose, ref, timestamp, linkContext,
tabId = getCurrentTabId(), tabId = getCurrentTabId(),
} = payload; } = payload;
@ -1708,6 +1710,7 @@ addActionHandler('openChatByUsername', async (global, actions, payload): Promise
attach, attach,
text, text,
timestamp, timestamp,
linkContext,
}, tabId, }, tabId,
); );
if (onChatChanged) { if (onChatChanged) {
@ -1785,7 +1788,7 @@ addActionHandler('openChatByUsername', async (global, actions, payload): Promise
addActionHandler('openPrivateChannel', (global, actions, payload): ActionReturnType => { addActionHandler('openPrivateChannel', (global, actions, payload): ActionReturnType => {
const { const {
id, commentId, messageId, threadId, timestamp, tabId = getCurrentTabId(), id, commentId, messageId, threadId, timestamp, linkContext, tabId = getCurrentTabId(),
} = payload; } = payload;
const chat = selectChat(global, id); const chat = selectChat(global, id);
if (!chat) { if (!chat) {
@ -1827,6 +1830,7 @@ addActionHandler('openPrivateChannel', (global, actions, payload): ActionReturnT
messageId, messageId,
threadId, threadId,
timestamp, timestamp,
linkContext,
}, tabId); }, tabId);
}); });
@ -3401,11 +3405,13 @@ async function openChatByUsername<T extends GlobalState>(
attach?: string; attach?: string;
text?: string; text?: string;
timestamp?: number; timestamp?: number;
linkContext?: LinkContext;
}, },
...[tabId = getCurrentTabId()]: TabArgs<T> ...[tabId = getCurrentTabId()]: TabArgs<T>
) { ) {
const { const {
username, threadId, channelPostId, startParam, ref, startAttach, attach, text, timestamp, username, threadId, channelPostId, startParam, ref, startAttach, attach, text, timestamp,
linkContext,
} = params; } = params;
const currentChat = selectCurrentChat(global, tabId); const currentChat = selectCurrentChat(global, tabId);
@ -3461,6 +3467,7 @@ async function openChatByUsername<T extends GlobalState>(
attach, attach,
text, text,
timestamp, timestamp,
linkContext,
}, tabId); }, tabId);
} }
@ -3478,11 +3485,13 @@ async function openChatWithParams<T extends GlobalState>(
attach?: string; attach?: string;
text?: string; text?: string;
timestamp?: number; timestamp?: number;
linkContext?: LinkContext;
}, },
...[tabId = getCurrentTabId()]: TabArgs<T> ...[tabId = getCurrentTabId()]: TabArgs<T>
) { ) {
const { const {
isCurrentChat, threadId, messageId, startParam, referrer, startAttach, attach, text, timestamp, isCurrentChat, threadId, messageId, startParam, referrer, startAttach, attach, text, timestamp,
linkContext,
} = params; } = params;
if (messageId) { if (messageId) {
@ -3506,6 +3515,7 @@ async function openChatWithParams<T extends GlobalState>(
if (!isTopicProcessed) { if (!isTopicProcessed) {
actions.focusMessage({ actions.focusMessage({
chatId: chat.id, threadId, messageId, timestamp, tabId, chatId: chat.id, threadId, messageId, timestamp, tabId,
replyMessageId: linkContext?.messageId,
}); });
} }
} else if (!isCurrentChat) { } else if (!isCurrentChat) {

View File

@ -2372,7 +2372,7 @@ addActionHandler('readAllMentions', (global, actions, payload): ActionReturnType
addActionHandler('openUrl', (global, actions, payload): ActionReturnType => { addActionHandler('openUrl', (global, actions, payload): ActionReturnType => {
const { const {
url, shouldSkipModal, ignoreDeepLinks, tabId = getCurrentTabId(), url, shouldSkipModal, ignoreDeepLinks, linkContext, tabId = getCurrentTabId(),
} = payload; } = payload;
const urlWithProtocol = ensureProtocol(url); const urlWithProtocol = ensureProtocol(url);
const parsedUrl = new URL(urlWithProtocol); const parsedUrl = new URL(urlWithProtocol);
@ -2382,7 +2382,7 @@ addActionHandler('openUrl', (global, actions, payload): ActionReturnType => {
actions.closeStoryViewer({ tabId }); actions.closeStoryViewer({ tabId });
actions.closePaymentModal({ tabId }); actions.closePaymentModal({ tabId });
actions.openTelegramLink({ url, tabId }); actions.openTelegramLink({ url, linkContext, tabId });
return; return;
} }

View File

@ -1,3 +1,4 @@
import { LinkContext } from './../../api/types/messages';
import type { import type {
ApiAttachBot, ApiAttachBot,
ApiAttachment, ApiAttachment,
@ -55,6 +56,7 @@ import type {
ApiUser, ApiUser,
ApiVideo, ApiVideo,
BotsPrivacyType, BotsPrivacyType,
LinkContext,
PrivacyVisibility, PrivacyVisibility,
} from '../../api/types'; } from '../../api/types';
import type { ApiEmojiStatusCollectible, ApiEmojiStatusType } from '../../api/types/users'; import type { ApiEmojiStatusCollectible, ApiEmojiStatusType } from '../../api/types/users';
@ -652,6 +654,7 @@ export interface ActionPayloads {
openTelegramLink: { openTelegramLink: {
url: string; url: string;
shouldIgnoreCache?: boolean; shouldIgnoreCache?: boolean;
linkContext?: LinkContext;
} & WithTabId; } & WithTabId;
resolveBusinessChatLink: { resolveBusinessChatLink: {
slug: string; slug: string;
@ -673,6 +676,7 @@ export interface ActionPayloads {
originalParts?: (string | undefined)[]; originalParts?: (string | undefined)[];
timestamp?: number; timestamp?: number;
onChatChanged?: CallbackAction; onChatChanged?: CallbackAction;
linkContext?: LinkContext;
} & WithTabId; } & WithTabId;
processBoostParameters: { processBoostParameters: {
usernameOrId: string; usernameOrId: string;
@ -1202,6 +1206,7 @@ export interface ActionPayloads {
messageId?: number; messageId?: number;
commentId?: number; commentId?: number;
timestamp?: number; timestamp?: number;
linkContext?: LinkContext;
} & WithTabId; } & WithTabId;
loadFullChat: { loadFullChat: {
chatId: string; chatId: string;
@ -2035,6 +2040,7 @@ export interface ActionPayloads {
clickBotInlineButton: { clickBotInlineButton: {
chatId: string; chatId: string;
messageId: number; messageId: number;
threadId?: ThreadId;
button: ApiKeyboardButton; button: ApiKeyboardButton;
} & WithTabId; } & WithTabId;
clickSuggestedMessageButton: { clickSuggestedMessageButton: {
@ -2290,6 +2296,7 @@ export interface ActionPayloads {
url: string; url: string;
shouldSkipModal?: boolean; shouldSkipModal?: boolean;
ignoreDeepLinks?: boolean; ignoreDeepLinks?: boolean;
linkContext?: LinkContext;
} & WithTabId; } & WithTabId;
openMapModal: { openMapModal: {
geoPoint: ApiGeoPoint; geoPoint: ApiGeoPoint;

View File

@ -1,6 +1,6 @@
import { getActions } from '../global'; 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 type { DeepLinkMethod } from './deepLinkParser';
import { LeftColumnContent, SettingsScreens } from '../types'; 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 { IS_BAD_URL_PARSER } from './browser/globalEnvironment';
import { tryParseDeepLink } from './deepLinkParser'; import { tryParseDeepLink } from './deepLinkParser';
export const processDeepLink = (url: string): boolean => { export const processDeepLink = (url: string, linkContext?: LinkContext): boolean => {
const actions = getActions(); const actions = getActions();
const parsedLink = tryParseDeepLink(url); const parsedLink = tryParseDeepLink(url);
@ -21,6 +21,7 @@ export const processDeepLink = (url: string): boolean => {
messageId: parsedLink.messageId, messageId: parsedLink.messageId,
commentId: parsedLink.commentId, commentId: parsedLink.commentId,
timestamp: parsedLink.timestamp, timestamp: parsedLink.timestamp,
linkContext,
}); });
return true; return true;
case 'publicMessageLink': { case 'publicMessageLink': {
@ -30,6 +31,7 @@ export const processDeepLink = (url: string): boolean => {
messageId: parsedLink.messageId, messageId: parsedLink.messageId,
commentId: parsedLink.commentId, commentId: parsedLink.commentId,
timestamp: parsedLink.timestamp, timestamp: parsedLink.timestamp,
linkContext,
}); });
return true; return true;
} }