DeepLink: Open private message link (#4157)

This commit is contained in:
Alexander Zinchuk 2024-01-12 13:00:32 +01:00
parent a5c38e59cc
commit b8f0607700
3 changed files with 87 additions and 51 deletions

View File

@ -28,6 +28,7 @@ import {
TOPICS_SLICE_SECOND_LOAD, TOPICS_SLICE_SECOND_LOAD,
} from '../../../config'; } from '../../../config';
import { formatShareText, parseChooseParameter, processDeepLink } from '../../../util/deeplink'; import { formatShareText, parseChooseParameter, processDeepLink } from '../../../util/deeplink';
import { isDeepLink } from '../../../util/deepLinkParser';
import { getCurrentTabId } from '../../../util/establishMultitabRole'; import { getCurrentTabId } from '../../../util/establishMultitabRole';
import { getOrderedIds } from '../../../util/folderManager'; import { getOrderedIds } from '../../../util/folderManager';
import { buildCollectionByKey, omit, pick } from '../../../util/iteratees'; import { buildCollectionByKey, omit, pick } from '../../../util/iteratees';
@ -1167,6 +1168,13 @@ addActionHandler('openTelegramLink', (global, actions, payload): ActionReturnTyp
tabId = getCurrentTabId(), tabId = getCurrentTabId(),
} = payload; } = payload;
if (isDeepLink(url)) {
const isProcessed = processDeepLink(url);
if (isProcessed || url.match(RE_TG_LINK)) {
return;
}
}
const { const {
openChatByPhoneNumber, openChatByPhoneNumber,
openChatByInvite, openChatByInvite,
@ -1184,11 +1192,6 @@ addActionHandler('openTelegramLink', (global, actions, payload): ActionReturnTyp
checkGiftCode, checkGiftCode,
} = actions; } = actions;
if (url.match(RE_TG_LINK)) {
processDeepLink(url);
return;
}
const uri = new URL(url.toLowerCase().startsWith('http') ? url : `https://${url}`); const uri = new URL(url.toLowerCase().startsWith('http') ? url : `https://${url}`);
if (TME_WEB_DOMAINS.has(uri.hostname) && uri.pathname === '/') { if (TME_WEB_DOMAINS.has(uri.hostname) && uri.pathname === '/') {
window.open(uri.toString(), '_blank', 'noopener'); window.open(uri.toString(), '_blank', 'noopener');

View File

@ -9,20 +9,22 @@ interface PublicMessageLink {
type: 'publicMessageLink'; type: 'publicMessageLink';
username: string; username: string;
messageId: number; messageId: number;
isSingle?: boolean; isSingle: boolean;
threadId?: number; threadId?: number;
commentId?: number; commentId?: number;
mediaTimestamp?: string; mediaTimestamp?: string;
isBoost: boolean;
} }
interface PrivateMessageLink { export interface PrivateMessageLink {
type: 'privateMessageLink'; type: 'privateMessageLink';
channelId: string; channelId: string;
messageId: number; messageId: number;
isSingle?: boolean; isSingle: boolean;
threadId?: number; threadId?: number;
commentId?: number; commentId?: number;
mediaTimestamp?: string; mediaTimestamp?: string;
isBoost: boolean;
} }
interface ShareLink { interface ShareLink {
@ -63,6 +65,16 @@ type BuilderParams<T extends DeepLink> = Record<keyof Omit<T, 'type'>, string>;
type BuilderReturnType<T extends DeepLink> = T | undefined; type BuilderReturnType<T extends DeepLink> = T | undefined;
type DeepLinkType = DeepLink['type'] | 'unknown'; type DeepLinkType = DeepLink['type'] | 'unknown';
type PrivateMessageLinkBuilderParams = Omit<BuilderParams<PrivateMessageLink>, 'isSingle' | 'isBoost'> & {
single: string;
boost: string;
};
type PublicMessageLinkBuilderParams = Omit<BuilderParams<PublicMessageLink>, 'isSingle' | 'isBoost'> & {
single: string;
boost: string;
};
const ELIGIBLE_HOSTNAMES = new Set(['t.me', 'telegram.me', 'telegram.dog']); const ELIGIBLE_HOSTNAMES = new Set(['t.me', 'telegram.me', 'telegram.dog']);
export function isDeepLink(link: string): boolean { export function isDeepLink(link: string): boolean {
@ -100,28 +112,30 @@ function handleTgLink(url: URL) {
switch (deepLinkType) { switch (deepLinkType) {
case 'publicMessageLink': { case 'publicMessageLink': {
const { const {
domain, post, single, thread, comment, t, domain, post, single, thread, comment, t, boost,
} = queryParams; } = queryParams;
return buildPublicMessageLink({ return buildPublicMessageLink({
username: domain, username: domain,
messageId: post, messageId: post,
isSingle: single, single,
threadId: thread, threadId: thread,
commentId: comment, commentId: comment,
mediaTimestamp: t, mediaTimestamp: t,
boost,
}); });
} }
case 'privateMessageLink': { case 'privateMessageLink': {
const { const {
channel, post, single, thread, comment, t, channel, post, single, thread, comment, t, boost,
} = queryParams; } = queryParams;
return buildPrivateMessageLink({ return buildPrivateMessageLink({
channelId: channel, channelId: channel,
messageId: post, messageId: post,
isSingle: single, single,
threadId: thread, threadId: thread,
commentId: comment, commentId: comment,
mediaTimestamp: t, mediaTimestamp: t,
boost,
}); });
} }
case 'shareLink': case 'shareLink':
@ -156,7 +170,7 @@ function handleHttpLink(url: URL) {
switch (deepLinkType) { switch (deepLinkType) {
case 'publicMessageLink': { case 'publicMessageLink': {
const { const {
single, comment, t, single, comment, t, boost,
} = queryParams; } = queryParams;
const { const {
username, username,
@ -174,15 +188,16 @@ function handleHttpLink(url: URL) {
return buildPublicMessageLink({ return buildPublicMessageLink({
username, username,
messageId, messageId,
isSingle: single, single,
threadId: thread, threadId: thread,
commentId: comment, commentId: comment,
mediaTimestamp: t, mediaTimestamp: t,
boost,
}); });
} }
case 'privateMessageLink': { case 'privateMessageLink': {
const { const {
single, comment, t, single, comment, t, boost,
} = queryParams; } = queryParams;
const { const {
channelId, channelId,
@ -200,10 +215,11 @@ function handleHttpLink(url: URL) {
return buildPrivateMessageLink({ return buildPrivateMessageLink({
channelId, channelId,
messageId, messageId,
isSingle: single, single,
threadId: thread, threadId: thread,
commentId: comment, commentId: comment,
mediaTimestamp: t, mediaTimestamp: t,
boost,
}); });
} }
case 'shareLink': { case 'shareLink': {
@ -306,9 +322,9 @@ function buildShareLink(params: BuilderParams<ShareLink>): BuilderReturnType<Sha
}; };
} }
function buildPublicMessageLink(params: BuilderParams<PublicMessageLink>): BuilderReturnType<PublicMessageLink> { function buildPublicMessageLink(params: PublicMessageLinkBuilderParams): BuilderReturnType<PublicMessageLink> {
const { const {
messageId, threadId, commentId, username, isSingle, mediaTimestamp, messageId, threadId, commentId, username, single, mediaTimestamp, boost,
} = params; } = params;
if (!username || !isUsernameValid(username)) { if (!username || !isUsernameValid(username)) {
return undefined; return undefined;
@ -326,16 +342,17 @@ function buildPublicMessageLink(params: BuilderParams<PublicMessageLink>): Build
type: 'publicMessageLink', type: 'publicMessageLink',
username, username,
messageId: Number(messageId), messageId: Number(messageId),
isSingle: isSingle === '', isSingle: single === '',
threadId: threadId ? Number(threadId) : undefined, threadId: threadId ? Number(threadId) : undefined,
commentId: commentId ? Number(commentId) : undefined, commentId: commentId ? Number(commentId) : undefined,
mediaTimestamp, mediaTimestamp,
isBoost: boost === '',
}; };
} }
function buildPrivateMessageLink(params: BuilderParams<PrivateMessageLink>): BuilderReturnType<PrivateMessageLink> { function buildPrivateMessageLink(params: PrivateMessageLinkBuilderParams): BuilderReturnType<PrivateMessageLink> {
const { const {
messageId, threadId, commentId, channelId, isSingle, mediaTimestamp, messageId, threadId, commentId, channelId, single, mediaTimestamp, boost,
} = params; } = params;
if (!channelId || !isNumber(channelId)) { if (!channelId || !isNumber(channelId)) {
return undefined; return undefined;
@ -353,10 +370,11 @@ function buildPrivateMessageLink(params: BuilderParams<PrivateMessageLink>): Bui
type: 'privateMessageLink', type: 'privateMessageLink',
channelId, channelId,
messageId: Number(messageId), messageId: Number(messageId),
isSingle: isSingle === '', isSingle: single === '',
threadId: threadId ? Number(threadId) : undefined, threadId: threadId ? Number(threadId) : undefined,
commentId: commentId ? Number(commentId) : undefined, commentId: commentId ? Number(commentId) : undefined,
mediaTimestamp, mediaTimestamp,
isBoost: boost === '',
}; };
} }

View File

@ -1,24 +1,42 @@
import { getActions } from '../global'; import { getActions } from '../global';
import type { ApiChatType } from '../api/types'; import type { ApiChatType } from '../api/types';
import type { DeepLinkMethod } from './deepLinkParser'; import type { DeepLinkMethod, PrivateMessageLink } from './deepLinkParser';
import { API_CHAT_TYPES } from '../config'; import { API_CHAT_TYPES } from '../config';
import { toChannelId } from '../global/helpers';
import { tryParseDeepLink } from './deepLinkParser';
import { IS_SAFARI } from './windowEnvironment'; 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 { const {
protocol, searchParams, pathname, hostname, protocol, searchParams, pathname, hostname,
} = new URL(url); } = 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 { const {
openChatByInvite, openChatByInvite,
openChatByUsername, openChatByUsername,
openChatByPhoneNumber, openChatByPhoneNumber,
openStickerSet, openStickerSet,
focusMessage,
joinVoiceChatByLink, joinVoiceChatByLink,
openInvoice, openInvoice,
processAttachBotParameters, processAttachBotParameters,
@ -27,11 +45,7 @@ export const processDeepLink = (url: string) => {
openStoryViewerByUsername, openStoryViewerByUsername,
processBoostParameters, processBoostParameters,
checkGiftCode, checkGiftCode,
} = getActions(); } = actions;
// 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);
switch (method) { switch (method) {
case 'resolve': { case 'resolve': {
@ -84,24 +98,6 @@ export const processDeepLink = (url: string) => {
} }
break; 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': { case 'bg': {
// const { // const {
// slug, color, rotation, mode, intensity, bg_color: bgColor, gradient, // slug, color, rotation, mode, intensity, bg_color: bgColor, gradient,
@ -163,9 +159,9 @@ export const processDeepLink = (url: string) => {
} }
default: default:
// Unsupported deeplink // Unsupported deeplink
return false;
break;
} }
return true;
}; };
export function parseChooseParameter(choose?: string) { export function parseChooseParameter(choose?: string) {
@ -177,3 +173,22 @@ export function parseChooseParameter(choose?: string) {
export function formatShareText(url?: string, text?: string, title?: string): string { export function formatShareText(url?: string, text?: string, title?: string): string {
return [url, title, text].filter(Boolean).join('\n'); return [url, title, text].filter(Boolean).join('\n');
} }
function handlePrivateMessageLink(link: PrivateMessageLink, actions: ReturnType<typeof getActions>) {
const {
focusMessage,
processBoostParameters,
} = actions;
const {
isBoost, channelId, messageId, threadId,
} = link;
if (isBoost) {
processBoostParameters({ usernameOrId: channelId, isPrivate: true });
return;
}
focusMessage({
chatId: toChannelId(channelId),
threadId,
messageId,
});
}