diff --git a/lib/hooks/search-sidebars.js b/lib/hooks/search-sidebars.js deleted file mode 100644 --- a/lib/hooks/search-sidebars.js +++ /dev/null @@ -1,97 +0,0 @@ -// @flow - -import * as React from 'react'; - -import { sidebarInfoSelector } from '../selectors/thread-selectors'; -import SearchIndex from '../shared/search-index'; -import { threadSearchText } from '../shared/thread-utils'; -import type { SetState } from '../types/hook-types'; -import type { SidebarInfo, ThreadInfo } from '../types/thread-types'; -import { useSelector } from '../utils/redux-utils'; - -type SidebarSearchState = { - +text: string, - +results: $ReadOnlySet, -}; - -function useSearchSidebars( - threadInfo: ThreadInfo, -): { - +listData: $ReadOnlyArray, - +searchState: SidebarSearchState, - +setSearchState: SetState, - +onChangeSearchInputText: (text: string) => mixed, - +clearQuery: (event: SyntheticEvent) => void, -} { - const [searchState, setSearchState] = React.useState({ - text: '', - results: new Set(), - }); - - const userInfos = useSelector(state => state.userStore.userInfos); - - const sidebarInfos = useSelector( - state => sidebarInfoSelector(state)[threadInfo.id] ?? [], - ); - - const listData = React.useMemo(() => { - if (!searchState.text) { - return sidebarInfos; - } - return sidebarInfos.filter(sidebarInfo => - searchState.results.has(sidebarInfo.threadInfo.id), - ); - }, [sidebarInfos, searchState]); - - const viewerID = useSelector( - state => state.currentUserInfo && state.currentUserInfo.id, - ); - const searchIndex = React.useMemo(() => { - const index = new SearchIndex(); - for (const sidebarInfo of sidebarInfos) { - const threadInfoFromSidebarInfo = sidebarInfo.threadInfo; - index.addEntry( - threadInfoFromSidebarInfo.id, - threadSearchText(threadInfoFromSidebarInfo, userInfos, viewerID), - ); - } - return index; - }, [sidebarInfos, userInfos, viewerID]); - - const onChangeSearchInputText = React.useCallback( - (text: string) => { - setSearchState({ - text, - results: new Set(searchIndex.getSearchResults(text)), - }); - }, - [searchIndex, setSearchState], - ); - - const clearQuery = React.useCallback( - (event: SyntheticEvent) => { - event.preventDefault(); - setSearchState({ text: '', results: new Set() }); - }, - [setSearchState], - ); - - return React.useMemo( - () => ({ - listData, - searchState, - setSearchState, - onChangeSearchInputText, - clearQuery, - }), - [ - listData, - setSearchState, - searchState, - onChangeSearchInputText, - clearQuery, - ], - ); -} - -export { useSearchSidebars }; diff --git a/lib/hooks/search-threads.js b/lib/hooks/search-threads.js new file mode 100644 --- /dev/null +++ b/lib/hooks/search-threads.js @@ -0,0 +1,139 @@ +// @flow + +import * as React from 'react'; + +import type { ChatThreadItem } from '../selectors/chat-selectors'; +import { useFilteredChatListData } from '../selectors/chat-selectors'; +import { threadInfosWithLastUpdatedTimeAndNonLocalMessageSelector } from '../selectors/thread-selectors'; +import SearchIndex from '../shared/search-index'; +import { threadInChatList, threadSearchText } from '../shared/thread-utils'; +import type { SetState } from '../types/hook-types'; +import type { + ThreadInfoWithLastUpdatedTimeAndNonLocalMessage, + ThreadInfo, + RawThreadInfo, +} from '../types/thread-types'; +import { threadTypes } from '../types/thread-types'; +import { useSelector } from '../utils/redux-utils'; + +export type ThreadSearchState = { + +text: string, + +results: $ReadOnlySet, +}; + +function useSearchThreads< + U: ThreadInfoWithLastUpdatedTimeAndNonLocalMessage | ChatThreadItem, +>( + threadInfo: ThreadInfo, + threadInfos: $ReadOnlyArray, +): { + +listData: $ReadOnlyArray, + +searchState: ThreadSearchState, + +setSearchState: SetState, + +onChangeSearchInputText: (text: string) => mixed, + +clearQuery: (event: SyntheticEvent) => void, +} { + const [searchState, setSearchState] = React.useState({ + text: '', + results: new Set(), + }); + + const userInfos = useSelector(state => state.userStore.userInfos); + + const listData = React.useMemo(() => { + if (!searchState.text) { + return threadInfos; + } + return threadInfos.filter(thread => + searchState.results.has(thread.threadInfo.id), + ); + }, [threadInfos, searchState]); + + const viewerID = useSelector( + state => state.currentUserInfo && state.currentUserInfo.id, + ); + const searchIndex = React.useMemo(() => { + const index = new SearchIndex(); + for (const thread of threadInfos) { + const threadInfoFromItem = thread.threadInfo; + index.addEntry( + threadInfoFromItem.id, + threadSearchText(threadInfoFromItem, userInfos, viewerID), + ); + } + return index; + }, [threadInfos, userInfos, viewerID]); + + const onChangeSearchInputText = React.useCallback( + (text: string) => { + setSearchState({ + text, + results: new Set(searchIndex.getSearchResults(text)), + }); + }, + [searchIndex, setSearchState], + ); + + const clearQuery = React.useCallback( + (event: SyntheticEvent) => { + event.preventDefault(); + setSearchState({ text: '', results: new Set() }); + }, + [setSearchState], + ); + + return React.useMemo( + () => ({ + listData, + searchState, + setSearchState, + onChangeSearchInputText, + clearQuery, + }), + [ + listData, + setSearchState, + searchState, + onChangeSearchInputText, + clearQuery, + ], + ); +} + +function useSearchSidebars( + threadInfo: ThreadInfo, +): { + +listData: $ReadOnlyArray, + +searchState: ThreadSearchState, + +setSearchState: SetState, + +onChangeSearchInputText: (text: string) => mixed, + +clearQuery: (event: SyntheticEvent) => void, +} { + const threadInfos = + useSelector( + threadInfosWithLastUpdatedTimeAndNonLocalMessageSelector( + (thread: ThreadInfo) => + !threadInChatList(thread) || thread.type !== threadTypes.SIDEBAR, + ), + )[threadInfo.id] ?? []; + return useSearchThreads(threadInfo, threadInfos); +} + +function useSearchSubchannels( + threadInfo: ThreadInfo, +): { + +listData: $ReadOnlyArray, + +searchState: ThreadSearchState, + +setSearchState: SetState, + +onChangeSearchInputText: (text: string) => mixed, + +clearQuery: (event: SyntheticEvent) => void, +} { + const threadInfos = useFilteredChatListData( + (thread: ?(ThreadInfo | RawThreadInfo)) => + thread?.type !== threadTypes.SIDEBAR && + thread?.parentThreadID === threadInfo.id, + ); + return useSearchThreads(threadInfo, threadInfos); +} + +export { useSearchSubchannels, useSearchSidebars }; 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 @@ -35,7 +35,7 @@ import { type ThreadInfo, type RawThreadInfo, - type SidebarInfo, + type ThreadInfoWithLastUpdatedTimeAndNonLocalMessage, maxReadSidebars, maxUnreadSidebars, threadTypes, @@ -51,7 +51,7 @@ export type SidebarItem = | { - ...SidebarInfo, + ...ThreadInfoWithLastUpdatedTimeAndNonLocalMessage, +type: 'sidebar', } | { @@ -121,7 +121,7 @@ threadInfo: ThreadInfo, messageStore: MessageStore, messages: { +[id: string]: ?MessageInfo }, - sidebarInfos: ?$ReadOnlyArray, + sidebarInfos: ?$ReadOnlyArray, ): ChatThreadItem { const mostRecentMessageInfo = getMostRecentMessageInfo( threadInfo, @@ -207,7 +207,9 @@ threadInfos: { +[id: string]: ThreadInfo }, messageStore: MessageStore, messageInfos: { +[id: string]: ?MessageInfo }, - sidebarInfos: { +[id: string]: $ReadOnlyArray }, + sidebarInfos: { + +[id: string]: $ReadOnlyArray, + }, ): $ReadOnlyArray => getChatThreadItems( threadInfos, @@ -247,7 +249,9 @@ threadInfos: { +[id: string]: ThreadInfo }, messageStore: MessageStore, messageInfos: { +[id: string]: ?MessageInfo }, - sidebarInfos: { +[id: string]: $ReadOnlyArray }, + sidebarInfos: { + +[id: string]: $ReadOnlyArray, + }, filterFunction: (threadInfo: ?(ThreadInfo | RawThreadInfo)) => boolean, ): $ReadOnlyArray { return _flow( diff --git a/lib/selectors/thread-selectors.js b/lib/selectors/thread-selectors.js --- a/lib/selectors/thread-selectors.js +++ b/lib/selectors/thread-selectors.js @@ -35,7 +35,7 @@ type RelativeMemberInfo, threadPermissions, threadTypes, - type SidebarInfo, + type ThreadInfoWithLastUpdatedTimeAndNonLocalMessage, threadTypeIsCommunityRoot, } from '../types/thread-types'; import { dateString, dateFromString } from '../utils/date-utils'; @@ -207,38 +207,50 @@ return null; } -const sidebarInfoSelector: ( +const threadInfosWithLastUpdatedTimeAndNonLocalMessageSelector: ( + threadRejectionCondition: (ThreadInfo) => boolean, +) => ( state: BaseAppState<*>, -) => { +[id: string]: $ReadOnlyArray } = createObjectSelector( - childThreadInfos, - (state: BaseAppState<*>) => state.messageStore, - (childThreads: $ReadOnlyArray, messageStore: MessageStore) => { - const sidebarInfos = []; - for (const childThreadInfo of childThreads) { - if ( - !threadInChatList(childThreadInfo) || - childThreadInfo.type !== threadTypes.SIDEBAR - ) { - continue; +) => { + +[id: string]: $ReadOnlyArray, +} = _memoize((threadRejectionCondition: ThreadInfo => boolean) => + createObjectSelector( + childThreadInfos, + (state: BaseAppState<*>) => state.messageStore, + (childThreads: $ReadOnlyArray, messageStore: MessageStore) => { + const sidebarInfos = []; + for (const childThreadInfo of childThreads) { + if (threadRejectionCondition(childThreadInfo)) { + continue; + } + const mostRecentRawMessageInfo = getMostRecentRawMessageInfo( + childThreadInfo, + messageStore, + ); + const lastUpdatedTime = + mostRecentRawMessageInfo?.time ?? childThreadInfo.creationTime; + const mostRecentNonLocalMessage = getMostRecentNonLocalMessageID( + childThreadInfo.id, + messageStore, + ); + sidebarInfos.push({ + threadInfo: childThreadInfo, + lastUpdatedTime, + mostRecentNonLocalMessage, + }); } - const mostRecentRawMessageInfo = getMostRecentRawMessageInfo( - childThreadInfo, - messageStore, - ); - const lastUpdatedTime = - mostRecentRawMessageInfo?.time ?? childThreadInfo.creationTime; - const mostRecentNonLocalMessage = getMostRecentNonLocalMessageID( - childThreadInfo.id, - messageStore, - ); - sidebarInfos.push({ - threadInfo: childThreadInfo, - lastUpdatedTime, - mostRecentNonLocalMessage, - }); - } - return _orderBy('lastUpdatedTime')('desc')(sidebarInfos); - }, + return _orderBy('lastUpdatedTime')('desc')(sidebarInfos); + }, + ), +); + +const sidebarInfoSelector: ( + state: BaseAppState<*>, +) => { + +[id: string]: $ReadOnlyArray, +} = threadInfosWithLastUpdatedTimeAndNonLocalMessageSelector( + (thread: ThreadInfo) => + !threadInChatList(thread) || thread.type !== threadTypes.SIDEBAR, ); const unreadCount: (state: BaseAppState<*>) => number = createSelector( @@ -430,4 +442,5 @@ sidebarInfoSelector, threadInfoFromSourceMessageIDSelector, pendingToRealizedThreadIDsSelector, + threadInfosWithLastUpdatedTimeAndNonLocalMessageSelector, }; diff --git a/lib/types/thread-types.js b/lib/types/thread-types.js --- a/lib/types/thread-types.js +++ b/lib/types/thread-types.js @@ -434,7 +434,7 @@ +userInfos: $ReadOnlyArray, }; -export type SidebarInfo = { +export type ThreadInfoWithLastUpdatedTimeAndNonLocalMessage = { +threadInfo: ThreadInfo, +lastUpdatedTime: number, +mostRecentNonLocalMessage: ?string, diff --git a/native/chat/chat-thread-list-sidebar.react.js b/native/chat/chat-thread-list-sidebar.react.js --- a/native/chat/chat-thread-list-sidebar.react.js +++ b/native/chat/chat-thread-list-sidebar.react.js @@ -3,7 +3,10 @@ import * as React from 'react'; import { View } from 'react-native'; -import type { ThreadInfo, SidebarInfo } from 'lib/types/thread-types'; +import type { + ThreadInfo, + ThreadInfoWithLastUpdatedTimeAndNonLocalMessage, +} from 'lib/types/thread-types'; import ExtendedArrow from '../components/arrow-extended.react'; import Arrow from '../components/arrow.react'; @@ -14,7 +17,7 @@ import SwipeableThread from './swipeable-thread.react'; type Props = { - +sidebarInfo: SidebarInfo, + +sidebarInfo: ThreadInfoWithLastUpdatedTimeAndNonLocalMessage, +onPressItem: (threadInfo: ThreadInfo) => void, +onSwipeableWillOpen: (threadInfo: ThreadInfo) => void, +currentlyOpenedSwipeableId: string, diff --git a/native/chat/sidebar-item.react.js b/native/chat/sidebar-item.react.js --- a/native/chat/sidebar-item.react.js +++ b/native/chat/sidebar-item.react.js @@ -3,14 +3,14 @@ import * as React from 'react'; import { Text, View } from 'react-native'; -import type { SidebarInfo } from 'lib/types/thread-types'; +import type { ThreadInfoWithLastUpdatedTimeAndNonLocalMessage } from 'lib/types/thread-types'; import { shortAbsoluteDate } from 'lib/utils/date-utils'; import { SingleLine } from '../components/single-line.react'; import { useStyles } from '../themes/colors'; type Props = { - +sidebarInfo: SidebarInfo, + +sidebarInfo: ThreadInfoWithLastUpdatedTimeAndNonLocalMessage, }; function SidebarItem(props: Props): React.Node { const { lastUpdatedTime } = props.sidebarInfo; diff --git a/native/chat/sidebar-list-modal.react.js b/native/chat/sidebar-list-modal.react.js --- a/native/chat/sidebar-list-modal.react.js +++ b/native/chat/sidebar-list-modal.react.js @@ -3,8 +3,11 @@ import * as React from 'react'; import { TextInput, FlatList, View } from 'react-native'; -import { useSearchSidebars } from 'lib/hooks/search-sidebars'; -import type { ThreadInfo, SidebarInfo } from 'lib/types/thread-types'; +import { useSearchSidebars } from 'lib/hooks/search-threads'; +import type { + ThreadInfo, + ThreadInfoWithLastUpdatedTimeAndNonLocalMessage, +} from 'lib/types/thread-types'; import ExtendedArrow from '../components/arrow-extended.react'; import Arrow from '../components/arrow.react'; @@ -22,10 +25,15 @@ +threadInfo: ThreadInfo, }; -function keyExtractor(sidebarInfo: SidebarInfo) { +function keyExtractor( + sidebarInfo: ThreadInfoWithLastUpdatedTimeAndNonLocalMessage, +) { return sidebarInfo.threadInfo.id; } -function getItemLayout(data: ?$ReadOnlyArray, index: number) { +function getItemLayout( + data: ?$ReadOnlyArray, + index: number, +) { return { length: 24, offset: 24 * index, index }; } @@ -79,7 +87,11 @@ ); const renderItem = React.useCallback( - (row: { item: SidebarInfo, index: number, ... }) => { + (row: { + item: ThreadInfoWithLastUpdatedTimeAndNonLocalMessage, + index: number, + ... + }) => { let extendArrow: boolean = false; if (row.index < numOfSidebarsWithExtendedArrow) { extendArrow = true; @@ -119,7 +131,7 @@ } function Item(props: { - item: SidebarInfo, + item: ThreadInfoWithLastUpdatedTimeAndNonLocalMessage, onPressItem: (threadInfo: ThreadInfo) => void, extendArrow: boolean, }): React.Node { diff --git a/web/chat/chat-thread-list-sidebar.react.js b/web/chat/chat-thread-list-sidebar.react.js --- a/web/chat/chat-thread-list-sidebar.react.js +++ b/web/chat/chat-thread-list-sidebar.react.js @@ -3,7 +3,7 @@ import classNames from 'classnames'; import * as React from 'react'; -import type { SidebarInfo } from 'lib/types/thread-types'; +import type { ThreadInfoWithLastUpdatedTimeAndNonLocalMessage } from 'lib/types/thread-types'; import { useOnClickThread, @@ -14,7 +14,7 @@ import SidebarItem from './sidebar-item.react'; type Props = { - +sidebarInfo: SidebarInfo, + +sidebarInfo: ThreadInfoWithLastUpdatedTimeAndNonLocalMessage, +isSubsequentItem: boolean, }; function ChatThreadListSidebar(props: Props): React.Node { diff --git a/web/chat/sidebar-item.react.js b/web/chat/sidebar-item.react.js --- a/web/chat/sidebar-item.react.js +++ b/web/chat/sidebar-item.react.js @@ -3,13 +3,13 @@ import classNames from 'classnames'; import * as React from 'react'; -import type { SidebarInfo } from 'lib/types/thread-types'; +import type { ThreadInfoWithLastUpdatedTimeAndNonLocalMessage } from 'lib/types/thread-types'; import { useOnClickThread } from '../selectors/thread-selectors'; import css from './chat-thread-list.css'; type Props = { - +sidebarInfo: SidebarInfo, + +sidebarInfo: ThreadInfoWithLastUpdatedTimeAndNonLocalMessage, +extendArrow?: boolean, }; function SidebarItem(props: Props): React.Node { diff --git a/web/selectors/chat-selectors.js b/web/selectors/chat-selectors.js --- a/web/selectors/chat-selectors.js +++ b/web/selectors/chat-selectors.js @@ -14,7 +14,10 @@ } from 'lib/selectors/thread-selectors'; import { threadIsPending } from 'lib/shared/thread-utils'; import type { MessageStore, MessageInfo } from 'lib/types/message-types'; -import type { ThreadInfo, SidebarInfo } from 'lib/types/thread-types'; +import type { + ThreadInfo, + ThreadInfoWithLastUpdatedTimeAndNonLocalMessage, +} from 'lib/types/thread-types'; import type { AppState } from '../redux/redux-setup'; import { useSelector } from '../redux/redux-utils'; @@ -34,7 +37,9 @@ messageInfos: { +[id: string]: ?MessageInfo }, activeChatThreadID: ?string, pendingThreadInfo: ?ThreadInfo, - sidebarInfos: { +[id: string]: $ReadOnlyArray }, + sidebarInfos: { + +[id: string]: $ReadOnlyArray, + }, ): ?ChatThreadItem => { if (!activeChatThreadID) { return null;