From 57913234dd0ee56365bfa0a0eb473763ecbcee4f Mon Sep 17 00:00:00 2001 From: Alexander Zinchuk Date: Fri, 10 Sep 2021 20:32:46 +0300 Subject: [PATCH] Support `tg://` schema and `t.me` for comments (#1385) --- src/api/gramjs/methods/messages.ts | 8 +- src/components/common/SafeLink.tsx | 7 +- src/config.ts | 5 +- src/global/cache.ts | 30 +++--- src/global/types.ts | 2 +- src/modules/actions/api/bots.ts | 7 +- src/modules/actions/api/chats.ts | 160 +++++++++++++++++++++-------- src/util/deeplink.ts | 52 +++++++--- src/util/routing.ts | 9 +- 9 files changed, 199 insertions(+), 81 deletions(-) diff --git a/src/api/gramjs/methods/messages.ts b/src/api/gramjs/methods/messages.ts index 784e4ac55..6a4c79be6 100644 --- a/src/api/gramjs/methods/messages.ts +++ b/src/api/gramjs/methods/messages.ts @@ -718,12 +718,12 @@ export async function requestThreadInfoUpdate({ ]); if (!topMessageResult || !topMessageResult.messages.length) { - return; + return undefined; } const discussionChatId = resolveMessageApiChatId(topMessageResult.messages[0]); if (!discussionChatId) { - return; + return undefined; } onUpdate({ @@ -749,6 +749,10 @@ export async function requestThreadInfoUpdate({ noTopChatsRequest: true, }); }); + + return { + discussionChatId, + }; } export async function searchMessagesLocal({ diff --git a/src/components/common/SafeLink.tsx b/src/components/common/SafeLink.tsx index e52a6d554..b3fab3874 100644 --- a/src/components/common/SafeLink.tsx +++ b/src/components/common/SafeLink.tsx @@ -2,7 +2,9 @@ import React, { FC, memo, useCallback } from '../../lib/teact/teact'; import { getDispatch } from '../../lib/teact/teactn'; import convertPunycode from '../../lib/punycode'; -import { DEBUG, RE_TME_INVITE_LINK, RE_TME_LINK } from '../../config'; +import { + DEBUG, RE_TG_LINK, RE_TME_ADDSTICKERS_LINK, RE_TME_INVITE_LINK, RE_TME_LINK, +} from '../../config'; import buildClassName from '../../util/buildClassName'; type OwnProps = { @@ -28,7 +30,8 @@ const SafeLink: FC = ({ const handleClick = useCallback((e: React.MouseEvent) => { if ( e.ctrlKey || e.altKey || e.shiftKey || e.metaKey - || !url || (!url.match(RE_TME_LINK) && !url.match(RE_TME_INVITE_LINK)) + || !url || (!url.match(RE_TME_LINK) && !url.match(RE_TME_INVITE_LINK) && !url.match(RE_TG_LINK) + && !url.match(RE_TME_ADDSTICKERS_LINK)) ) { if (isNotSafe) { toggleSafeLinkModal({ url }); diff --git a/src/config.ts b/src/config.ts index d98348311..875fe7845 100644 --- a/src/config.ts +++ b/src/config.ts @@ -128,8 +128,11 @@ export const CONTENT_TYPES_FOR_QUICK_UPLOAD = new Set([ // eslint-disable-next-line max-len export const RE_LINK_TEMPLATE = '((ftp|https?):\\/\\/)?((www\\.)?[-a-zA-Z0-9@:%._+~#=]{1,256}\\.[a-zA-Z0-9()]{1,63})\\b([-a-zA-Z0-9()@:%_+.~#?&/=]*)'; export const RE_MENTION_TEMPLATE = '(@[\\w\\d_-]+)'; -export const RE_TME_LINK = /^(?:https?:\/\/)?(?:t\.me\/)([\d\w_]+)(?:\/([\d]+))?(?:\/([\d]+))?$/gm; +export const RE_TG_LINK = /^tg:(\/\/)?([?=&\d\w_-]+)?/gm; +// eslint-disable-next-line max-len +export const RE_TME_LINK = /^(?:https?:\/\/)?(?:t\.me\/)([\d\w_]+)(?:\/([\d]+))?(?:\/([\d]+)(?:\?([\w]+)=([\d]+))?)?$/gm; export const RE_TME_INVITE_LINK = /^(?:https?:\/\/)?(?:t\.me\/joinchat\/)([\d\w_-]+)?$/gm; +export const RE_TME_ADDSTICKERS_LINK = /^(?:https?:\/\/)?(?:t\.me\/addstickers\/)([\d\w_-]+)$/gm; // MTProto constants export const SERVICE_NOTIFICATIONS_USER_ID = 777000; diff --git a/src/global/cache.ts b/src/global/cache.ts index 35922e32e..0d15b65b5 100644 --- a/src/global/cache.ts +++ b/src/global/cache.ts @@ -20,6 +20,8 @@ import { pick } from '../util/iteratees'; import { selectCurrentMessageList } from '../modules/selectors'; import { hasStoredSession } from '../util/sessions'; import { INITIAL_STATE } from './initial'; +import { parseLocationHash } from '../util/routing'; +import { LOCATION_HASH } from '../hooks/useHistoryBack'; const UPDATE_THROTTLE = 5000; @@ -55,7 +57,7 @@ export function initCache() { }); } -export function loadCache(initialState: GlobalState) { +export function loadCache(initialState: GlobalState): GlobalState | undefined { if (GLOBAL_STATE_CACHE_DISABLED) { return undefined; } @@ -87,7 +89,7 @@ function clearCaching() { } } -function readCache(initialState: GlobalState) { +function readCache(initialState: GlobalState): GlobalState { if (DEBUG) { // eslint-disable-next-line no-console console.time('global-state-cache-read'); @@ -116,19 +118,25 @@ function readCache(initialState: GlobalState) { ...cached.chatFolders, }; - if (!cached.messages.messageLists) { - cached.messages.messageLists = initialState.messages.messageLists; - } - if (!cached.stickers.greeting) { cached.stickers.greeting = initialState.stickers.greeting; } } - return { + const newState = { ...initialState, ...cached, }; + + const parsedMessageList = !IS_SINGLE_COLUMN_LAYOUT ? parseLocationHash(LOCATION_HASH) : undefined; + + return { + ...newState, + messages: { + ...newState.messages, + messageLists: parsedMessageList ? [parsedMessageList] : [], + }, + }; } function updateCache() { @@ -237,15 +245,9 @@ function reduceMessages(global: GlobalState): GlobalState['messages'] { }; }); - const currentMessageList = selectCurrentMessageList(global); - return { byChatId, - messageLists: !currentMessageList || IS_SINGLE_COLUMN_LAYOUT ? [] : [{ - ...currentMessageList, - threadId: MAIN_THREAD_ID, - type: 'thread', - }], + messageLists: [], }; } diff --git a/src/global/types.ts b/src/global/types.ts index 227d343a4..a858aaa16 100644 --- a/src/global/types.ts +++ b/src/global/types.ts @@ -467,7 +467,7 @@ export type ActionTypes = ( 'openTelegramLink' | 'openChatByUsername' | 'requestThreadInfoUpdate' | 'setScrollOffset' | 'unpinAllMessages' | 'setReplyingToId' | 'setEditingId' | 'editLastMessage' | 'saveDraft' | 'clearDraft' | 'loadPinnedMessages' | 'toggleMessageWebPage' | 'replyToNextMessage' | 'deleteChatUser' | 'deleteChat' | - 'reportMessages' | 'focusNextReply' | + 'reportMessages' | 'focusNextReply' | 'openChatByInvite' | // scheduled messages 'loadScheduledHistory' | 'sendScheduledMessages' | 'rescheduleMessage' | 'deleteScheduledMessages' | // poll result diff --git a/src/modules/actions/api/bots.ts b/src/modules/actions/api/bots.ts index a4954db04..a85f30611 100644 --- a/src/modules/actions/api/bots.ts +++ b/src/modules/actions/api/bots.ts @@ -5,7 +5,9 @@ import { import { ApiChat } from '../../../api/types'; import { InlineBotSettings } from '../../../types'; -import { RE_TME_INVITE_LINK, RE_TME_LINK } from '../../../config'; +import { + RE_TG_LINK, RE_TME_ADDSTICKERS_LINK, RE_TME_INVITE_LINK, RE_TME_LINK, +} from '../../../config'; import { callApi } from '../../../api/gramjs'; import { selectChat, selectChatBot, selectChatMessage, selectCurrentChat, selectCurrentMessageList, @@ -28,7 +30,8 @@ addReducer('clickInlineButton', (global, actions, payload) => { actions.sendBotCommand({ command: button.value }); break; case 'url': - if (button.value.match(RE_TME_INVITE_LINK) || button.value.match(RE_TME_LINK)) { + if (button.value.match(RE_TME_INVITE_LINK) || button.value.match(RE_TME_LINK) || button.value.match(RE_TG_LINK) + || button.value.match(RE_TME_ADDSTICKERS_LINK)) { actions.openTelegramLink({ url: button.value }); } else { actions.toggleSafeLinkModal({ url: button.value }); diff --git a/src/modules/actions/api/chats.ts b/src/modules/actions/api/chats.ts index 6be5af0ed..5d2b341e0 100644 --- a/src/modules/actions/api/chats.ts +++ b/src/modules/actions/api/chats.ts @@ -15,7 +15,7 @@ import { RE_TME_INVITE_LINK, RE_TME_LINK, TIPS_USERNAME, - LOCALIZED_TIPS, + LOCALIZED_TIPS, RE_TG_LINK, RE_TME_ADDSTICKERS_LINK, } from '../../../config'; import { callApi } from '../../../api/gramjs'; import { @@ -30,7 +30,6 @@ import { } from '../../reducers'; import { selectChat, - selectCurrentChat, selectUser, selectChatListType, selectIsChatPinned, @@ -39,12 +38,14 @@ import { selectChatByUsername, selectThreadTopMessageId, selectCurrentMessageList, + selectThreadInfo, } from '../../selectors'; import { buildCollectionByKey } from '../../../util/iteratees'; import { debounce, pause, throttle } from '../../../util/schedulers'; import { isChatSummaryOnly, isChatArchived, prepareChatList, isChatBasicGroup, } from '../../helpers'; +import { processDeepLink } from '../../../util/deeplink'; const TOP_CHATS_PRELOAD_PAUSE = 100; // We expect this ID does not exist @@ -432,33 +433,56 @@ addReducer('toggleChatUnread', (global, actions, payload) => { } }); +addReducer('openChatByInvite', (global, actions, payload) => { + const { hash } = payload!; + + (async () => { + const result = await callApi('openChatByInvite', hash); + if (!result) { + return; + } + + actions.openChat({ id: result.chatId }); + })(); +}); + addReducer('openTelegramLink', (global, actions, payload) => { const { url } = payload!; - let match = RE_TME_INVITE_LINK.exec(url); - - if (match) { - const hash = match[1]; - - (async () => { - const result = await callApi('openChatByInvite', hash); - if (!result) { - return; - } - - actions.openChat({ id: result.chatId }); - })(); + const stickersMatch = RE_TME_ADDSTICKERS_LINK.exec(url); + if (stickersMatch) { + actions.openStickerSetShortName({ + stickerSetShortName: stickersMatch[1], + }); + } else if (url.match(RE_TG_LINK)) { + processDeepLink(url.match(RE_TG_LINK)[0]); } else { - match = RE_TME_LINK.exec(url)!; + let match = RE_TME_INVITE_LINK.exec(url); - const username = match[1]; - const chatOrChannelPostId = match[2] ? Number(match[2]) : undefined; - const messageId = match[3] ? Number(match[3]) : undefined; + if (match) { + const hash = match[1]; - // Open message in private chat - if (username === 'c' && chatOrChannelPostId && messageId) { - actions.focusMessage({ chatId: -chatOrChannelPostId, messageId }); + actions.openChatByInvite({ hash }); } else { - void openChatByUsername(actions, username, chatOrChannelPostId); + match = RE_TME_LINK.exec(url)!; + + const username = match[1]; + const chatOrChannelPostId = match[2] ? Number(match[2]) : undefined; + const messageId = match[3] ? Number(match[3]) : undefined; + const commentId = match[4] === 'comment' && match[5] ? Number(match[5]) : undefined; + + // Open message in private group + if (username === 'c' && chatOrChannelPostId && messageId) { + actions.focusMessage({ + chatId: -chatOrChannelPostId, + messageId, + }); + } else { + actions.openChatByUsername({ + username, + messageId, + commentId, + }); + } } } }); @@ -476,9 +500,18 @@ addReducer('acceptInviteConfirmation', (global, actions, payload) => { }); addReducer('openChatByUsername', (global, actions, payload) => { - const { username } = payload!; + const { username, messageId, commentId } = payload!; - void openChatByUsername(actions, username); + (async () => { + if (!commentId) { + await openChatByUsername(actions, username, messageId); + return; + } + + if (!messageId) return; + + await openCommentsByUsername(actions, username, messageId, commentId); + })(); }); addReducer('togglePreHistoryHidden', (global, actions, payload) => { @@ -1027,42 +1060,79 @@ async function deleteChatFolder(id: number) { await callApi('deleteChatFolder', id); } +async function fetchChatByUsername( + username: string, +) { + const global = getGlobal(); + const localChat = selectChatByUsername(global, username); + if (localChat && !localChat.isMin) { + return localChat; + } + + const chat = await callApi('getChatByUsername', username); + if (!chat) { + return undefined; + } + + setGlobal(updateChat(getGlobal(), chat.id, chat)); + + return chat; +} + async function openChatByUsername( actions: GlobalActions, username: string, channelPostId?: number, ) { - const global = getGlobal(); - const localChat = selectChatByUsername(global, username); - if (localChat && !localChat.isMin) { - if (channelPostId) { - actions.focusMessage({ chatId: localChat.id, messageId: channelPostId }); - } else { - actions.openChat({ id: localChat.id }); - } - return; - } - - const previousChat = selectCurrentChat(global); // Open temporary empty chat to make the click response feel faster actions.openChat({ id: TMP_CHAT_ID }); - const chat = await callApi('getChatByUsername', username); + const chat = await fetchChatByUsername(username); + if (!chat) { - if (previousChat) { - actions.openChat({ id: previousChat.id }); - } - + actions.openPreviousChat(); actions.showNotification({ message: 'User does not exist' }); - return; } - setGlobal(updateChat(getGlobal(), chat.id, chat)); - if (channelPostId) { actions.focusMessage({ chatId: chat.id, messageId: channelPostId }); } else { actions.openChat({ id: chat.id }); } } + +async function openCommentsByUsername( + actions: GlobalActions, + username: string, + messageId: number, + commentId: number, +) { + actions.openChat({ id: TMP_CHAT_ID }); + + const chat = await fetchChatByUsername(username); + + if (!chat) return; + + const global = getGlobal(); + + const threadInfo = selectThreadInfo(global, chat.id, messageId); + let discussionChatId: number | undefined; + + if (!threadInfo) { + const result = await callApi('requestThreadInfoUpdate', { chat, threadId: messageId }); + if (!result) return; + + discussionChatId = result.discussionChatId; + } else { + discussionChatId = threadInfo.chatId; + } + + if (!discussionChatId) return; + + actions.focusMessage({ + chatId: discussionChatId, + threadId: messageId, + messageId: Number(commentId), + }); +} diff --git a/src/util/deeplink.ts b/src/util/deeplink.ts index 51d422c7c..f94da80fd 100644 --- a/src/util/deeplink.ts +++ b/src/util/deeplink.ts @@ -1,13 +1,21 @@ import { getDispatch } from '../lib/teact/teactn'; +type DeepLinkMethod = 'resolve' | 'login' | 'passport' | 'settings' | 'join' | 'addstickers' | 'setlanguage' | +'addtheme' | 'confirmphone' | 'socks' | 'proxy' | 'privatepost' | 'bg' | 'share' | 'msg' | 'msg_url'; + export const processDeepLink = (url: string) => { const { protocol, searchParams, pathname } = new URL(url); if (protocol !== 'tg:') return; - const { openChatByUsername, openStickerSetShortName } = getDispatch(); + const { + openChatByInvite, + openChatByUsername, + openStickerSetShortName, + focusMessage, + } = getDispatch(); - const method = pathname.replace(/^\/\//, ''); + const method = pathname.replace(/^\/\//, '') as DeepLinkMethod; const params: Record = {}; searchParams.forEach((value, key) => { params[key] = value; @@ -15,26 +23,40 @@ export const processDeepLink = (url: string) => { switch (method) { case 'resolve': { - const { - domain, - } = params; + const { domain, post, comment } = params; if (domain !== 'telegrampassport') { openChatByUsername({ username: domain, + messageId: Number(post), + commentId: Number(comment), }); } break; } - case 'privatepost': + case 'privatepost': { + const { + post, channel, + } = params; + focusMessage({ + chatId: -Number(channel), + id: post, + }); break; - case 'bg': - - break; - case 'join': + } + case 'bg': { + // const { + // slug, color, rotation, mode, intensity, bg_color: bgColor, gradient, + // } = params; + break; + } + case 'join': { + const { invite } = params; + openChatByInvite({ hash: invite }); break; + } case 'addstickers': { const { set } = params; @@ -43,9 +65,15 @@ export const processDeepLink = (url: string) => { }); break; } - case 'msg': - + case 'share': + case 'msg': { + // const { url, text } = params; break; + } + case 'login': { + // const { code, token } = params; + break; + } default: // Unsupported deeplink diff --git a/src/util/routing.ts b/src/util/routing.ts index ad3675976..3f367c9be 100644 --- a/src/util/routing.ts +++ b/src/util/routing.ts @@ -7,8 +7,13 @@ export const createMessageHash = (messageList: MessageList): string => ( : (messageList.threadId !== -1 ? `_${messageList.threadId}` : '')) ); -export const parseMessageHash = (value: string): MessageList => { - const [chatId, typeOrThreadId] = value.split('_'); +export const parseLocationHash = (value: string): MessageList | undefined => { + if (!value) return undefined; + + const [chatId, typeOrThreadId] = value.replace(/^#/, '').split('_'); + + if (!chatId) return undefined; + const isType = ['thread', 'pinned', 'scheduled'].includes(typeOrThreadId); return {