Support tg:// schema and t.me for comments (#1385)

This commit is contained in:
Alexander Zinchuk 2021-09-10 20:32:46 +03:00
parent fc650222ee
commit 57913234dd
9 changed files with 199 additions and 81 deletions

View File

@ -718,12 +718,12 @@ export async function requestThreadInfoUpdate({
]); ]);
if (!topMessageResult || !topMessageResult.messages.length) { if (!topMessageResult || !topMessageResult.messages.length) {
return; return undefined;
} }
const discussionChatId = resolveMessageApiChatId(topMessageResult.messages[0]); const discussionChatId = resolveMessageApiChatId(topMessageResult.messages[0]);
if (!discussionChatId) { if (!discussionChatId) {
return; return undefined;
} }
onUpdate({ onUpdate({
@ -749,6 +749,10 @@ export async function requestThreadInfoUpdate({
noTopChatsRequest: true, noTopChatsRequest: true,
}); });
}); });
return {
discussionChatId,
};
} }
export async function searchMessagesLocal({ export async function searchMessagesLocal({

View File

@ -2,7 +2,9 @@ import React, { FC, memo, useCallback } from '../../lib/teact/teact';
import { getDispatch } from '../../lib/teact/teactn'; import { getDispatch } from '../../lib/teact/teactn';
import convertPunycode from '../../lib/punycode'; 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'; import buildClassName from '../../util/buildClassName';
type OwnProps = { type OwnProps = {
@ -28,7 +30,8 @@ const SafeLink: FC<OwnProps> = ({
const handleClick = useCallback((e: React.MouseEvent<HTMLAnchorElement, MouseEvent>) => { const handleClick = useCallback((e: React.MouseEvent<HTMLAnchorElement, MouseEvent>) => {
if ( if (
e.ctrlKey || e.altKey || e.shiftKey || e.metaKey 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) { if (isNotSafe) {
toggleSafeLinkModal({ url }); toggleSafeLinkModal({ url });

View File

@ -128,8 +128,11 @@ export const CONTENT_TYPES_FOR_QUICK_UPLOAD = new Set([
// eslint-disable-next-line max-len // 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_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_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_INVITE_LINK = /^(?:https?:\/\/)?(?:t\.me\/joinchat\/)([\d\w_-]+)?$/gm;
export const RE_TME_ADDSTICKERS_LINK = /^(?:https?:\/\/)?(?:t\.me\/addstickers\/)([\d\w_-]+)$/gm;
// MTProto constants // MTProto constants
export const SERVICE_NOTIFICATIONS_USER_ID = 777000; export const SERVICE_NOTIFICATIONS_USER_ID = 777000;

View File

@ -20,6 +20,8 @@ import { pick } from '../util/iteratees';
import { selectCurrentMessageList } from '../modules/selectors'; import { selectCurrentMessageList } from '../modules/selectors';
import { hasStoredSession } from '../util/sessions'; import { hasStoredSession } from '../util/sessions';
import { INITIAL_STATE } from './initial'; import { INITIAL_STATE } from './initial';
import { parseLocationHash } from '../util/routing';
import { LOCATION_HASH } from '../hooks/useHistoryBack';
const UPDATE_THROTTLE = 5000; 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) { if (GLOBAL_STATE_CACHE_DISABLED) {
return undefined; return undefined;
} }
@ -87,7 +89,7 @@ function clearCaching() {
} }
} }
function readCache(initialState: GlobalState) { function readCache(initialState: GlobalState): GlobalState {
if (DEBUG) { if (DEBUG) {
// eslint-disable-next-line no-console // eslint-disable-next-line no-console
console.time('global-state-cache-read'); console.time('global-state-cache-read');
@ -116,19 +118,25 @@ function readCache(initialState: GlobalState) {
...cached.chatFolders, ...cached.chatFolders,
}; };
if (!cached.messages.messageLists) {
cached.messages.messageLists = initialState.messages.messageLists;
}
if (!cached.stickers.greeting) { if (!cached.stickers.greeting) {
cached.stickers.greeting = initialState.stickers.greeting; cached.stickers.greeting = initialState.stickers.greeting;
} }
} }
return { const newState = {
...initialState, ...initialState,
...cached, ...cached,
}; };
const parsedMessageList = !IS_SINGLE_COLUMN_LAYOUT ? parseLocationHash(LOCATION_HASH) : undefined;
return {
...newState,
messages: {
...newState.messages,
messageLists: parsedMessageList ? [parsedMessageList] : [],
},
};
} }
function updateCache() { function updateCache() {
@ -237,15 +245,9 @@ function reduceMessages(global: GlobalState): GlobalState['messages'] {
}; };
}); });
const currentMessageList = selectCurrentMessageList(global);
return { return {
byChatId, byChatId,
messageLists: !currentMessageList || IS_SINGLE_COLUMN_LAYOUT ? [] : [{ messageLists: [],
...currentMessageList,
threadId: MAIN_THREAD_ID,
type: 'thread',
}],
}; };
} }

View File

@ -467,7 +467,7 @@ export type ActionTypes = (
'openTelegramLink' | 'openChatByUsername' | 'requestThreadInfoUpdate' | 'setScrollOffset' | 'unpinAllMessages' | 'openTelegramLink' | 'openChatByUsername' | 'requestThreadInfoUpdate' | 'setScrollOffset' | 'unpinAllMessages' |
'setReplyingToId' | 'setEditingId' | 'editLastMessage' | 'saveDraft' | 'clearDraft' | 'loadPinnedMessages' | 'setReplyingToId' | 'setEditingId' | 'editLastMessage' | 'saveDraft' | 'clearDraft' | 'loadPinnedMessages' |
'toggleMessageWebPage' | 'replyToNextMessage' | 'deleteChatUser' | 'deleteChat' | 'toggleMessageWebPage' | 'replyToNextMessage' | 'deleteChatUser' | 'deleteChat' |
'reportMessages' | 'focusNextReply' | 'reportMessages' | 'focusNextReply' | 'openChatByInvite' |
// scheduled messages // scheduled messages
'loadScheduledHistory' | 'sendScheduledMessages' | 'rescheduleMessage' | 'deleteScheduledMessages' | 'loadScheduledHistory' | 'sendScheduledMessages' | 'rescheduleMessage' | 'deleteScheduledMessages' |
// poll result // poll result

View File

@ -5,7 +5,9 @@ import {
import { ApiChat } from '../../../api/types'; import { ApiChat } from '../../../api/types';
import { InlineBotSettings } from '../../../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 { callApi } from '../../../api/gramjs';
import { import {
selectChat, selectChatBot, selectChatMessage, selectCurrentChat, selectCurrentMessageList, selectChat, selectChatBot, selectChatMessage, selectCurrentChat, selectCurrentMessageList,
@ -28,7 +30,8 @@ addReducer('clickInlineButton', (global, actions, payload) => {
actions.sendBotCommand({ command: button.value }); actions.sendBotCommand({ command: button.value });
break; break;
case 'url': 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 }); actions.openTelegramLink({ url: button.value });
} else { } else {
actions.toggleSafeLinkModal({ url: button.value }); actions.toggleSafeLinkModal({ url: button.value });

View File

@ -15,7 +15,7 @@ import {
RE_TME_INVITE_LINK, RE_TME_INVITE_LINK,
RE_TME_LINK, RE_TME_LINK,
TIPS_USERNAME, TIPS_USERNAME,
LOCALIZED_TIPS, LOCALIZED_TIPS, RE_TG_LINK, RE_TME_ADDSTICKERS_LINK,
} from '../../../config'; } from '../../../config';
import { callApi } from '../../../api/gramjs'; import { callApi } from '../../../api/gramjs';
import { import {
@ -30,7 +30,6 @@ import {
} from '../../reducers'; } from '../../reducers';
import { import {
selectChat, selectChat,
selectCurrentChat,
selectUser, selectUser,
selectChatListType, selectChatListType,
selectIsChatPinned, selectIsChatPinned,
@ -39,12 +38,14 @@ import {
selectChatByUsername, selectChatByUsername,
selectThreadTopMessageId, selectThreadTopMessageId,
selectCurrentMessageList, selectCurrentMessageList,
selectThreadInfo,
} from '../../selectors'; } from '../../selectors';
import { buildCollectionByKey } from '../../../util/iteratees'; import { buildCollectionByKey } from '../../../util/iteratees';
import { debounce, pause, throttle } from '../../../util/schedulers'; import { debounce, pause, throttle } from '../../../util/schedulers';
import { import {
isChatSummaryOnly, isChatArchived, prepareChatList, isChatBasicGroup, isChatSummaryOnly, isChatArchived, prepareChatList, isChatBasicGroup,
} from '../../helpers'; } from '../../helpers';
import { processDeepLink } from '../../../util/deeplink';
const TOP_CHATS_PRELOAD_PAUSE = 100; const TOP_CHATS_PRELOAD_PAUSE = 100;
// We expect this ID does not exist // 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) => { addReducer('openTelegramLink', (global, actions, payload) => {
const { url } = payload!; const { url } = payload!;
let match = RE_TME_INVITE_LINK.exec(url); const stickersMatch = RE_TME_ADDSTICKERS_LINK.exec(url);
if (stickersMatch) {
if (match) { actions.openStickerSetShortName({
const hash = match[1]; stickerSetShortName: stickersMatch[1],
});
(async () => { } else if (url.match(RE_TG_LINK)) {
const result = await callApi('openChatByInvite', hash); processDeepLink(url.match(RE_TG_LINK)[0]);
if (!result) {
return;
}
actions.openChat({ id: result.chatId });
})();
} else { } else {
match = RE_TME_LINK.exec(url)!; let match = RE_TME_INVITE_LINK.exec(url);
const username = match[1]; if (match) {
const chatOrChannelPostId = match[2] ? Number(match[2]) : undefined; const hash = match[1];
const messageId = match[3] ? Number(match[3]) : undefined;
// Open message in private chat actions.openChatByInvite({ hash });
if (username === 'c' && chatOrChannelPostId && messageId) {
actions.focusMessage({ chatId: -chatOrChannelPostId, messageId });
} else { } 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) => { 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) => { addReducer('togglePreHistoryHidden', (global, actions, payload) => {
@ -1027,42 +1060,79 @@ async function deleteChatFolder(id: number) {
await callApi('deleteChatFolder', id); 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( async function openChatByUsername(
actions: GlobalActions, actions: GlobalActions,
username: string, username: string,
channelPostId?: number, 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 // Open temporary empty chat to make the click response feel faster
actions.openChat({ id: TMP_CHAT_ID }); actions.openChat({ id: TMP_CHAT_ID });
const chat = await callApi('getChatByUsername', username); const chat = await fetchChatByUsername(username);
if (!chat) { if (!chat) {
if (previousChat) { actions.openPreviousChat();
actions.openChat({ id: previousChat.id });
}
actions.showNotification({ message: 'User does not exist' }); actions.showNotification({ message: 'User does not exist' });
return; return;
} }
setGlobal(updateChat(getGlobal(), chat.id, chat));
if (channelPostId) { if (channelPostId) {
actions.focusMessage({ chatId: chat.id, messageId: channelPostId }); actions.focusMessage({ chatId: chat.id, messageId: channelPostId });
} else { } else {
actions.openChat({ id: chat.id }); 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),
});
}

View File

@ -1,13 +1,21 @@
import { getDispatch } from '../lib/teact/teactn'; 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) => { export const processDeepLink = (url: string) => {
const { protocol, searchParams, pathname } = new URL(url); const { protocol, searchParams, pathname } = new URL(url);
if (protocol !== 'tg:') return; 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<string, string> = {}; const params: Record<string, string> = {};
searchParams.forEach((value, key) => { searchParams.forEach((value, key) => {
params[key] = value; params[key] = value;
@ -15,26 +23,40 @@ export const processDeepLink = (url: string) => {
switch (method) { switch (method) {
case 'resolve': { case 'resolve': {
const { const { domain, post, comment } = params;
domain,
} = params;
if (domain !== 'telegrampassport') { if (domain !== 'telegrampassport') {
openChatByUsername({ openChatByUsername({
username: domain, username: domain,
messageId: Number(post),
commentId: Number(comment),
}); });
} }
break; break;
} }
case 'privatepost': case 'privatepost': {
const {
post, channel,
} = params;
focusMessage({
chatId: -Number(channel),
id: post,
});
break; break;
case 'bg': }
case 'bg': {
break; // const {
case 'join': // slug, color, rotation, mode, intensity, bg_color: bgColor, gradient,
// } = params;
break;
}
case 'join': {
const { invite } = params;
openChatByInvite({ hash: invite });
break; break;
}
case 'addstickers': { case 'addstickers': {
const { set } = params; const { set } = params;
@ -43,9 +65,15 @@ export const processDeepLink = (url: string) => {
}); });
break; break;
} }
case 'msg': case 'share':
case 'msg': {
// const { url, text } = params;
break; break;
}
case 'login': {
// const { code, token } = params;
break;
}
default: default:
// Unsupported deeplink // Unsupported deeplink

View File

@ -7,8 +7,13 @@ export const createMessageHash = (messageList: MessageList): string => (
: (messageList.threadId !== -1 ? `_${messageList.threadId}` : '')) : (messageList.threadId !== -1 ? `_${messageList.threadId}` : ''))
); );
export const parseMessageHash = (value: string): MessageList => { export const parseLocationHash = (value: string): MessageList | undefined => {
const [chatId, typeOrThreadId] = value.split('_'); if (!value) return undefined;
const [chatId, typeOrThreadId] = value.replace(/^#/, '').split('_');
if (!chatId) return undefined;
const isType = ['thread', 'pinned', 'scheduled'].includes(typeOrThreadId); const isType = ['thread', 'pinned', 'scheduled'].includes(typeOrThreadId);
return { return {