From 74fd74203e8bf819878c581659ba90043b10f81d Mon Sep 17 00:00:00 2001 From: Alexander Zinchuk Date: Tue, 12 Dec 2023 12:34:44 +0100 Subject: [PATCH] Left Search: Search message deep link (#4061) --- src/components/left/search/ChatResults.tsx | 3 +- src/global/actions/api/globalSearch.ts | 55 +++++++++++++++++++++- src/global/actions/api/messages.ts | 5 +- src/util/deepLinkParser.ts | 5 ++ 4 files changed, 62 insertions(+), 6 deletions(-) diff --git a/src/components/left/search/ChatResults.tsx b/src/components/left/search/ChatResults.tsx index 820ac1bde..04e229a7d 100644 --- a/src/components/left/search/ChatResults.tsx +++ b/src/components/left/search/ChatResults.tsx @@ -177,8 +177,7 @@ const ChatResults: FC = ({ return globalMessagesByChatId?.[chatId]?.byId[Number(messageId)]; }) - .filter(Boolean) - .sort((a, b) => b.date - a.date); + .filter(Boolean); }, [foundIds, globalMessagesByChatId, searchQuery, searchDate]); const handleClickShowMoreLocal = useCallback(() => { diff --git a/src/global/actions/api/globalSearch.ts b/src/global/actions/api/globalSearch.ts index 76113682b..21767974b 100644 --- a/src/global/actions/api/globalSearch.ts +++ b/src/global/actions/api/globalSearch.ts @@ -5,10 +5,12 @@ import type { ActionReturnType, GlobalState, TabArgs } from '../../types'; import { GLOBAL_SEARCH_SLICE, GLOBAL_TOPIC_SEARCH_SLICE } from '../../../config'; import { timestampPlusDay } from '../../../util/dateFormat'; +import { DeepLinkType, isDeepLink, tryParseDeepLink } from '../../../util/deepLinkParser'; import { getCurrentTabId } from '../../../util/establishMultitabRole'; import { buildCollectionByKey } from '../../../util/iteratees'; import { throttle } from '../../../util/schedulers'; import { callApi } from '../../../api/gramjs'; +import { isChatChannel, isChatGroup, toChannelId } from '../../helpers/chats'; import { addActionHandler, getGlobal, setGlobal } from '../../index'; import { addChats, @@ -19,7 +21,9 @@ import { updateGlobalSearchResults, updateTopics, } from '../../reducers'; -import { selectChat, selectCurrentGlobalSearchQuery, selectTabState } from '../../selectors'; +import { + selectChat, selectChatByUsername, selectChatMessage, selectCurrentGlobalSearchQuery, selectTabState, +} from '../../selectors'; const searchThrottled = throttle((cb) => cb(), 500, false); @@ -120,6 +124,8 @@ async function searchMessagesGlobal( nextRate: number | undefined; } | undefined; + let messageLink: ApiMessage | undefined; + if (chat) { const localResultRequest = callApi('searchMessagesLocal', { chat, @@ -164,6 +170,14 @@ async function searchMessagesGlobal( maxDate, minDate, }); + if (isDeepLink(query)) { + const link = tryParseDeepLink(query); + if (link?.type === DeepLinkType.PublicMessageLink) { + messageLink = await getMessageByPublicLink(global, link); + } else if (link?.type === DeepLinkType.PrivateMessageLink) { + messageLink = await getMessageByPrivateLink(global, link); + } + } } global = getGlobal(); @@ -174,6 +188,10 @@ async function searchMessagesGlobal( return; } + if (messageLink) { + result.totalCount = result.messages.unshift(messageLink); + } + const { messages, users, chats, totalCount, nextRate, } = result; @@ -210,3 +228,38 @@ async function searchMessagesGlobal( setGlobal(global); } + +async function getMessageByPublicLink(global: GlobalState, link: { username: string; messageId: number }) { + const { username, messageId } = link; + const localChat = selectChatByUsername(global, username); + if (localChat) { + return getChatGroupOrChannelMessage(global, localChat, messageId); + } + const { chat } = await callApi('getChatByUsername', username) ?? {}; + if (!chat) { + return undefined; + } + return getChatGroupOrChannelMessage(global, chat, messageId); +} + +function getMessageByPrivateLink(global: GlobalState, link: { channelId: string; messageId: number }) { + const { channelId, messageId } = link; + const internalChannelId = toChannelId(channelId); + const chat = selectChat(global, internalChannelId); + if (!chat) { + return undefined; + } + return getChatGroupOrChannelMessage(global, chat, messageId); +} + +async function getChatGroupOrChannelMessage(global: GlobalState, chat: ApiChat, messageId: number) { + if (!isChatGroup(chat) && !isChatChannel(chat)) { + return undefined; + } + const localMessage = selectChatMessage(global, chat.id, messageId); + if (localMessage) { + return localMessage; + } + const result = await callApi('fetchMessage', { chat, messageId }); + return result === 'MESSAGE_DELETED' ? undefined : result?.message; +} diff --git a/src/global/actions/api/messages.ts b/src/global/actions/api/messages.ts index 18eb3ebe0..fe10b1292 100644 --- a/src/global/actions/api/messages.ts +++ b/src/global/actions/api/messages.ts @@ -26,13 +26,12 @@ import { MAX_MEDIA_FILES_FOR_ALBUM, MESSAGE_LIST_SLICE, RE_TELEGRAM_LINK, - RE_TG_LINK, - RE_TME_LINK, SERVICE_NOTIFICATIONS_USER_ID, SUPPORTED_AUDIO_CONTENT_TYPES, SUPPORTED_IMAGE_CONTENT_TYPES, SUPPORTED_VIDEO_CONTENT_TYPES, } from '../../../config'; +import { isDeepLink } from '../../../util/deepLinkParser'; import { ensureProtocol } from '../../../util/ensureProtocol'; import { getCurrentTabId } from '../../../util/establishMultitabRole'; import { @@ -1542,7 +1541,7 @@ addActionHandler('openUrl', (global, actions, payload): ActionReturnType => { const urlWithProtocol = ensureProtocol(url)!; const isStoriesViewerOpen = Boolean(selectTabState(global, tabId).storyViewer.peerId); - if (urlWithProtocol.match(RE_TME_LINK) || urlWithProtocol.match(RE_TG_LINK)) { + if (isDeepLink(urlWithProtocol)) { if (isStoriesViewerOpen) { actions.closeStoryViewer({ tabId }); } diff --git a/src/util/deepLinkParser.ts b/src/util/deepLinkParser.ts index bd9eb9f95..08ef0be08 100644 --- a/src/util/deepLinkParser.ts +++ b/src/util/deepLinkParser.ts @@ -1,3 +1,4 @@ +import { RE_TG_LINK, RE_TME_LINK } from '../config'; import { isUsernameValid } from './username'; export type DeepLinkMethod = 'resolve' | 'login' | 'passport' | 'settings' | 'join' | 'addstickers' | 'addemoji' | @@ -50,6 +51,10 @@ type BuilderReturnType = T | undefined; const ELIGIBLE_HOSTNAMES = new Set(['t.me', 'telegram.me', 'telegram.dog']); +export function isDeepLink(link: string): boolean { + return Boolean(link.match(RE_TME_LINK) || link.match(RE_TG_LINK)); +} + export function tryParseDeepLink(link: string): DeepLink | undefined { try { return parseDeepLink(link);