From a3f57d29825a42e342bb7bd304492cae527f47ae Mon Sep 17 00:00:00 2001 From: zubiden <19638254+zubiden@users.noreply.github.com> Date: Sun, 29 Dec 2024 11:58:24 +0100 Subject: [PATCH] Deep Link: Rewrite boost link handling (#5330) --- package-lock.json | 16 ++- package.json | 1 + src/assets/localization/fallback.strings | 3 +- src/global/actions/api/chats.ts | 35 +----- src/global/actions/ui/chats.ts | 16 +++ src/global/types.ts | 3 + src/types/language.d.ts | 1 + src/util/deepLinkParser.ts | 131 ++++++++++++++++------- src/util/deeplink.ts | 31 +++--- 9 files changed, 146 insertions(+), 91 deletions(-) diff --git a/package-lock.json b/package-lock.json index 85549f15b..953852442 100644 --- a/package-lock.json +++ b/package-lock.json @@ -82,6 +82,7 @@ "eslint-plugin-react-hooks-static-deps": "^1.0.7", "eslint-plugin-simple-import-sort": "^12.1.0", "eslint-plugin-teactn": "git+https://github.com/korenskoy/eslint-plugin-teactn#c2c39dd005d58c07c24c4361de804dce1c6261b5", + "fake-indexeddb": "^6.0.0", "git-revision-webpack-plugin": "^5.0.0", "gitlog": "^4.0.8", "html-webpack-plugin": "^5.6.0", @@ -7949,9 +7950,9 @@ } }, "node_modules/caniuse-lite": { - "version": "1.0.30001616", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001616.tgz", - "integrity": "sha512-RHVYKov7IcdNjVHJFNY/78RdG4oGVjbayxv8u5IO74Wv7Hlq4PnJE6mo/OjFijjVFNy5ijnCt6H3IIo4t+wfEw==", + "version": "1.0.30001689", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001689.tgz", + "integrity": "sha512-CmeR2VBycfa+5/jOfnp/NpWPGd06nf1XYiefUvhXFfZE4GkRc9jv+eGPS4nT558WS/8lYCzV8SlANCIPvbWP1g==", "dev": true, "funding": [ { @@ -12252,6 +12253,15 @@ ], "optional": true }, + "node_modules/fake-indexeddb": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/fake-indexeddb/-/fake-indexeddb-6.0.0.tgz", + "integrity": "sha512-YEboHE5VfopUclOck7LncgIqskAqnv4q0EWbYCaxKKjAvO93c+TJIaBuGy8CBFdbg9nKdpN3AuPRwVBJ4k7NrQ==", + "dev": true, + "engines": { + "node": ">=18" + } + }, "node_modules/fast-deep-equal": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", diff --git a/package.json b/package.json index 205935b95..de8aff602 100644 --- a/package.json +++ b/package.json @@ -104,6 +104,7 @@ "eslint-plugin-react-hooks-static-deps": "^1.0.7", "eslint-plugin-simple-import-sort": "^12.1.0", "eslint-plugin-teactn": "git+https://github.com/korenskoy/eslint-plugin-teactn#c2c39dd005d58c07c24c4361de804dce1c6261b5", + "fake-indexeddb": "^6.0.0", "git-revision-webpack-plugin": "^5.0.0", "gitlog": "^4.0.8", "html-webpack-plugin": "^5.6.0", diff --git a/src/assets/localization/fallback.strings b/src/assets/localization/fallback.strings index 9f6180170..c31d37c8d 100644 --- a/src/assets/localization/fallback.strings +++ b/src/assets/localization/fallback.strings @@ -987,7 +987,8 @@ "StickerPackErrorNotFound" = "Sorry, this sticker set doesn't seem to exist."; "ContactsPhoneNumberNotRegistred" = "The person with this phone number is not registered on Telegram yet."; "VoipPeerIncompatible" = "**{user}**'s app is using an incompatible protocol. They need to update their app before you can call them."; -"NoUsernameFound" = "Username not found."; +"NoUsernameFound" = "Username not found"; +"PrivateChannelInaccessible" = "Unfortunately, you can't access this chat. You need to be a member to do that."; "HiddenName" = "Deleted Account"; "ChannelPersmissionDeniedSendMessagesForever" = "The admins of this group have restricted your ability to send messages."; "ChannelPersmissionDeniedSendMessagesDefaultRestrictedText" = "Sending messages is not allowed in this group."; diff --git a/src/global/actions/api/chats.ts b/src/global/actions/api/chats.ts index efb63a124..1ffbbe6c2 100644 --- a/src/global/actions/api/chats.ts +++ b/src/global/actions/api/chats.ts @@ -49,7 +49,6 @@ import { isChatChannel, isChatSuperGroup, isUserBot, - toChannelId, } from '../../helpers'; import { addActionHandler, getGlobal, setGlobal, @@ -1280,12 +1279,10 @@ addActionHandler('openTelegramLink', async (global, actions, payload): Promise { + const { id, tabId = getCurrentTabId() } = payload; + const chat = selectChat(global, id); + if (!chat) { + actions.showNotification({ + message: { + key: 'PrivateChannelInaccessible', + }, + tabId, + }); + return; + } + + actions.openChat({ id, tabId }); +}); + addActionHandler('openChatInNewTab', (global, actions, payload): ActionReturnType => { const { chatId, threadId = MAIN_THREAD_ID } = payload; diff --git a/src/global/types.ts b/src/global/types.ts index 090b4fc28..b49c222c1 100644 --- a/src/global/types.ts +++ b/src/global/types.ts @@ -2330,6 +2330,9 @@ export interface ActionPayloads { noForumTopicPanel?: boolean; isComments?: boolean; } & WithTabId; + openPrivateChannel: { + id: string; + } & WithTabId; loadFullChat: { chatId: string; withPhotos?: boolean; diff --git a/src/types/language.d.ts b/src/types/language.d.ts index 3294e230c..4a469f8f3 100644 --- a/src/types/language.d.ts +++ b/src/types/language.d.ts @@ -850,6 +850,7 @@ export interface LangPair { 'StickerPackErrorNotFound': undefined; 'ContactsPhoneNumberNotRegistred': undefined; 'NoUsernameFound': undefined; + 'PrivateChannelInaccessible': undefined; 'HiddenName': undefined; 'ChannelPersmissionDeniedSendMessagesForever': undefined; 'ChannelPersmissionDeniedSendMessagesDefaultRestrictedText': undefined; diff --git a/src/util/deepLinkParser.ts b/src/util/deepLinkParser.ts index 45c4ab05e..3fe62b714 100644 --- a/src/util/deepLinkParser.ts +++ b/src/util/deepLinkParser.ts @@ -1,6 +1,7 @@ import type { ThreadId } from '../types'; import { RE_TG_LINK, RE_TME_LINK } from '../config'; +import { toChannelId } from '../global/helpers'; import { ensureProtocol } from './ensureProtocol'; import { isUsernameValid } from './username'; @@ -16,7 +17,6 @@ interface PublicMessageLink { threadId?: ThreadId; commentId?: number; mediaTimestamp?: string; - isBoost: boolean; } export interface PrivateMessageLink { @@ -27,7 +27,6 @@ export interface PrivateMessageLink { threadId?: ThreadId; commentId?: number; mediaTimestamp?: string; - isBoost: boolean; } interface ShareLink { @@ -70,6 +69,17 @@ interface PublicUsernameOrBotLink { choose?: string; } +interface PrivateChannelLink { + type: 'privateChannelLink'; + channelId: string; +} + +interface ChatBoostLink { + type: 'chatBoostLink'; + username?: string; + id?: string; +} + interface BusinessChatLink { type: 'businessChatLink'; slug: string; @@ -93,22 +103,22 @@ type DeepLink = ShareLink | ChatFolderLink | PublicUsernameOrBotLink | + PrivateChannelLink | BusinessChatLink | PremiumReferrerLink | - PremiumMultigiftLink; + PremiumMultigiftLink | + ChatBoostLink; type BuilderParams = Record, string | undefined>; type BuilderReturnType = T | undefined; type DeepLinkType = DeepLink['type'] | 'unknown'; -type PrivateMessageLinkBuilderParams = Omit, 'isSingle' | 'isBoost'> & { - single: string | undefined; - boost: string | undefined; +type PrivateMessageLinkBuilderParams = Omit, 'isSingle'> & { + single?: string; }; -type PublicMessageLinkBuilderParams = Omit, 'isSingle' | 'isBoost'> & { - single: string | undefined; - boost: string | undefined; +type PublicMessageLinkBuilderParams = Omit, 'isSingle'> & { + single?: string; }; const ELIGIBLE_HOSTNAMES = new Set(['t.me', 'telegram.me', 'telegram.dog']); @@ -133,7 +143,7 @@ function parseDeepLink(url: string) { if (!correctUrl) { return undefined; } - if (correctUrl.startsWith('https:')) { + if (correctUrl.startsWith('https:') || correctUrl.startsWith('http:')) { const urlParsed = new URL(correctUrl); return parseHttpLink(urlParsed); } @@ -155,7 +165,7 @@ function parseTgLink(url: URL) { switch (deepLinkType) { case 'publicMessageLink': { const { - domain, post, single, thread, comment, t, boost, + domain, post, single, thread, comment, t, } = queryParams; return buildPublicMessageLink({ username: domain, @@ -164,12 +174,11 @@ function parseTgLink(url: URL) { threadId: thread, commentId: comment, mediaTimestamp: t, - boost, }); } case 'privateMessageLink': { const { - channel, post, single, thread, comment, t, boost, + channel, post, single, thread, comment, t, } = queryParams; return buildPrivateMessageLink({ channelId: channel, @@ -178,7 +187,6 @@ function parseTgLink(url: URL) { threadId: thread, commentId: comment, mediaTimestamp: t, - boost, }); } case 'shareLink': @@ -209,12 +217,17 @@ function parseTgLink(url: URL) { choose: queryParams.choose, ref: queryParams.ref, }); + case 'privateChannelLink': { + return buildPrivateChannelLink({ channelId: queryParams.channel }); + } case 'businessChatLink': return buildBusinessChatLink({ slug: queryParams.slug }); case 'premiumReferrerLink': return buildPremiumReferrerLink({ referrer: queryParams.ref }); case 'premiumMultigiftLink': return buildPremiumMultigiftLink({ referrer: queryParams.ref }); + case 'chatBoostLink': + return buildChatBoostLink({ username: queryParams.domain, id: queryParams.channel }); default: break; } @@ -232,7 +245,7 @@ function parseHttpLink(url: URL) { switch (deepLinkType) { case 'publicMessageLink': { const { - single, comment, t, boost, + single, comment, t, } = queryParams; const { username, @@ -254,12 +267,11 @@ function parseHttpLink(url: URL) { threadId: thread, commentId: comment, mediaTimestamp: t, - boost, }); } case 'privateMessageLink': { const { - single, comment, t, boost, + single, comment, t, } = queryParams; const { channelId, @@ -281,7 +293,6 @@ function parseHttpLink(url: URL) { threadId: thread, commentId: comment, mediaTimestamp: t, - boost, }); } case 'shareLink': { @@ -304,8 +315,21 @@ function parseHttpLink(url: URL) { choose: queryParams.choose, ref: queryParams.ref, }); + case 'privateChannelLink': { + return buildPrivateChannelLink({ channelId: pathParams[1] }); + } case 'businessChatLink': return buildBusinessChatLink({ slug: pathParams[1] }); + case 'chatBoostLink': { + if (pathParams[0] === 'boost') { + return buildChatBoostLink({ username: pathParams[1], id: queryParams.c }); + } + const isPrivateChannel = pathParams[0] === 'c'; + return buildChatBoostLink({ + username: !isPrivateChannel ? pathParams[0] : undefined, + id: isPrivateChannel ? pathParams[1] : undefined, + }); + } default: break; } @@ -319,25 +343,25 @@ function getHttpDeepLinkType( const len = pathParams.length; const method = pathParams[0]; if (len === 1) { - if (method === 'share') { - return 'shareLink'; - } + if (method === 'share') return 'shareLink'; + if (method === 'boost' || queryParams.boost !== undefined) return 'chatBoostLink'; + if (isUsernameValid(method)) { return 'publicUsernameOrBotLink'; } } else if (len === 2) { - if (method === 'addlist') { - return 'chatFolderLink'; - } - if (method === 'login') { - return 'loginCodeLink'; + if (method === 'addlist') return 'chatFolderLink'; + if (method === 'login') return 'loginCodeLink'; + if (method === 'm') return 'businessChatLink'; + if (method === 'boost') return 'chatBoostLink'; + if (method === 'c') { + if (queryParams.boost !== undefined) return 'chatBoostLink'; + return 'privateChannelLink'; } + if (isUsernameValid(pathParams[0]) && isNumber(pathParams[1])) { return 'publicMessageLink'; } - if (method === 'm') { - return 'businessChatLink'; - } } else if (len === 3) { if (method === 'c' && pathParams.slice(1).every(isNumber)) { return 'privateMessageLink'; @@ -377,8 +401,9 @@ function getTgDeepLinkType( } case 'privatepost': { const { channel, post } = queryParams; - if (channel && post) { - return 'privateMessageLink'; + if (channel) { + if (post) return 'privateMessageLink'; + return 'privateChannelLink'; } break; } @@ -396,6 +421,8 @@ function getTgDeepLinkType( return 'premiumReferrerLink'; case 'premium_multigift': return 'premiumMultigiftLink'; + case 'boost': + return 'chatBoostLink'; default: break; } @@ -416,7 +443,7 @@ function buildShareLink(params: BuilderParams): BuilderReturnType { const { - messageId, threadId, commentId, username, single, mediaTimestamp, boost, + messageId, threadId, commentId, username, single, mediaTimestamp, } = params; if (!username || !isUsernameValid(username)) { return undefined; @@ -438,13 +465,12 @@ function buildPublicMessageLink(params: PublicMessageLinkBuilderParams): Builder threadId: threadId ? Number(threadId) : undefined, commentId: commentId ? Number(commentId) : undefined, mediaTimestamp, - isBoost: boost === '', }; } function buildPrivateMessageLink(params: PrivateMessageLinkBuilderParams): BuilderReturnType { const { - messageId, threadId, commentId, channelId, single, mediaTimestamp, boost, + messageId, threadId, commentId, channelId, single, mediaTimestamp, } = params; if (!channelId || !isNumber(channelId)) { return undefined; @@ -466,7 +492,6 @@ function buildPrivateMessageLink(params: PrivateMessageLinkBuilderParams): Build threadId: threadId ? Number(threadId) : undefined, commentId: commentId ? Number(commentId) : undefined, mediaTimestamp, - isBoost: boost === '', }; } @@ -557,6 +582,37 @@ function buildPublicUsernameOrBotLink( }; } +function buildPrivateChannelLink(params: BuilderParams): BuilderReturnType { + const { + channelId, + } = params; + if (!channelId) { + return undefined; + } + + return { + type: 'privateChannelLink', + channelId: toChannelId(channelId), + }; +} + +export function buildChatBoostLink(params: BuilderParams): BuilderReturnType { + const { + username, + id, + } = params; + + if (!username && !id) { + return undefined; + } + + return { + type: 'chatBoostLink', + username, + id: id ? toChannelId(id) : undefined, + }; +} + function buildBusinessChatLink(params: BuilderParams): BuilderReturnType { const { slug, @@ -587,8 +643,9 @@ function buildPremiumReferrerLink(params: BuilderParams): B }; } -function buildPremiumMultigiftLink(params: BuilderParams): -BuilderReturnType { +function buildPremiumMultigiftLink( + params: BuilderParams, +): BuilderReturnType { const { referrer, } = params; diff --git a/src/util/deeplink.ts b/src/util/deeplink.ts index 59927b466..14575dbb4 100644 --- a/src/util/deeplink.ts +++ b/src/util/deeplink.ts @@ -33,6 +33,12 @@ export const processDeepLink = (url: string): boolean => { }); return true; } + case 'privateChannelLink': { + actions.openPrivateChannel({ + id: parsedLink.channelId, + }); + return true; + } case 'businessChatLink': actions.resolveBusinessChatLink({ slug: parsedLink.slug, @@ -44,6 +50,12 @@ export const processDeepLink = (url: string): boolean => { case 'premiumMultigiftLink': actions.openGiftRecipientPicker(); return true; + case 'chatBoostLink': + actions.processBoostParameters({ + usernameOrId: (parsedLink.username || parsedLink.id)!, + isPrivate: Boolean(parsedLink.id), + }); + return true; default: break; } @@ -72,7 +84,6 @@ export const processDeepLink = (url: string): boolean => { openChatWithDraft, checkChatlistInvite, openStoryViewerByUsername, - processBoostParameters, checkGiftCode, openStarsBalanceModal, } = actions; @@ -84,7 +95,6 @@ export const processDeepLink = (url: string): boolean => { appname, startapp, mode, story, text, } = params; - const hasBoost = params.hasOwnProperty('boost'); const threadId = Number(thread) || Number(topic) || undefined; if (domain !== 'telegrampassport') { @@ -101,8 +111,6 @@ export const processDeepLink = (url: string): boolean => { username: domain, inviteHash: voicechat || livestream, }); - } else if (hasBoost) { - processBoostParameters({ usernameOrId: domain }); } else if (phone) { openChatByPhoneNumber({ phoneNumber: phone, @@ -182,14 +190,6 @@ export const processDeepLink = (url: string): boolean => { break; } - case 'boost': { - const { channel, domain } = params; - const isPrivate = Boolean(channel); - - processBoostParameters({ usernameOrId: channel || domain, isPrivate }); - break; - } - case 'giftcode': { const { slug } = params; checkGiftCode({ slug }); @@ -211,15 +211,10 @@ export function formatShareText(url?: string, text?: string, title?: string): Ap function handlePrivateMessageLink(link: PrivateMessageLink, actions: ReturnType) { const { focusMessage, - processBoostParameters, } = actions; const { - isBoost, channelId, messageId, threadId, + channelId, messageId, threadId, } = link; - if (isBoost) { - processBoostParameters({ usernameOrId: channelId, isPrivate: true }); - return; - } focusMessage({ chatId: toChannelId(channelId), threadId,