diff --git a/lib/shared/search-utils.js b/lib/shared/search-utils.js --- a/lib/shared/search-utils.js +++ b/lib/shared/search-utils.js @@ -229,6 +229,7 @@ onResultsReceived: ( messages: $ReadOnlyArray, endReached: boolean, + threadID: string, ) => mixed, cursor?: string, ) => void { @@ -239,7 +240,7 @@ (query, threadID, onResultsReceived, cursor) => { const searchMessagesPromise = (async () => { if (query === '') { - onResultsReceived([], true); + onResultsReceived([], true, threadID); return; } const { messages, endReached } = await callSearchMessages({ @@ -247,7 +248,7 @@ threadID, cursor, }); - onResultsReceived(messages, endReached); + onResultsReceived(messages, endReached, threadID); })(); dispatchActionPromise(searchMessagesActionTypes, searchMessagesPromise); diff --git a/web/search/message-search-state-provider.react.js b/web/search/message-search-state-provider.react.js --- a/web/search/message-search-state-provider.react.js +++ b/web/search/message-search-state-provider.react.js @@ -3,10 +3,22 @@ import invariant from 'invariant'; import * as React from 'react'; +import { useSearchMessages } from 'lib/shared/search-utils.js'; +import { messageTypes } from 'lib/types/message-types-enum.js'; +import type { RawMessageInfo } from 'lib/types/message-types.js'; + type MessageSearchState = { +getQuery: (threadID: string) => string, +setQuery: (query: string, threadID: string) => void, +clearQuery: (threadID: string) => void, + +getSearchResults: (threadID: string) => $ReadOnlyArray, + +appendSearchResult: ( + $ReadOnlyArray, + threadID: string, + ) => void, + +getEndReached: (threadID: string) => boolean, + +setEndReached: (threadID: string) => void, + +searchMessages: (threadID: string) => void, }; const MessageSearchContext: React.Context = @@ -17,28 +29,121 @@ }; function MessageSearchStateProvider(props: Props): React.Node { - const [queries, setQueries] = React.useState<{ + const queries = React.useRef<{ [threadID: string]: string, }>({}); - const setQuery = React.useCallback( - (query: string, threadID: string) => - setQueries(prevQueries => ({ ...prevQueries, [threadID]: query })), + const [results, setResults] = React.useState<{ + [threadID: string]: $ReadOnlyArray, + }>({}); + + const endsReached = React.useRef(new Set()); + + const lastIDs = React.useRef<{ + [threadID: string]: string, + }>({}); + + const setEndReached = React.useCallback((threadID: string) => { + endsReached.current.add(threadID); + }, []); + + const removeEndReached = React.useCallback( + (threadID: string) => endsReached.current.delete(threadID), [], ); - const clearQuery = React.useCallback( - (threadID: string) => - setQueries(prevQueries => { - const { [threadID]: deleted, ...newState } = prevQueries; - return newState; - }), + const getEndReached = React.useCallback( + (threadID: string) => endsReached.current.has(threadID), + [endsReached], + ); + + const appendResult = React.useCallback( + (result: $ReadOnlyArray, threadID: string) => { + const lastMessageID = oldestMessageID(result); + if (lastMessageID) { + lastIDs.current[threadID] = lastMessageID; + } + setResults(prevResults => { + const prevThreadResults = prevResults[threadID] ?? []; + const newThreadResults = [...prevThreadResults, ...result]; + return { ...prevResults, [threadID]: newThreadResults }; + }); + }, [], ); + const clearResults = React.useCallback( + (threadID: string) => { + loading.current = false; + delete lastIDs.current[threadID]; + removeEndReached(threadID); + setResults(prevResults => { + const { [threadID]: deleted, ...newState } = prevResults; + return newState; + }); + }, + [removeEndReached], + ); + + const getResults = React.useCallback( + (threadID: string) => results[threadID] ?? [], + [results], + ); + const getQuery = React.useCallback( - (threadID: string) => queries[threadID] ?? '', - [queries], + (threadID: string) => queries.current[threadID] ?? '', + [], + ); + + const setQuery = React.useCallback( + (query: string, threadID: string) => { + clearResults(threadID); + queries.current = { ...queries.current, [threadID]: query }; + }, + [clearResults], + ); + + const clearQuery = React.useCallback( + (threadID: string) => { + clearResults(threadID); + delete queries.current[threadID]; + }, + [clearResults], + ); + + const searchMessagesCall = useSearchMessages(); + + const loading = React.useRef(false); + + const appendResults = React.useCallback( + ( + newMessages: $ReadOnlyArray, + end: boolean, + threadID: string, + ) => { + appendResult(newMessages, threadID); + if (end) { + setEndReached(threadID); + } + loading.current = false; + }, + [appendResult, setEndReached], + ); + + const searchMessages = React.useCallback( + (threadID: string) => { + if (loading.current || endsReached.current.has(threadID)) { + return; + } + loading.current = true; + searchMessagesCall( + queries.current[threadID], + threadID, + appendResults, + lastIDs.current[threadID], + ); + }, + [appendResults, endsReached, searchMessagesCall], ); const state = React.useMemo( @@ -46,8 +151,22 @@ getQuery, setQuery, clearQuery, + getSearchResults: getResults, + appendSearchResult: appendResult, + getEndReached, + setEndReached, + searchMessages, }), - [getQuery, setQuery, clearQuery], + [ + getQuery, + setQuery, + clearQuery, + getResults, + appendResult, + getEndReached, + setEndReached, + searchMessages, + ], ); return ( @@ -57,6 +176,18 @@ ); } +function oldestMessageID(data: $ReadOnlyArray) { + if (!data) { + return undefined; + } + for (let i = data.length - 1; i >= 0; i--) { + if (data[i].type === messageTypes.TEXT) { + return data[i].id; + } + } + return undefined; +} + function useMessageSearchContext(): MessageSearchState { const context = React.useContext(MessageSearchContext); invariant(context, 'MessageSearchContext not found');