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) {
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({

View File

@ -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<OwnProps> = ({
const handleClick = useCallback((e: React.MouseEvent<HTMLAnchorElement, 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 });

View File

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

View File

@ -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: [],
};
}

View File

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

View File

@ -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 });

View File

@ -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),
});
}

View File

@ -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<string, string> = {};
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

View File

@ -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 {