Deep Link: Rewrite boost link handling (#5330)

This commit is contained in:
zubiden 2024-12-29 11:58:24 +01:00 committed by Alexander Zinchuk
parent dc14185d99
commit a3f57d2982
9 changed files with 146 additions and 91 deletions

16
package-lock.json generated
View File

@ -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",

View File

@ -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",

View File

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

View File

@ -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<v
openStickerSet,
openChatWithDraft,
joinVoiceChatByLink,
focusMessage,
openInvoice,
checkChatlistInvite,
openChatByUsername: openChatByUsernameAction,
openStoryViewerByUsername,
processBoostParameters,
checkGiftCode,
} = actions;
@ -1317,7 +1314,6 @@ addActionHandler('openTelegramLink', async (global, actions, payload): Promise<v
}
const storyId = part2 === 's' && (Number(part3) || undefined);
const hasBoost = params.hasOwnProperty('boost');
if (part1.match(/^\+([0-9]+)(\?|$)/)) {
openChatByPhoneNumber({
@ -1392,30 +1388,6 @@ addActionHandler('openTelegramLink', async (global, actions, payload): Promise<v
inviteHash: params.voicechat || params.livestream,
tabId,
});
} else if (part1 === 'boost') {
const username = part2;
const id = params.c;
const isPrivate = !username && Boolean(id);
processBoostParameters({
usernameOrId: username || id,
isPrivate,
tabId,
});
} else if (hasBoost) {
const isPrivate = part1 === 'c' && Boolean(chatOrChannelPostId);
processBoostParameters({
usernameOrId: chatOrChannelPostId || part1,
isPrivate,
tabId,
});
} else if (part1 === 'c' && chatOrChannelPostId && messageId) {
focusMessage({
chatId: toChannelId(chatOrChannelPostId),
messageId,
tabId,
});
} else if (part1.startsWith('$')) {
openInvoice({
type: 'slug',
@ -1455,16 +1427,15 @@ addActionHandler('processBoostParameters', async (global, actions, payload): Pro
let chat: ApiChat | undefined;
if (isPrivate) {
const chatId = toChannelId(usernameOrId);
chat = selectChat(global, chatId);
chat = selectChat(global, usernameOrId);
if (!chat) {
actions.showNotification({ message: 'Chat does not exist', tabId });
actions.showNotification({ message: { key: 'PrivateChannelInaccessible' }, tabId });
return;
}
} else {
chat = await fetchChatByUsername(global, usernameOrId);
if (!chat) {
actions.showNotification({ message: 'User does not exist', tabId });
actions.showNotification({ message: { key: 'NoUsernameFound' }, tabId });
return;
}
}

View File

@ -88,6 +88,22 @@ addActionHandler('processOpenChatOrThread', (global, actions, payload): ActionRe
return updateCurrentMessageList(global, chatId, threadId, type, shouldReplaceHistory, shouldReplaceLast, tabId);
});
addActionHandler('openPrivateChannel', (global, actions, payload): ActionReturnType => {
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;

View File

@ -2330,6 +2330,9 @@ export interface ActionPayloads {
noForumTopicPanel?: boolean;
isComments?: boolean;
} & WithTabId;
openPrivateChannel: {
id: string;
} & WithTabId;
loadFullChat: {
chatId: string;
withPhotos?: boolean;

View File

@ -850,6 +850,7 @@ export interface LangPair {
'StickerPackErrorNotFound': undefined;
'ContactsPhoneNumberNotRegistred': undefined;
'NoUsernameFound': undefined;
'PrivateChannelInaccessible': undefined;
'HiddenName': undefined;
'ChannelPersmissionDeniedSendMessagesForever': undefined;
'ChannelPersmissionDeniedSendMessagesDefaultRestrictedText': undefined;

View File

@ -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<T extends DeepLink> = Record<keyof Omit<T, 'type'>, string | undefined>;
type BuilderReturnType<T extends DeepLink> = T | undefined;
type DeepLinkType = DeepLink['type'] | 'unknown';
type PrivateMessageLinkBuilderParams = Omit<BuilderParams<PrivateMessageLink>, 'isSingle' | 'isBoost'> & {
single: string | undefined;
boost: string | undefined;
type PrivateMessageLinkBuilderParams = Omit<BuilderParams<PrivateMessageLink>, 'isSingle'> & {
single?: string;
};
type PublicMessageLinkBuilderParams = Omit<BuilderParams<PublicMessageLink>, 'isSingle' | 'isBoost'> & {
single: string | undefined;
boost: string | undefined;
type PublicMessageLinkBuilderParams = Omit<BuilderParams<PublicMessageLink>, '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<ShareLink>): BuilderReturnType<Sha
function buildPublicMessageLink(params: PublicMessageLinkBuilderParams): BuilderReturnType<PublicMessageLink> {
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<PrivateMessageLink> {
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<PrivateChannelLink>): BuilderReturnType<PrivateChannelLink> {
const {
channelId,
} = params;
if (!channelId) {
return undefined;
}
return {
type: 'privateChannelLink',
channelId: toChannelId(channelId),
};
}
export function buildChatBoostLink(params: BuilderParams<ChatBoostLink>): BuilderReturnType<ChatBoostLink> {
const {
username,
id,
} = params;
if (!username && !id) {
return undefined;
}
return {
type: 'chatBoostLink',
username,
id: id ? toChannelId(id) : undefined,
};
}
function buildBusinessChatLink(params: BuilderParams<BusinessChatLink>): BuilderReturnType<BusinessChatLink> {
const {
slug,
@ -587,8 +643,9 @@ function buildPremiumReferrerLink(params: BuilderParams<PremiumReferrerLink>): B
};
}
function buildPremiumMultigiftLink(params: BuilderParams<PremiumMultigiftLink>):
BuilderReturnType<PremiumMultigiftLink> {
function buildPremiumMultigiftLink(
params: BuilderParams<PremiumMultigiftLink>,
): BuilderReturnType<PremiumMultigiftLink> {
const {
referrer,
} = params;

View File

@ -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<typeof getActions>) {
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,