diff --git a/lib/hooks/message-hooks.js b/lib/hooks/message-hooks.js --- a/lib/hooks/message-hooks.js +++ b/lib/hooks/message-hooks.js @@ -1,6 +1,21 @@ // @flow +import _max from 'lodash/fp/max.js'; +import _min from 'lodash/fp/min.js'; +import * as React from 'react'; + +import { + fetchLatestMessages, + fetchLatestMessagesActionTypes, +} from '../actions/message-actions.js'; +import { createLoadingStatusSelector } from '../selectors/loading-selectors.js'; import { getOldestNonLocalMessageID } from '../shared/message-utils.js'; +import type { LoadingStatus } from '../types/loading-types.js'; +import { + useDispatchActionPromise, + useServerCall, +} from '../utils/action-utils.js'; +import { values } from '../utils/objects.js'; import { useSelector } from '../utils/redux-utils.js'; function useOldestMessageServerID(threadID: string): ?string { @@ -9,4 +24,80 @@ ); } -export { useOldestMessageServerID }; +type FetchLatestMessagesState = + | { type: 'everything_loaded' } + | { type: 'loaded_upto_message', message: string }; + +function useFetchLatestMessages(home: boolean): { + fetchMoreLatestMessages: () => Promise, + loadingStatus: LoadingStatus, +} { + const dispatchActionPromise = useDispatchActionPromise(); + const callFetchLatestMessages = useServerCall(fetchLatestMessages); + + const messageStoreThreads = useSelector(state => state.messageStore.threads); + const [fetchState, setFetchState] = + React.useState(null); + + React.useEffect(() => { + if (values(messageStoreThreads).length === 0) { + setFetchState(null); + } else if (fetchState === null) { + const latestMessageForThread = thread => + _max(thread.messageIDs.map(Number)); + + const oldestLatestMessage = _min( + values(messageStoreThreads).map(latestMessageForThread), + ); + + if (oldestLatestMessage) { + setFetchState({ + type: 'loaded_upto_message', + message: oldestLatestMessage.toString(), + }); + } + } + }, [fetchState, messageStoreThreads]); + + const fetchMoreLatestMessages = React.useCallback(async () => { + if (fetchState?.type !== 'loaded_upto_message') { + return; + } + + await dispatchActionPromise( + fetchLatestMessagesActionTypes, + (async () => { + const result = await callFetchLatestMessages({ + home, + fromMessage: fetchState.message, + }); + + if ( + !result.oldestMessage || + result.oldestMessage === fetchState.message + ) { + setFetchState({ type: 'everything_loaded' }); + } else { + setFetchState({ + type: 'loaded_upto_message', + message: result.oldestMessage, + }); + } + + return result; + })(), + ); + }, [fetchState, dispatchActionPromise, callFetchLatestMessages, home]); + + const loadingStatusSelector = createLoadingStatusSelector( + fetchLatestMessagesActionTypes, + ); + const loadingStatus = useSelector(loadingStatusSelector); + + return { + fetchMoreLatestMessages, + loadingStatus, + }; +} + +export { useOldestMessageServerID, useFetchLatestMessages }; diff --git a/lib/selectors/chat-selectors.js b/lib/selectors/chat-selectors.js --- a/lib/selectors/chat-selectors.js +++ b/lib/selectors/chat-selectors.js @@ -127,12 +127,17 @@ messageStore: MessageStore, messages: { +[id: string]: ?MessageInfo }, sidebarInfos: ?$ReadOnlyArray, -): ChatThreadItem { +): ?ChatThreadItem { const mostRecentMessageInfo = getMostRecentMessageInfo( threadInfo, messageStore, messages, ); + + if (!mostRecentMessageInfo) { + return null; + } + const mostRecentNonLocalMessage = getMostRecentNonLocalMessageID( threadInfo.id, messageStore, @@ -256,7 +261,7 @@ ): $ReadOnlyArray { return _flow( _filter(filterFunction), - _map((threadInfo: ThreadInfo): ChatThreadItem => + _map((threadInfo: ThreadInfo): ?ChatThreadItem => createChatThreadItem( threadInfo, messageStore, @@ -264,6 +269,7 @@ sidebarInfos[threadInfo.id], ), ), + _filter(Boolean), _orderBy('lastUpdatedTimeIncludingSidebars')('desc'), )(threadInfos); } diff --git a/native/chat/chat-thread-list.react.js b/native/chat/chat-thread-list.react.js --- a/native/chat/chat-thread-list.react.js +++ b/native/chat/chat-thread-list.react.js @@ -18,6 +18,7 @@ import { searchUsers } from 'lib/actions/user-actions.js'; import { useLoggedInUserInfo } from 'lib/hooks/account-hooks.js'; +import { useFetchLatestMessages } from 'lib/hooks/message-hooks.js'; import { type ChatThreadItem, useFlattenedChatListData, @@ -29,6 +30,7 @@ createPendingThread, getThreadListSearchResults, } from 'lib/shared/thread-utils.js'; +import type { LoadingStatus } from 'lib/types/loading-types.js'; import type { UserSearchResult } from 'lib/types/search-types.js'; import { threadTypes } from 'lib/types/thread-types-enum.js'; import type { ThreadInfo } from 'lib/types/thread-types.js'; @@ -112,6 +114,8 @@ +navigateToThread: (params: MessageListParams) => void, // async functions that hit server APIs +searchUsers: (usernamePrefix: string) => Promise, + +fetchMoreLatestMessages: () => Promise, + +loadingStatus: LoadingStatus, }; type SearchStatus = 'inactive' | 'activating' | 'active'; type State = { @@ -215,6 +219,10 @@ this.searchCancelButtonOpen.setValue(0); } + if (this.listData.length < 3 && this.props.loadingStatus !== 'loading') { + this.props.fetchMoreLatestMessages(); + } + const { flatList } = this; if (!flatList) { return; @@ -440,12 +448,13 @@ } onEndReached = () => { - if (this.listData.length === this.fullListData.length) { - return; + if (this.listData.length !== this.fullListData.length) { + this.setState(prevState => ({ + numItemsToDisplay: prevState.numItemsToDisplay + 25, + })); + } else if (this.props.loadingStatus !== 'loading') { + this.props.fetchMoreLatestMessages(); } - this.setState(prevState => ({ - numItemsToDisplay: prevState.numItemsToDisplay + 25, - })); }; render() { @@ -488,6 +497,7 @@ scrollEnabled={scrollEnabled} onEndReached={this.onEndReached} onEndReachedThreshold={1} + refreshing={this.props.loadingStatus === 'loading'} ref={this.flatListRef} /> {floatingAction} @@ -629,6 +639,10 @@ const navigateToThread = useNavigateToThread(); + const { fetchMoreLatestMessages, loadingStatus } = useFetchLatestMessages( + props.route.name === 'HomeChatThreadList', + ); + return (