import type { ChatMediaSearchParams, ChatMediaSearchSegment, LoadingState, SharedMediaType, ThreadId, } from '../../../types'; import type { ActionReturnType, GlobalState, TabArgs } from '../../types'; import { type ApiPeer, MAIN_THREAD_ID } from '../../../api/types'; import { LoadMoreDirection } from '../../../types'; import { CHAT_MEDIA_SLICE, MESSAGE_SEARCH_SLICE, SHARED_MEDIA_SLICE, } from '../../../config'; import { getCurrentTabId } from '../../../util/establishMultitabRole'; import { buildCollectionByKey, isInsideSortedArrayRange } from '../../../util/iteratees'; import { getSearchResultKey } from '../../../util/keys/searchResultKey'; import { callApi } from '../../../api/gramjs'; import { getChatMediaMessageIds, getIsSavedDialog, isSameReaction } from '../../helpers'; import { addActionHandler, getGlobal, setGlobal, } from '../../index'; import { addChatMessagesById, addMessages, addUserStatuses, initializeChatMediaSearchResults, mergeWithChatMediaSearchSegment, setChatMediaSearchLoading, updateChatMediaSearchResults, updateMiddleSearch, updateMiddleSearchResults, updateSharedMediaSearchResults, } from '../../reducers'; import { selectChat, selectCurrentChatMediaSearch, selectCurrentMessageList, selectCurrentMiddleSearch, selectCurrentSharedMediaSearch, selectPeer, } from '../../selectors'; const MEDIA_PRELOAD_OFFSET = 9; addActionHandler('performMiddleSearch', async (global, actions, payload): Promise => { const { query, chatId, threadId = MAIN_THREAD_ID, tabId = getCurrentTabId(), } = payload || {}; if (!chatId) return; const currentUserId = global.currentUserId!; const isSavedDialog = getIsSavedDialog(chatId, threadId, currentUserId); const realChatId = isSavedDialog ? String(threadId) : chatId; const peer = realChatId ? selectPeer(global, realChatId) : undefined; let currentSearch = selectCurrentMiddleSearch(global, tabId); if (!peer) { return; } if (!currentSearch) { global = updateMiddleSearch(global, realChatId, threadId, {}, tabId); setGlobal(global); global = getGlobal(); } currentSearch = selectCurrentMiddleSearch(global, tabId)!; const { results, savedTag, type, isHashtag, } = currentSearch; const shouldReuseParams = results?.query === query; const offsetId = shouldReuseParams ? results?.nextOffsetId : undefined; const offsetRate = shouldReuseParams ? results?.nextOffsetRate : undefined; const offsetPeerId = shouldReuseParams ? results?.nextOffsetPeerId : undefined; const offsetPeer = shouldReuseParams && offsetPeerId ? selectChat(global, offsetPeerId) : undefined; const shouldHaveQuery = isHashtag || !savedTag; if (shouldHaveQuery && !query) { global = updateMiddleSearch(global, realChatId, threadId, { fetchingQuery: undefined, }, tabId); setGlobal(global); return; } global = updateMiddleSearch(global, realChatId, threadId, { fetchingQuery: query, }, tabId); setGlobal(global); let result; if (type === 'chat') { result = await callApi('searchMessagesInChat', { peer, type: 'text', query: isHashtag ? `#${query}` : query, threadId, limit: MESSAGE_SEARCH_SLICE, offsetId, isSavedDialog, savedTag, }); } if (type === 'myChats') { result = await callApi('searchMessagesGlobal', { type: 'text', query: isHashtag ? `#${query}` : query!, limit: MESSAGE_SEARCH_SLICE, offsetId, offsetRate, offsetPeer, }); } if (type === 'channels') { result = await callApi('searchHashtagPosts', { hashtag: query!, limit: MESSAGE_SEARCH_SLICE, offsetId, offsetPeer, offsetRate, }); } if (!result) { return; } const { userStatusesById, messages, totalCount, nextOffsetId, nextOffsetRate, nextOffsetPeerId, } = result; const newFoundIds = messages.map(getSearchResultKey); global = getGlobal(); currentSearch = selectCurrentMiddleSearch(global, tabId); const hasTagChanged = currentSearch?.savedTag && !isSameReaction(savedTag, currentSearch.savedTag); const hasSearchChanged = currentSearch?.fetchingQuery !== query; if (!currentSearch || hasSearchChanged || hasTagChanged) { return; } const resultChatId = isSavedDialog ? currentUserId : peer.id; global = addUserStatuses(global, userStatusesById); global = addMessages(global, messages); global = updateMiddleSearch(global, resultChatId, threadId, { fetchingQuery: undefined, }, tabId); global = updateMiddleSearchResults(global, resultChatId, threadId, { foundIds: newFoundIds, totalCount, nextOffsetId, nextOffsetRate, nextOffsetPeerId, query: query || '', }, tabId); setGlobal(global); }); addActionHandler('searchHashtag', (global, actions, payload): ActionReturnType => { const { hashtag, tabId = getCurrentTabId() } = payload; const messageList = selectCurrentMessageList(global, tabId); if (!messageList) { return; } const cleanQuery = hashtag.replace(/^#/, ''); actions.updateMiddleSearch({ chatId: messageList.chatId, threadId: messageList.threadId, update: { isHashtag: true, requestedQuery: cleanQuery, }, tabId, }); }); addActionHandler('searchSharedMediaMessages', (global, actions, payload): ActionReturnType => { const { tabId = getCurrentTabId() } = payload || {}; const { chatId, threadId } = selectCurrentMessageList(global, tabId) || {}; if (!chatId || !threadId) { return; } const isSavedDialog = getIsSavedDialog(chatId, threadId, global.currentUserId); const realChatId = isSavedDialog ? String(threadId) : chatId; const peer = selectPeer(global, realChatId); const currentSearch = selectCurrentSharedMediaSearch(global, tabId); if (!peer || !currentSearch) { return; } const { currentType: type, resultsByType } = currentSearch; const currentResults = type && resultsByType && resultsByType[type]; const offsetId = currentResults?.nextOffsetId; if (!type) { return; } void searchSharedMedia(global, peer, threadId, type, offsetId, undefined, isSavedDialog, tabId); }); addActionHandler('searchChatMediaMessages', (global, actions, payload): ActionReturnType => { const { chatId, threadId, currentMediaMessageId, limit, direction, tabId = getCurrentTabId(), } = payload; if (!chatId || !threadId || !currentMediaMessageId) { return; } const isSavedDialog = getIsSavedDialog(chatId, threadId, global.currentUserId); const realChatId = isSavedDialog ? String(threadId) : chatId; const chat = selectChat(global, realChatId); if (!chat) { return; } let currentSearch = selectCurrentChatMediaSearch(global, tabId); if (!currentSearch) { global = initializeChatMediaSearchResults(global, chatId, threadId, tabId); setGlobal(global); currentSearch = selectCurrentChatMediaSearch(global, tabId); if (!currentSearch) { return; } global = getGlobal(); } void searchChatMedia(global, chat, threadId, currentMediaMessageId, currentSearch, direction, isSavedDialog, limit, tabId); }); addActionHandler('searchMessagesByDate', async (global, actions, payload): Promise => { const { timestamp, tabId = getCurrentTabId() } = payload; const { chatId } = selectCurrentMessageList(global, tabId) || {}; if (!chatId) { return; } const chat = selectChat(global, chatId); if (!chat) { return; } const messageId = await callApi('findFirstMessageIdAfterDate', { chat, timestamp, }); if (!messageId) { return; } actions.focusMessage({ chatId: chat.id, messageId, tabId, }); }); async function searchSharedMedia( global: T, peer: ApiPeer, threadId: ThreadId, type: SharedMediaType, offsetId?: number, isBudgetPreload = false, isSavedDialog?: boolean, ...[tabId = getCurrentTabId()]: TabArgs ) { const resultChatId = isSavedDialog ? global.currentUserId! : peer.id; const result = await callApi('searchMessagesInChat', { peer, type, limit: SHARED_MEDIA_SLICE * 2, threadId, offsetId, isSavedDialog, }); if (!result) { return; } const { userStatusesById, messages, totalCount, nextOffsetId, } = result; const byId = buildCollectionByKey(messages, 'id'); const newFoundIds = Object.keys(byId).map(Number); global = getGlobal(); const currentSearch = selectCurrentSharedMediaSearch(global, tabId); if (!currentSearch) { return; } global = addUserStatuses(global, userStatusesById); global = addChatMessagesById(global, resultChatId, byId); global = updateSharedMediaSearchResults( global, resultChatId, threadId, type, newFoundIds, totalCount, nextOffsetId, tabId, ); setGlobal(global); if (!isBudgetPreload) { void searchSharedMedia(global, peer, threadId, type, nextOffsetId, true, isSavedDialog, tabId); } } function selectCurrentChatMediaSearchSegment( params: ChatMediaSearchParams, currentMediaMessageId: number, ): ChatMediaSearchSegment | undefined { if (isInsideSortedArrayRange(currentMediaMessageId, params.currentSegment.foundIds)) { return params.currentSegment; } const index = params.segments.findIndex( (segment) => isInsideSortedArrayRange(currentMediaMessageId, segment.foundIds), ); if (index === -1) { if (params.currentSegment && params.currentSegment.foundIds.length) { params.segments.push(params.currentSegment); } return undefined; } const result = params.segments.splice(index, 1)[0]; params.segments.push(params.currentSegment); return result; } function calcChatMediaSearchAddOffset( direction: LoadMoreDirection, limit: number, ): number { if (direction === LoadMoreDirection.Backwards) return 0; if (direction === LoadMoreDirection.Forwards) return -(limit + 1); return -(Math.round(limit / 2) + 1); } function calcChatMediaSearchOffsetId( direction: LoadMoreDirection, currentMessageId: number, segment?: ChatMediaSearchSegment, ) : number { if (!segment) return currentMessageId; if (direction === LoadMoreDirection.Backwards) return segment.foundIds[0]; if (direction === LoadMoreDirection.Forwards) return segment.foundIds[segment.foundIds.length - 1]; return currentMessageId; } function calcLoadMoreDirection(currentMessageId: number, currentSegment?: ChatMediaSearchSegment) { if (!currentSegment) return LoadMoreDirection.Around; const currentSegmentFoundIdsCount = currentSegment.foundIds.length; const idIndexInSegment = currentSegment.foundIds.indexOf(currentMessageId); if (idIndexInSegment === -1) return LoadMoreDirection.Around; if (currentSegment.loadingState.areAllItemsLoadedBackwards && currentSegment.loadingState.areAllItemsLoadedForwards) { return undefined; } const halfMediaCount = Math.floor(currentSegmentFoundIdsCount / 2); const preloadOffset = MEDIA_PRELOAD_OFFSET > halfMediaCount ? 0 : MEDIA_PRELOAD_OFFSET; const lastMediaIndex = currentSegmentFoundIdsCount - 1; if (idIndexInSegment <= preloadOffset) { if (currentSegment.loadingState.areAllItemsLoadedBackwards) return undefined; return LoadMoreDirection.Backwards; } if (idIndexInSegment >= lastMediaIndex - preloadOffset) { if (currentSegment.loadingState.areAllItemsLoadedForwards) return undefined; return LoadMoreDirection.Forwards; } return undefined; } function calcLoadingState( direction : LoadMoreDirection, limit : number, newFoundIdsCount : number, currentSegment?: ChatMediaSearchSegment, ) : LoadingState { let areAllItemsLoadedForwards = Boolean(currentSegment?.loadingState.areAllItemsLoadedForwards); let areAllItemsLoadedBackwards = Boolean(currentSegment?.loadingState.areAllItemsLoadedBackwards); if (newFoundIdsCount < limit) { if (direction === LoadMoreDirection.Forwards) { areAllItemsLoadedForwards = true; } else if (direction === LoadMoreDirection.Backwards) { areAllItemsLoadedBackwards = true; } } return { areAllItemsLoadedForwards, areAllItemsLoadedBackwards, }; } async function searchChatMedia( global: T, peer: ApiPeer, threadId: ThreadId, currentMediaMessageId: number, chatMediaSearchParams: ChatMediaSearchParams, direction?: LoadMoreDirection, isSavedDialog?: boolean, limit = CHAT_MEDIA_SLICE, ...[tabId = getCurrentTabId()]: TabArgs ) { const { isSynced } = global; if (!isSynced || chatMediaSearchParams.isLoading) { return; } let currentSegment = selectCurrentChatMediaSearchSegment(chatMediaSearchParams, currentMediaMessageId); if (direction === undefined) { direction = calcLoadMoreDirection(currentMediaMessageId, currentSegment); } if (direction === undefined) { return; } const offsetId = calcChatMediaSearchOffsetId(direction, currentMediaMessageId, currentSegment); const addOffset = calcChatMediaSearchAddOffset(direction, limit); const resultChatId = isSavedDialog ? global.currentUserId! : peer.id; global = setChatMediaSearchLoading(global, resultChatId, threadId, true, tabId); setGlobal(global); const result = await callApi('searchMessagesInChat', { peer, type: 'media', limit, threadId, offsetId, isSavedDialog, addOffset, }); global = getGlobal(); if (!result) { global = setChatMediaSearchLoading(global, resultChatId, threadId, false, tabId); setGlobal(global); return; } const { messages, userStatusesById, } = result; const byId = buildCollectionByKey(messages, 'id'); const newFoundIds = Object.keys(byId).map(Number); global = addUserStatuses(global, userStatusesById); global = addChatMessagesById(global, resultChatId, byId); const loadingState = calcLoadingState(direction, limit, newFoundIds.length, currentSegment); const filteredIds = getChatMediaMessageIds(byId, newFoundIds, false); currentSegment = mergeWithChatMediaSearchSegment( filteredIds, loadingState, currentSegment, ); global = updateChatMediaSearchResults( global, resultChatId, threadId, currentSegment, chatMediaSearchParams, tabId, ); global = setChatMediaSearchLoading(global, resultChatId, threadId, false, tabId); setGlobal(global); }