From b8f06077005d2dbc07a0ebf2ddb936cd21e7efd2 Mon Sep 17 00:00:00 2001 From: Alexander Zinchuk Date: Fri, 12 Jan 2024 13:00:32 +0100 Subject: [PATCH] DeepLink: Open private message link (#4157) --- src/global/actions/api/chats.ts | 13 +++--- src/util/deepLinkParser.ts | 52 +++++++++++++++-------- src/util/deeplink.ts | 73 ++++++++++++++++++++------------- 3 files changed, 87 insertions(+), 51 deletions(-) diff --git a/src/global/actions/api/chats.ts b/src/global/actions/api/chats.ts index d26c8ab78..de0f24759 100644 --- a/src/global/actions/api/chats.ts +++ b/src/global/actions/api/chats.ts @@ -28,6 +28,7 @@ import { TOPICS_SLICE_SECOND_LOAD, } from '../../../config'; import { formatShareText, parseChooseParameter, processDeepLink } from '../../../util/deeplink'; +import { isDeepLink } from '../../../util/deepLinkParser'; import { getCurrentTabId } from '../../../util/establishMultitabRole'; import { getOrderedIds } from '../../../util/folderManager'; import { buildCollectionByKey, omit, pick } from '../../../util/iteratees'; @@ -1167,6 +1168,13 @@ addActionHandler('openTelegramLink', (global, actions, payload): ActionReturnTyp tabId = getCurrentTabId(), } = payload; + if (isDeepLink(url)) { + const isProcessed = processDeepLink(url); + if (isProcessed || url.match(RE_TG_LINK)) { + return; + } + } + const { openChatByPhoneNumber, openChatByInvite, @@ -1184,11 +1192,6 @@ addActionHandler('openTelegramLink', (global, actions, payload): ActionReturnTyp checkGiftCode, } = actions; - if (url.match(RE_TG_LINK)) { - processDeepLink(url); - return; - } - const uri = new URL(url.toLowerCase().startsWith('http') ? url : `https://${url}`); if (TME_WEB_DOMAINS.has(uri.hostname) && uri.pathname === '/') { window.open(uri.toString(), '_blank', 'noopener'); diff --git a/src/util/deepLinkParser.ts b/src/util/deepLinkParser.ts index cf1b1d536..1af5f2f0f 100644 --- a/src/util/deepLinkParser.ts +++ b/src/util/deepLinkParser.ts @@ -9,20 +9,22 @@ interface PublicMessageLink { type: 'publicMessageLink'; username: string; messageId: number; - isSingle?: boolean; + isSingle: boolean; threadId?: number; commentId?: number; mediaTimestamp?: string; + isBoost: boolean; } -interface PrivateMessageLink { +export interface PrivateMessageLink { type: 'privateMessageLink'; channelId: string; messageId: number; - isSingle?: boolean; + isSingle: boolean; threadId?: number; commentId?: number; mediaTimestamp?: string; + isBoost: boolean; } interface ShareLink { @@ -63,6 +65,16 @@ type BuilderParams = Record, string>; type BuilderReturnType = T | undefined; type DeepLinkType = DeepLink['type'] | 'unknown'; +type PrivateMessageLinkBuilderParams = Omit, 'isSingle' | 'isBoost'> & { + single: string; + boost: string; +}; + +type PublicMessageLinkBuilderParams = Omit, 'isSingle' | 'isBoost'> & { + single: string; + boost: string; +}; + const ELIGIBLE_HOSTNAMES = new Set(['t.me', 'telegram.me', 'telegram.dog']); export function isDeepLink(link: string): boolean { @@ -100,28 +112,30 @@ function handleTgLink(url: URL) { switch (deepLinkType) { case 'publicMessageLink': { const { - domain, post, single, thread, comment, t, + domain, post, single, thread, comment, t, boost, } = queryParams; return buildPublicMessageLink({ username: domain, messageId: post, - isSingle: single, + single, threadId: thread, commentId: comment, mediaTimestamp: t, + boost, }); } case 'privateMessageLink': { const { - channel, post, single, thread, comment, t, + channel, post, single, thread, comment, t, boost, } = queryParams; return buildPrivateMessageLink({ channelId: channel, messageId: post, - isSingle: single, + single, threadId: thread, commentId: comment, mediaTimestamp: t, + boost, }); } case 'shareLink': @@ -156,7 +170,7 @@ function handleHttpLink(url: URL) { switch (deepLinkType) { case 'publicMessageLink': { const { - single, comment, t, + single, comment, t, boost, } = queryParams; const { username, @@ -174,15 +188,16 @@ function handleHttpLink(url: URL) { return buildPublicMessageLink({ username, messageId, - isSingle: single, + single, threadId: thread, commentId: comment, mediaTimestamp: t, + boost, }); } case 'privateMessageLink': { const { - single, comment, t, + single, comment, t, boost, } = queryParams; const { channelId, @@ -200,10 +215,11 @@ function handleHttpLink(url: URL) { return buildPrivateMessageLink({ channelId, messageId, - isSingle: single, + single, threadId: thread, commentId: comment, mediaTimestamp: t, + boost, }); } case 'shareLink': { @@ -306,9 +322,9 @@ function buildShareLink(params: BuilderParams): BuilderReturnType): BuilderReturnType { +function buildPublicMessageLink(params: PublicMessageLinkBuilderParams): BuilderReturnType { const { - messageId, threadId, commentId, username, isSingle, mediaTimestamp, + messageId, threadId, commentId, username, single, mediaTimestamp, boost, } = params; if (!username || !isUsernameValid(username)) { return undefined; @@ -326,16 +342,17 @@ function buildPublicMessageLink(params: BuilderParams): Build type: 'publicMessageLink', username, messageId: Number(messageId), - isSingle: isSingle === '', + isSingle: single === '', threadId: threadId ? Number(threadId) : undefined, commentId: commentId ? Number(commentId) : undefined, mediaTimestamp, + isBoost: boost === '', }; } -function buildPrivateMessageLink(params: BuilderParams): BuilderReturnType { +function buildPrivateMessageLink(params: PrivateMessageLinkBuilderParams): BuilderReturnType { const { - messageId, threadId, commentId, channelId, isSingle, mediaTimestamp, + messageId, threadId, commentId, channelId, single, mediaTimestamp, boost, } = params; if (!channelId || !isNumber(channelId)) { return undefined; @@ -353,10 +370,11 @@ function buildPrivateMessageLink(params: BuilderParams): Bui type: 'privateMessageLink', channelId, messageId: Number(messageId), - isSingle: isSingle === '', + isSingle: single === '', threadId: threadId ? Number(threadId) : undefined, commentId: commentId ? Number(commentId) : undefined, mediaTimestamp, + isBoost: boost === '', }; } diff --git a/src/util/deeplink.ts b/src/util/deeplink.ts index 251d8ccaf..2020560a1 100644 --- a/src/util/deeplink.ts +++ b/src/util/deeplink.ts @@ -1,24 +1,42 @@ import { getActions } from '../global'; import type { ApiChatType } from '../api/types'; -import type { DeepLinkMethod } from './deepLinkParser'; +import type { DeepLinkMethod, PrivateMessageLink } from './deepLinkParser'; import { API_CHAT_TYPES } from '../config'; +import { toChannelId } from '../global/helpers'; +import { tryParseDeepLink } from './deepLinkParser'; import { IS_SAFARI } from './windowEnvironment'; -export const processDeepLink = (url: string) => { +export const processDeepLink = (url: string): boolean => { + const actions = getActions(); + + const parsedLink = tryParseDeepLink(url); + if (parsedLink) { + switch (parsedLink.type) { + case 'privateMessageLink': + handlePrivateMessageLink(parsedLink, actions); + return true; + default: + break; + } + } + const { protocol, searchParams, pathname, hostname, } = new URL(url); - if (protocol !== 'tg:') return; + if (protocol !== 'tg:') return false; + + // Safari thinks the path in tg://path links is hostname for some reason + const method = (IS_SAFARI ? hostname : pathname).replace(/^\/\//, '') as DeepLinkMethod; + const params = Object.fromEntries(searchParams); const { openChatByInvite, openChatByUsername, openChatByPhoneNumber, openStickerSet, - focusMessage, joinVoiceChatByLink, openInvoice, processAttachBotParameters, @@ -27,11 +45,7 @@ export const processDeepLink = (url: string) => { openStoryViewerByUsername, processBoostParameters, checkGiftCode, - } = getActions(); - - // Safari thinks the path in tg://path links is hostname for some reason - const method = (IS_SAFARI ? hostname : pathname).replace(/^\/\//, '') as DeepLinkMethod; - const params = Object.fromEntries(searchParams); + } = actions; switch (method) { case 'resolve': { @@ -84,24 +98,6 @@ export const processDeepLink = (url: string) => { } break; } - case 'privatepost': { - const { - post, channel, - } = params; - - const hasBoost = params.hasOwnProperty('boost'); - - if (hasBoost) { - processBoostParameters({ usernameOrId: channel, isPrivate: true }); - return; - } - - focusMessage({ - chatId: `-${channel}`, - messageId: Number(post), - }); - break; - } case 'bg': { // const { // slug, color, rotation, mode, intensity, bg_color: bgColor, gradient, @@ -163,9 +159,9 @@ export const processDeepLink = (url: string) => { } default: // Unsupported deeplink - - break; + return false; } + return true; }; export function parseChooseParameter(choose?: string) { @@ -177,3 +173,22 @@ export function parseChooseParameter(choose?: string) { export function formatShareText(url?: string, text?: string, title?: string): string { return [url, title, text].filter(Boolean).join('\n'); } + +function handlePrivateMessageLink(link: PrivateMessageLink, actions: ReturnType) { + const { + focusMessage, + processBoostParameters, + } = actions; + const { + isBoost, channelId, messageId, threadId, + } = link; + if (isBoost) { + processBoostParameters({ usernameOrId: channelId, isPrivate: true }); + return; + } + focusMessage({ + chatId: toChannelId(channelId), + threadId, + messageId, + }); +}