diff --git a/src/api/gramjs/methods/chats.ts b/src/api/gramjs/methods/chats.ts index 2383f1ee7..abd6ad315 100644 --- a/src/api/gramjs/methods/chats.ts +++ b/src/api/gramjs/methods/chats.ts @@ -1998,12 +1998,9 @@ export function setViewForumAsMessages({ chat, isEnabled }: { chat: ApiChat; isE }); } -export async function fetchChannelRecommendations({ chat }: { chat: ApiChat }) { - const { id, accessHash } = chat; - const channel = buildInputEntity(id, accessHash); - +export async function fetchChannelRecommendations({ chat }: { chat?: ApiChat }) { const result = await invokeRequest(new GramJs.channels.GetChannelRecommendations({ - channel: channel as GramJs.InputChannel, + channel: chat && buildInputEntity(chat.id, chat.accessHash) as GramJs.InputChannel, })); if (!result) { return undefined; @@ -2011,12 +2008,13 @@ export async function fetchChannelRecommendations({ chat }: { chat: ApiChat }) { updateLocalDb(result); + const similarChannels = result?.chats + .map((c) => buildApiChatFromPreview(c)) + .filter(Boolean); + return { - similarChannels: result?.chats - .map((_chat) => buildApiChatFromPreview(_chat)) - .filter(Boolean), - count: - result instanceof GramJs.messages.ChatsSlice ? result.count : undefined, + similarChannels, + count: result instanceof GramJs.messages.ChatsSlice ? result.count : similarChannels.length, }; } diff --git a/src/api/gramjs/methods/messages.ts b/src/api/gramjs/methods/messages.ts index 31cb78ea7..d4a1cae9c 100644 --- a/src/api/gramjs/methods/messages.ts +++ b/src/api/gramjs/methods/messages.ts @@ -1232,6 +1232,7 @@ export async function searchMessagesGlobal({ q: query, offsetRate, offsetPeer: new GramJs.InputPeerEmpty(), + broadcastsOnly: type === 'channels' || undefined, limit, filter, folderId: ALL_FOLDER_ID, diff --git a/src/api/types/messages.ts b/src/api/types/messages.ts index 7a2bb97a2..06ad3e672 100644 --- a/src/api/types/messages.ts +++ b/src/api/types/messages.ts @@ -786,7 +786,7 @@ export type ApiReplyKeyboard = { }; export type ApiMessageSearchType = 'text' | 'media' | 'documents' | 'links' | 'audio' | 'voice' | 'profilePhoto'; -export type ApiGlobalMessageSearchType = 'text' | 'media' | 'documents' | 'links' | 'audio' | 'voice'; +export type ApiGlobalMessageSearchType = 'text' | 'channels' | 'media' | 'documents' | 'links' | 'audio' | 'voice'; export type ApiReportReason = 'spam' | 'violence' | 'pornography' | 'childAbuse' | 'copyright' | 'geoIrrelevant' | 'fake' | 'illegalDrugs' | 'personalDetails' | 'other'; diff --git a/src/components/left/search/ChatResults.tsx b/src/components/left/search/ChatResults.tsx index 1b2ae4a3b..88bdb5017 100644 --- a/src/components/left/search/ChatResults.tsx +++ b/src/components/left/search/ChatResults.tsx @@ -4,15 +4,16 @@ import React, { } from '../../../lib/teact/teact'; import { getActions, getGlobal, withGlobal } from '../../../global'; -import type { ApiChat, ApiMessage } from '../../../api/types'; +import type { ApiMessage } from '../../../api/types'; import { LoadMoreDirection } from '../../../types'; -import { ALL_FOLDER_ID } from '../../../config'; +import { ALL_FOLDER_ID, GLOBAL_SUGGESTED_CHANNELS_ID } from '../../../config'; import { filterChatsByName, filterUsersByName, + isChatChannel, } from '../../../global/helpers'; -import { selectTabState } from '../../../global/selectors'; +import { selectSimilarChannelIds, selectTabState } from '../../../global/selectors'; import { getOrderedIds } from '../../../util/folderManager'; import { unique } from '../../../util/iteratees'; import { MEMO_EMPTY_ARRAY } from '../../../util/memo'; @@ -21,6 +22,7 @@ import { renderMessageSummary } from '../../common/helpers/renderMessageText'; import sortChatIds from '../../common/helpers/sortChatIds'; import useAppLayout from '../../../hooks/useAppLayout'; +import useEffectOnce from '../../../hooks/useEffectOnce'; import useHorizontalScroll from '../../../hooks/useHorizontalScroll'; import useLang from '../../../hooks/useLang'; @@ -37,6 +39,7 @@ export type OwnProps = { searchQuery?: string; dateSearchQuery?: string; searchDate?: number; + isChannelList?: boolean; onReset: () => void; onSearchDateSelect: (value: Date) => void; }; @@ -50,8 +53,8 @@ type StateProps = { globalUserIds?: string[]; foundIds?: string[]; globalMessagesByChatId?: Record }>; - chatsById: Record; fetchingStatus?: { chats?: boolean; messages?: boolean }; + suggestedChannelIds?: string[]; }; const MIN_QUERY_LENGTH_FOR_GLOBAL_SEARCH = 4; @@ -60,6 +63,7 @@ const LESS_LIST_ITEMS_AMOUNT = 5; const runThrottled = throttle((cb) => cb(), 500, false); const ChatResults: FC = ({ + isChannelList, searchQuery, searchDate, dateSearchQuery, @@ -71,13 +75,13 @@ const ChatResults: FC = ({ globalUserIds, foundIds, globalMessagesByChatId, - chatsById, fetchingStatus, + suggestedChannelIds, onReset, onSearchDateSelect, }) => { const { - openChat, addRecentlyFoundChatId, searchMessagesGlobal, setGlobalSearchChatId, + openChat, addRecentlyFoundChatId, searchMessagesGlobal, setGlobalSearchChatId, loadChannelRecommendations, } = getActions(); // eslint-disable-next-line no-null/no-null @@ -89,11 +93,15 @@ const ChatResults: FC = ({ const [shouldShowMoreLocal, setShouldShowMoreLocal] = useState(false); const [shouldShowMoreGlobal, setShouldShowMoreGlobal] = useState(false); + useEffectOnce(() => { + if (isChannelList) loadChannelRecommendations({}); + }); + const handleLoadMore = useCallback(({ direction }: { direction: LoadMoreDirection }) => { if (direction === LoadMoreDirection.Backwards) { runThrottled(() => { searchMessagesGlobal({ - type: 'text', + type: isChannelList ? 'channels' : 'text', }); }); } @@ -120,21 +128,32 @@ const ChatResults: FC = ({ }, [setGlobalSearchChatId]); const localResults = useMemo(() => { - if (!searchQuery || (searchQuery.startsWith('@') && searchQuery.length < 2)) { + if (!isChannelList && (!searchQuery || (searchQuery.startsWith('@') && searchQuery.length < 2))) { return MEMO_EMPTY_ARRAY; } + // No need for expensive global updates, so we avoid them + const usersById = getGlobal().users.byId; + const chatsById = getGlobal().chats.byId; + + const orderedChatIds = getOrderedIds(ALL_FOLDER_ID) ?? []; + const filteredChatIds = orderedChatIds.filter((id) => { + if (!isChannelList) return true; + const chat = chatsById[id]; + return chat && isChatChannel(chat); + }); + const localChatIds = filterChatsByName(lang, filteredChatIds, chatsById, searchQuery, currentUserId); + + if (isChannelList) return localChatIds; + const contactIdsWithMe = [ ...(currentUserId ? [currentUserId] : []), ...(contactIds || []), ]; - // No need for expensive global updates on users, so we avoid them - const usersById = getGlobal().users.byId; + const localContactIds = filterUsersByName( contactIdsWithMe, usersById, searchQuery, currentUserId, lang('SavedMessages'), ); - const orderedChatIds = getOrderedIds(ALL_FOLDER_ID) ?? []; - const localChatIds = filterChatsByName(lang, orderedChatIds, chatsById, searchQuery, currentUserId); const localPeerIds = unique([ ...localContactIds, @@ -150,20 +169,27 @@ const ChatResults: FC = ({ ...sortChatIds(localPeerIds, undefined, currentUserId ? [currentUserId] : undefined), ...sortChatIds(accountPeerIds), ]; - }, [searchQuery, currentUserId, contactIds, lang, accountChatIds, accountUserIds, chatsById]); + }, [searchQuery, lang, currentUserId, contactIds, accountChatIds, accountUserIds, isChannelList]); - useHorizontalScroll(chatSelectionRef, !localResults.length, true); + useHorizontalScroll(chatSelectionRef, !localResults.length || isChannelList, true); const globalResults = useMemo(() => { if (!searchQuery || searchQuery.length < MIN_QUERY_LENGTH_FOR_GLOBAL_SEARCH || !globalChatIds || !globalUserIds) { return MEMO_EMPTY_ARRAY; } - return sortChatIds( - unique([...globalChatIds, ...globalUserIds]), - true, - ); - }, [globalChatIds, globalUserIds, searchQuery]); + // No need for expensive global updates, so we avoid them + const chatsById = getGlobal().chats.byId; + + const ids = unique([...globalChatIds, ...globalUserIds]); + const filteredIds = ids.filter((id) => { + if (!isChannelList) return true; + const chat = chatsById[id]; + return chat && isChatChannel(chat); + }); + + return sortChatIds(filteredIds, true); + }, [globalChatIds, globalUserIds, isChannelList, searchQuery]); const foundMessages = useMemo(() => { if ((!searchQuery && !searchDate) || !foundIds || foundIds.length === 0) { @@ -188,6 +214,8 @@ const ChatResults: FC = ({ }, [shouldShowMoreGlobal]); function renderFoundMessage(message: ApiMessage) { + const chatsById = getGlobal().chats.byId; + const text = renderMessageSummary(lang, message); const chat = chatsById[message.chatId]; @@ -207,7 +235,7 @@ const ChatResults: FC = ({ const nothingFound = fetchingStatus && !fetchingStatus.chats && !fetchingStatus.messages && !localResults.length && !globalResults.length && !foundMessages.length; - if (!searchQuery && !searchDate) { + if (!searchQuery && !searchDate && !isChannelList) { return ; } @@ -234,7 +262,7 @@ const ChatResults: FC = ({ description={lang('ChatList.Search.NoResultsDescription')} /> )} - {Boolean(localResults.length) && ( + {Boolean(localResults.length) && !isChannelList && (
= ({ {lang(shouldShowMoreLocal ? 'ChatList.Search.ShowLess' : 'ChatList.Search.ShowMore')} )} - {lang('DialogList.SearchSectionDialogs')} + {lang(isChannelList ? 'SearchMyChannels' : 'DialogList.SearchSectionDialogs')} {localResults.map((id, index) => { if (!shouldShowMoreLocal && index >= LESS_LIST_ITEMS_AMOUNT) { @@ -298,6 +326,22 @@ const ChatResults: FC = ({ })}
)} + {Boolean(suggestedChannelIds?.length) && !searchQuery && ( +
+

+ {lang('SearchRecommendedChannels')} +

+ {suggestedChannelIds.map((id) => { + return ( + + ); + })} +
+ )} {Boolean(foundMessages.length) && (

{lang('SearchMessages')}

@@ -309,18 +353,14 @@ const ChatResults: FC = ({ }; export default memo(withGlobal( - (global): StateProps => { - const { byId: chatsById } = global.chats; - + (global, { isChannelList }): StateProps => { const { userIds: contactIds } = global.contactList || {}; const { currentUserId, messages, } = global; if (!contactIds) { - return { - chatsById, - }; + return {}; } const { @@ -329,7 +369,8 @@ export default memo(withGlobal( const { chatIds: globalChatIds, userIds: globalUserIds } = globalResults || {}; const { chatIds: accountChatIds, userIds: accountUserIds } = localResults || {}; const { byChatId: globalMessagesByChatId } = messages; - const foundIds = resultsByType?.text?.foundIds; + const foundIds = resultsByType?.[isChannelList ? 'channels' : 'text']?.foundIds; + const { similarChannelIds } = selectSimilarChannelIds(global, GLOBAL_SUGGESTED_CHANNELS_ID) || {}; return { currentUserId, @@ -340,8 +381,8 @@ export default memo(withGlobal( globalUserIds, foundIds, globalMessagesByChatId, - chatsById, fetchingStatus, + suggestedChannelIds: similarChannelIds, }; }, )(ChatResults)); diff --git a/src/components/left/search/LeftSearch.tsx b/src/components/left/search/LeftSearch.tsx index 856ead852..c1844f3c9 100644 --- a/src/components/left/search/LeftSearch.tsx +++ b/src/components/left/search/LeftSearch.tsx @@ -1,6 +1,8 @@ import type { FC } from '../../../lib/teact/teact'; import React, { - memo, useCallback, useMemo, useRef, + memo, + useMemo, + useRef, useState, } from '../../../lib/teact/teact'; import { getActions, withGlobal } from '../../../global'; @@ -13,6 +15,7 @@ import { parseDateString } from '../../../util/date/dateFormat'; import useHistoryBack from '../../../hooks/useHistoryBack'; import useKeyboardListNavigation from '../../../hooks/useKeyboardListNavigation'; import useLang from '../../../hooks/useLang'; +import useLastCallback from '../../../hooks/useLastCallback'; import TabList from '../../ui/TabList'; import Transition from '../../ui/Transition'; @@ -39,6 +42,7 @@ type StateProps = { const TABS = [ { type: GlobalSearchContent.ChatList, title: 'SearchAllChatsShort' }, + { type: GlobalSearchContent.ChannelList, title: 'ChannelsTab' }, { type: GlobalSearchContent.Media, title: 'SharedMediaTab2' }, { type: GlobalSearchContent.Links, title: 'SharedLinksTab2' }, { type: GlobalSearchContent.Files, title: 'SharedFilesTab2' }, @@ -48,11 +52,9 @@ const TABS = [ const CHAT_TABS = [ { type: GlobalSearchContent.ChatList, title: 'All Messages' }, - ...TABS.slice(1), + ...TABS.slice(2), // Skip ChatList and ChannelList, replaced with All Messages ]; -const TRANSITION_RENDER_COUNT = Object.keys(GlobalSearchContent).length / 2; - const LeftSearch: FC = ({ searchQuery, searchDate, @@ -70,15 +72,17 @@ const LeftSearch: FC = ({ const [activeTab, setActiveTab] = useState(currentContent); const dateSearchQuery = useMemo(() => parseDateString(searchQuery), [searchQuery]); - const handleSwitchTab = useCallback((index: number) => { - const tab = TABS[index]; + const tabs = chatId ? CHAT_TABS : TABS; + + const handleSwitchTab = useLastCallback((index: number) => { + const tab = tabs[index]; setGlobalSearchContent({ content: tab.type }); setActiveTab(index); - }, [setGlobalSearchContent]); + }); - const handleSearchDateSelect = useCallback((value: Date) => { + const handleSearchDateSelect = useLastCallback((value: Date) => { setGlobalSearchDate({ date: value.getTime() / 1000 }); - }, [setGlobalSearchDate]); + }); useHistoryBack({ isActive, @@ -91,15 +95,16 @@ const LeftSearch: FC = ({ return (
- + {(() => { switch (currentContent) { case GlobalSearchContent.ChatList: + case GlobalSearchContent.ChannelList: if (chatId) { return ( = ({ } return ( = ({ loadPeerPinnedStories, loadStoriesArchive, openPremiumModal, - fetchChannelRecommendations, + loadChannelRecommendations, } = getActions(); // eslint-disable-next-line no-null/no-null @@ -257,7 +257,7 @@ const Profile: FC = ({ useEffect(() => { if (isChannel && !similarChannels) { - fetchChannelRecommendations({ chatId }); + loadChannelRecommendations({ chatId }); } }, [chatId, isChannel, similarChannels]); diff --git a/src/config.ts b/src/config.ts index bf0653b3a..49b2be81d 100644 --- a/src/config.ts +++ b/src/config.ts @@ -93,6 +93,8 @@ export const STORY_VIEWS_MIN_SEARCH = 15; export const STORY_MIN_REACTIONS_SORT = 10; export const STORY_VIEWS_MIN_CONTACTS_FILTER = 20; +export const GLOBAL_SUGGESTED_CHANNELS_ID = 'global'; + // As in Telegram for Android // https://github.com/DrKLO/Telegram/blob/51e9947527/TMessagesProj/src/main/java/org/telegram/messenger/MediaDataController.java#L7799 export const TOP_REACTIONS_LIMIT = 100; diff --git a/src/global/actions/api/chats.ts b/src/global/actions/api/chats.ts index d54ba2c9a..5f6dec28f 100644 --- a/src/global/actions/api/chats.ts +++ b/src/global/actions/api/chats.ts @@ -21,6 +21,7 @@ import { CHAT_LIST_LOAD_SLICE, DEBUG, GLOBAL_STATE_CACHE_ARCHIVED_CHAT_LIST_LIMIT, + GLOBAL_SUGGESTED_CHANNELS_ID, RE_TG_LINK, SAVED_FOLDER_ID, SERVICE_NOTIFICATIONS_USER_ID, @@ -105,6 +106,7 @@ import { selectIsChatPinned, selectIsChatWithSelf, selectLastServiceNotification, + selectSimilarChannelIds, selectStickerSet, selectSupportChat, selectTabState, @@ -2615,25 +2617,34 @@ addActionHandler('setViewForumAsMessages', (global, actions, payload): ActionRet void callApi('setViewForumAsMessages', { chat, isEnabled }); }); -addActionHandler('fetchChannelRecommendations', async (global, actions, payload): Promise => { +addActionHandler('loadChannelRecommendations', async (global, actions, payload): Promise => { const { chatId } = payload; - const chat = selectChat(global, chatId); + const chat = chatId ? selectChat(global, chatId) : undefined; - if (!chat) { + if (chatId && !chat) { return; } - const { similarChannels, count } = await callApi('fetchChannelRecommendations', { + if (!chatId) { + const similarChannelIds = selectSimilarChannelIds(global, GLOBAL_SUGGESTED_CHANNELS_ID); + if (similarChannelIds) return; // Already cached + } + + const result = await callApi('fetchChannelRecommendations', { chat, - }) || {}; + }); - if (!similarChannels) { + if (!result) { return; } + const { similarChannels, count } = result; + + const chatsById = buildCollectionByKey(similarChannels, 'id'); + global = getGlobal(); - global = addChats(global, buildCollectionByKey(similarChannels, 'id')); - global = addSimilarChannels(global, chatId, similarChannels.map((channel) => channel.id), count); + global = addChats(global, chatsById); + global = addSimilarChannels(global, chatId || GLOBAL_SUGGESTED_CHANNELS_ID, Object.keys(chatsById), count); setGlobal(global); }); diff --git a/src/global/actions/apiUpdaters/chats.ts b/src/global/actions/apiUpdaters/chats.ts index dfd756be0..24ad5debd 100644 --- a/src/global/actions/apiUpdaters/chats.ts +++ b/src/global/actions/apiUpdaters/chats.ts @@ -95,7 +95,7 @@ addActionHandler('apiUpdate', (global, actions, update): ActionReturnType => { const listType = selectChatListType(global, update.id); const chat = selectChat(global, update.id); if (chat && isChatChannel(chat)) { - actions.fetchChannelRecommendations({ chatId: chat.id }); + actions.loadChannelRecommendations({ chatId: chat.id }); const lastMessageId = selectChatLastMessageId(global, chat.id); const localMessage = buildLocalMessage(chat, lastMessageId); localMessage.content.action = { diff --git a/src/global/reducers/chats.ts b/src/global/reducers/chats.ts index f3cd470b7..38e705f0d 100644 --- a/src/global/reducers/chats.ts +++ b/src/global/reducers/chats.ts @@ -184,7 +184,10 @@ export function addChats(global: T, newById: Record