diff --git a/native/chat/message-result.react.js b/native/chat/message-result.react.js --- a/native/chat/message-result.react.js +++ b/native/chat/message-result.react.js @@ -7,7 +7,7 @@ import { type ThreadInfo } from 'lib/types/thread-types.js'; import { longAbsoluteDate } from 'lib/utils/date-utils.js'; -import type { ChatNavigationProp } from './chat.react'; +import { type ChatNavigationProp } from './chat.react.js'; import { MessageListContextProvider } from './message-list-types.js'; import { Message } from './message.react.js'; import { modifyItemForResultScreen } from './utils.js'; @@ -22,10 +22,12 @@ +threadInfo: ThreadInfo, +navigation: | AppNavigationProp<'TogglePinModal'> - | ChatNavigationProp<'MessageResultsScreen'>, + | ChatNavigationProp<'MessageResultsScreen'> + | ChatNavigationProp<'MessageSearch'>, +route: | NavigationRoute<'TogglePinModal'> - | NavigationRoute<'MessageResultsScreen'>, + | NavigationRoute<'MessageResultsScreen'> + | NavigationRoute<'MessageSearch'>, +messageVerticalBounds: ?VerticalBounds, }; diff --git a/native/chat/message.react.js b/native/chat/message.react.js --- a/native/chat/message.react.js +++ b/native/chat/message.react.js @@ -32,11 +32,13 @@ +navigation: | ChatNavigationProp<'MessageList'> | AppNavigationProp<'TogglePinModal'> - | ChatNavigationProp<'MessageResultsScreen'>, + | ChatNavigationProp<'MessageResultsScreen'> + | ChatNavigationProp<'MessageSearch'>, +route: | NavigationRoute<'MessageList'> | NavigationRoute<'TogglePinModal'> - | NavigationRoute<'MessageResultsScreen'>, + | NavigationRoute<'MessageResultsScreen'> + | NavigationRoute<'MessageSearch'>, +toggleFocus: (messageKey: string) => void, +verticalBounds: ?VerticalBounds, shouldDisplayPinIndicator: boolean, diff --git a/native/chat/robotext-message.react.js b/native/chat/robotext-message.react.js --- a/native/chat/robotext-message.react.js +++ b/native/chat/robotext-message.react.js @@ -31,11 +31,13 @@ +navigation: | ChatNavigationProp<'MessageList'> | AppNavigationProp<'TogglePinModal'> - | ChatNavigationProp<'MessageResultsScreen'>, + | ChatNavigationProp<'MessageResultsScreen'> + | ChatNavigationProp<'MessageSearch'>, +route: | NavigationRoute<'MessageList'> | NavigationRoute<'TogglePinModal'> - | NavigationRoute<'MessageResultsScreen'>, + | NavigationRoute<'MessageResultsScreen'> + | NavigationRoute<'MessageSearch'>, +focused: boolean, +toggleFocus: (messageKey: string) => void, +verticalBounds: ?VerticalBounds, diff --git a/native/chat/text-message.react.js b/native/chat/text-message.react.js --- a/native/chat/text-message.react.js +++ b/native/chat/text-message.react.js @@ -41,11 +41,13 @@ +navigation: | ChatNavigationProp<'MessageList'> | AppNavigationProp<'TogglePinModal'> - | ChatNavigationProp<'MessageResultsScreen'>, + | ChatNavigationProp<'MessageResultsScreen'> + | ChatNavigationProp<'MessageSearch'>, +route: | NavigationRoute<'MessageList'> | NavigationRoute<'TogglePinModal'> - | NavigationRoute<'MessageResultsScreen'>, + | NavigationRoute<'MessageResultsScreen'> + | NavigationRoute<'MessageSearch'>, +focused: boolean, +toggleFocus: (messageKey: string) => void, +verticalBounds: ?VerticalBounds, diff --git a/native/search/message-search.react.js b/native/search/message-search.react.js --- a/native/search/message-search.react.js +++ b/native/search/message-search.react.js @@ -3,15 +3,25 @@ import invariant from 'invariant'; import * as React from 'react'; import { View } from 'react-native'; +import { FlatList } from 'react-native-gesture-handler'; -import type { MessageInfo } from 'lib/types/message-types.js'; +import { messageListData } from 'lib/selectors/chat-selectors.js'; +import { createMessageInfo } from 'lib/shared/message-utils.js'; +import { useSearchMessages } from 'lib/shared/search-utils.js'; +import type { RawMessageInfo } from 'lib/types/message-types.js'; import type { ThreadInfo } from 'lib/types/thread-types.js'; import SearchFooter from './search-footer.react.js'; import { MessageSearchContext } from './search-provider.react.js'; +import { useHeightMeasurer } from '../chat/chat-context.js'; import type { ChatNavigationProp } from '../chat/chat.react.js'; +import { MessageListContextProvider } from '../chat/message-list-types.js'; +import MessageResult from '../chat/message-result.react.js'; +import ListLoadingIndicator from '../components/list-loading-indicator.react.js'; import type { NavigationRoute } from '../navigation/route-names.js'; +import { useSelector } from '../redux/redux-utils.js'; import { useStyles } from '../themes/colors.js'; +import type { ChatMessageItemWithHeight } from '../types/chat-types.js'; export type MessageSearchParams = { +threadInfo: ThreadInfo, @@ -26,43 +36,186 @@ const searchContext = React.useContext(MessageSearchContext); invariant(searchContext, 'searchContext should be set'); const { query, clearQuery } = searchContext; + const { threadInfo } = props.route.params; React.useEffect(() => { - return props.navigation.addListener('beforeRemove', () => { - clearQuery(); - }); + return props.navigation.addListener('beforeRemove', clearQuery); }, [props.navigation, clearQuery]); - // eslint-disable-next-line no-unused-vars const [lastID, setLastID] = React.useState(); - - // eslint-disable-next-line no-unused-vars const [searchResults, setSearchResults] = React.useState([]); + const [endReached, setEndReached] = React.useState(false); - // eslint-disable-next-line no-unused-vars const appendSearchResults = React.useCallback( - (newMessages: $ReadOnlyArray) => { + (newMessages: $ReadOnlyArray, end: boolean) => { setSearchResults(oldMessages => [...oldMessages, ...newMessages]); + setEndReached(end); }, [], ); + const searchMessages = useSearchMessages(); + React.useEffect(() => { setSearchResults([]); setLastID(undefined); - }, [query]); + setEndReached(false); + }, [query, searchMessages]); + + React.useEffect( + () => searchMessages(query, threadInfo.id, appendSearchResults, lastID), + [appendSearchResults, lastID, query, searchMessages, threadInfo.id], + ); + + const userInfos = useSelector(state => state.userStore.userInfos); + + const translatedSearchResults = React.useMemo(() => { + const threadInfos = { [threadInfo.id]: threadInfo }; + return searchResults + .map(rawMessageInfo => + createMessageInfo(rawMessageInfo, null, userInfos, threadInfos), + ) + .filter(Boolean); + }, [searchResults, threadInfo, userInfos]); + + const chatMessageInfos = useSelector( + messageListData(threadInfo.id, translatedSearchResults), + ); + + const filteredChatMessageInfos = React.useMemo(() => { + if (!chatMessageInfos) { + return null; + } + + const idSet = new Set(translatedSearchResults.map(item => item.id)); + + const chatMessageInfoItems = chatMessageInfos.filter( + item => item.messageInfo && idSet.has(item.messageInfo.id), + ); + + const uniqueChatMessageInfoItemsMap = new Map(); + chatMessageInfoItems.forEach( + item => + item.messageInfo && + item.messageInfo.id && + uniqueChatMessageInfoItemsMap.set(item.messageInfo.id, item), + ); + + const sortedChatMessageInfoItems = []; + for (let i = 0; i < translatedSearchResults.length; i++) { + sortedChatMessageInfoItems.push( + uniqueChatMessageInfoItemsMap.get(translatedSearchResults[i].id), + ); + } + if (!endReached) { + sortedChatMessageInfoItems.push({ itemType: 'loader' }); + } + + return sortedChatMessageInfoItems.filter(Boolean); + }, [chatMessageInfos, endReached, translatedSearchResults]); + + const [measuredMessages, setMeasuredMessages] = React.useState([]); + + const measureMessages = useHeightMeasurer(); + const measureCallback = React.useCallback( + (listDataWithHeights: $ReadOnlyArray) => { + setMeasuredMessages(listDataWithHeights); + }, + [setMeasuredMessages], + ); + + React.useEffect(() => { + measureMessages(filteredChatMessageInfos, threadInfo, measureCallback); + }, [filteredChatMessageInfos, measureCallback, measureMessages, threadInfo]); + + const [messageVerticalBounds, setMessageVerticalBounds] = React.useState(); + const scrollViewContainerRef = React.useRef(); + + const onLayout = React.useCallback(() => { + scrollViewContainerRef.current?.measure( + (x, y, width, height, pageX, pageY) => { + if ( + height === null || + height === undefined || + pageY === null || + pageY === undefined + ) { + return; + } + + setMessageVerticalBounds({ height, y: pageY }); + }, + ); + }, []); + + const renderItem = React.useCallback( + ({ item }) => { + if (item.itemType === 'loader') { + return ; + } + return ( + + ); + }, + [messageVerticalBounds, props.navigation, props.route, threadInfo], + ); + + const footer = React.useMemo(() => { + if (query === '') { + return ; + } + if (!endReached) { + return null; + } + if (measuredMessages.length > 0) { + return ; + } + const text = + 'No results, please try using different keywords to refine your search'; + return ; + }, [query, endReached, measuredMessages.length]); + + const onEndOfLoadedMessagesReached = React.useCallback(() => { + if (endReached) { + return; + } + setLastID(oldestMessageID(measuredMessages)); + }, [endReached, measuredMessages, setLastID]); const styles = useStyles(unboundStyles); - if (query === '') { - return ( - - + return ( + + + - ); - } + + ); +} - return null; +function oldestMessageID(data: $ReadOnlyArray) { + for (let i = data.length - 1; i >= 0; i--) { + if (data[i].itemType === 'message' && data[i].messageInfo.id) { + return data[i].messageInfo.id; + } + } + return undefined; } const unboundStyles = {