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;
};
export type LinkContext = {
type: 'message';
threadId?: ThreadId;
chatId: string;
messageId: number;
};
export const MAIN_THREAD_ID = -1;
// `Symbol` can not be transferred from worker

View File

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

View File

@ -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;
});

View File

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

View File

@ -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<OwnProps & StateProps> = ({
isOpen, message, onClose,
isOpen, message, onClose, threadId,
}) => {
const { clickBotInlineButton } = getActions();
@ -70,7 +72,9 @@ const BotKeyboardMenu: FC<OwnProps & StateProps> = ({
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]}
</Button>

View File

@ -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<OwnProps> = ({
message,
threadId,
canAutoLoadMedia,
}) => {
const { clickBotInlineButton } = getActions();
@ -41,6 +44,7 @@ const Game: FC<OwnProps> = ({
clickBotInlineButton({
chatId: message.chatId,
messageId: message.id,
threadId,
button: message.inlineButtons![0][0],
});
};

View File

@ -1286,6 +1286,7 @@ const Message: FC<OwnProps & StateProps> = ({
{game && (
<Game
message={message}
threadId={threadId}
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 { 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<T extends GlobalState>(
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>
) {
const {
@ -1317,7 +1317,7 @@ async function answerCallbackButton<T extends GlobalState>(
url, chatId: chat.id, messageId, tabId,
});
} 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,
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<v
const {
url,
shouldIgnoreCache,
linkContext,
tabId = getCurrentTabId(),
} = payload;
@ -1475,7 +1477,7 @@ addActionHandler('openTelegramLink', async (global, actions, payload): Promise<v
} = actions;
if (isDeepLink(url)) {
const isProcessed = processDeepLink(url);
const isProcessed = processDeepLink(url, linkContext);
if (isProcessed || url.match(RE_TG_LINK)) {
return;
}
@ -1653,7 +1655,7 @@ addActionHandler('openChatByUsername', async (global, actions, payload): Promise
const {
username, messageId, commentId, startParam, startAttach, attach, threadId, originalParts,
startApp, shouldStartMainApp, mode,
text, onChatChanged, choose, ref, timestamp,
text, onChatChanged, choose, ref, timestamp, linkContext,
tabId = getCurrentTabId(),
} = payload;
@ -1708,6 +1710,7 @@ addActionHandler('openChatByUsername', async (global, actions, payload): Promise
attach,
text,
timestamp,
linkContext,
}, tabId,
);
if (onChatChanged) {
@ -1785,7 +1788,7 @@ addActionHandler('openChatByUsername', async (global, actions, payload): Promise
addActionHandler('openPrivateChannel', (global, actions, payload): ActionReturnType => {
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<T extends GlobalState>(
attach?: string;
text?: string;
timestamp?: number;
linkContext?: LinkContext;
},
...[tabId = getCurrentTabId()]: TabArgs<T>
) {
const {
username, threadId, channelPostId, startParam, ref, startAttach, attach, text, timestamp,
linkContext,
} = params;
const currentChat = selectCurrentChat(global, tabId);
@ -3461,6 +3467,7 @@ async function openChatByUsername<T extends GlobalState>(
attach,
text,
timestamp,
linkContext,
}, tabId);
}
@ -3478,11 +3485,13 @@ async function openChatWithParams<T extends GlobalState>(
attach?: string;
text?: string;
timestamp?: number;
linkContext?: LinkContext;
},
...[tabId = getCurrentTabId()]: TabArgs<T>
) {
const {
isCurrentChat, threadId, messageId, startParam, referrer, startAttach, attach, text, timestamp,
linkContext,
} = params;
if (messageId) {
@ -3506,6 +3515,7 @@ async function openChatWithParams<T extends GlobalState>(
if (!isTopicProcessed) {
actions.focusMessage({
chatId: chat.id, threadId, messageId, timestamp, tabId,
replyMessageId: linkContext?.messageId,
});
}
} else if (!isCurrentChat) {

View File

@ -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;
}

View File

@ -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;

View File

@ -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;
}