diff --git a/native/chat/message-result.react.js b/native/chat/message-result.react.js index 4932db7f0..091b50a6d 100644 --- a/native/chat/message-result.react.js +++ b/native/chat/message-result.react.js @@ -1,82 +1,84 @@ // @flow import * as React from 'react'; import { Text, View } from 'react-native'; import { ScrollView } from 'react-native-gesture-handler'; 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'; import type { AppNavigationProp } from '../navigation/app-navigator.react'; import type { NavigationRoute } from '../navigation/route-names'; import { useStyles } from '../themes/colors.js'; import type { ChatMessageInfoItemWithHeight } from '../types/chat-types.js'; import type { VerticalBounds } from '../types/layout-types.js'; type MessageResultProps = { +item: ChatMessageInfoItemWithHeight, +threadInfo: ThreadInfo, +navigation: | AppNavigationProp<'TogglePinModal'> - | ChatNavigationProp<'MessageResultsScreen'>, + | ChatNavigationProp<'MessageResultsScreen'> + | ChatNavigationProp<'MessageSearch'>, +route: | NavigationRoute<'TogglePinModal'> - | NavigationRoute<'MessageResultsScreen'>, + | NavigationRoute<'MessageResultsScreen'> + | NavigationRoute<'MessageSearch'>, +messageVerticalBounds: ?VerticalBounds, }; function MessageResult(props: MessageResultProps): React.Node { const styles = useStyles(unboundStyles); const onToggleFocus = React.useCallback(() => {}, []); const item = React.useMemo( () => modifyItemForResultScreen(props.item), [props.item], ); return ( {longAbsoluteDate(props.item.messageInfo.time)} ); } const unboundStyles = { container: { marginTop: 5, backgroundColor: 'panelForeground', overflow: 'scroll', maxHeight: 400, }, viewContainer: { marginTop: 10, marginBottom: 10, }, messageDate: { color: 'messageLabel', fontSize: 12, marginLeft: 55, }, }; export default MessageResult; diff --git a/native/chat/message.react.js b/native/chat/message.react.js index 79eaa063c..c1f44ea1b 100644 --- a/native/chat/message.react.js +++ b/native/chat/message.react.js @@ -1,151 +1,153 @@ // @flow import _isEqual from 'lodash/fp/isEqual.js'; import * as React from 'react'; import { LayoutAnimation, TouchableWithoutFeedback, PixelRatio, } from 'react-native'; import shallowequal from 'shallowequal'; import { messageKey } from 'lib/shared/message-utils.js'; import type { ChatNavigationProp } from './chat.react.js'; import MultimediaMessage from './multimedia-message.react.js'; import { RobotextMessage } from './robotext-message.react.js'; import { TextMessage } from './text-message.react.js'; import { messageItemHeight } from './utils.js'; import { type KeyboardState, KeyboardContext, } from '../keyboard/keyboard-state.js'; import type { AppNavigationProp } from '../navigation/app-navigator.react'; import type { NavigationRoute } from '../navigation/route-names.js'; import type { ChatMessageInfoItemWithHeight } from '../types/chat-types.js'; import { type VerticalBounds } from '../types/layout-types.js'; import type { LayoutEvent } from '../types/react-native.js'; type BaseProps = { +item: ChatMessageInfoItemWithHeight, +focused: boolean, +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, }; type Props = { ...BaseProps, +keyboardState: ?KeyboardState, }; class Message extends React.Component { shouldComponentUpdate(nextProps: Props): boolean { const { item, ...props } = this.props; const { item: nextItem, ...newProps } = nextProps; return !_isEqual(item, nextItem) || !shallowequal(props, newProps); } componentDidUpdate(prevProps: Props) { if ( (prevProps.focused || prevProps.item.startsConversation) !== (this.props.focused || this.props.item.startsConversation) ) { LayoutAnimation.easeInEaseOut(); } } render() { let message; if (this.props.item.messageShapeType === 'text') { message = ( ); } else if (this.props.item.messageShapeType === 'multimedia') { message = ( ); } else { message = ( ); } const onLayout = __DEV__ ? this.onLayout : undefined; return ( {message} ); } onLayout = (event: LayoutEvent) => { if (this.props.focused) { return; } const measuredHeight = event.nativeEvent.layout.height; const expectedHeight = messageItemHeight(this.props.item); const pixelRatio = 1 / PixelRatio.get(); const distance = Math.abs(measuredHeight - expectedHeight); if (distance < pixelRatio) { return; } const approxMeasuredHeight = Math.round(measuredHeight * 100) / 100; const approxExpectedHeight = Math.round(expectedHeight * 100) / 100; console.log( `Message height for ${this.props.item.messageShapeType} ` + `${messageKey(this.props.item.messageInfo)} was expected to be ` + `${approxExpectedHeight} but is actually ${approxMeasuredHeight}. ` + "This means MessageList's FlatList isn't getting the right item " + 'height for some of its nodes, which is guaranteed to cause glitchy ' + 'behavior. Please investigate!!', ); }; dismissKeyboard = () => { const { keyboardState } = this.props; keyboardState && keyboardState.dismissKeyboard(); }; } const ConnectedMessage: React.ComponentType = React.memo( function ConnectedMessage(props: BaseProps) { const keyboardState = React.useContext(KeyboardContext); return ; }, ); export { ConnectedMessage as Message }; diff --git a/native/chat/robotext-message.react.js b/native/chat/robotext-message.react.js index c81d4e1d1..431c6c694 100644 --- a/native/chat/robotext-message.react.js +++ b/native/chat/robotext-message.react.js @@ -1,221 +1,223 @@ // @flow import invariant from 'invariant'; import * as React from 'react'; import { View } from 'react-native'; import { messageKey } from 'lib/shared/message-utils.js'; import { useCanCreateSidebarFromMessage } from 'lib/shared/thread-utils.js'; import { inlineEngagementCenterStyle } from './chat-constants.js'; import type { ChatNavigationProp } from './chat.react.js'; import { InlineEngagement } from './inline-engagement.react.js'; import { InnerRobotextMessage } from './inner-robotext-message.react.js'; import { Timestamp } from './timestamp.react.js'; import { getMessageTooltipKey, useContentAndHeaderOpacity } from './utils.js'; import { ChatContext } from '../chat/chat-context.js'; import { KeyboardContext } from '../keyboard/keyboard-state.js'; import type { AppNavigationProp } from '../navigation/app-navigator.react'; import { OverlayContext } from '../navigation/overlay-context.js'; import { RobotextMessageTooltipModalRouteName } from '../navigation/route-names.js'; import type { NavigationRoute } from '../navigation/route-names.js'; import { useStyles } from '../themes/colors.js'; import { fixedTooltipHeight } from '../tooltip/tooltip.react.js'; import type { ChatRobotextMessageInfoItemWithHeight } from '../types/chat-types.js'; import type { VerticalBounds } from '../types/layout-types.js'; import { AnimatedView } from '../types/styles.js'; type Props = { ...React.ElementConfig, +item: ChatRobotextMessageInfoItemWithHeight, +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, }; function RobotextMessage(props: Props): React.Node { const { item, navigation, route, focused, toggleFocus, verticalBounds, ...viewProps } = props; let timestamp = null; if (focused || item.startsConversation) { timestamp = ( ); } const styles = useStyles(unboundStyles); let inlineEngagement = null; if (item.threadCreatedFromMessage || Object.keys(item.reactions).length > 0) { inlineEngagement = ( ); } const chatContext = React.useContext(ChatContext); const keyboardState = React.useContext(KeyboardContext); const key = messageKey(item.messageInfo); const onPress = React.useCallback(() => { const didDismiss = keyboardState && keyboardState.dismissKeyboardIfShowing(); if (!didDismiss) { toggleFocus(key); } }, [keyboardState, toggleFocus, key]); const overlayContext = React.useContext(OverlayContext); const viewRef = React.useRef>(); const canCreateSidebarFromMessage = useCanCreateSidebarFromMessage( item.threadInfo, item.messageInfo, ); const visibleEntryIDs = React.useMemo(() => { const result = []; if (item.threadCreatedFromMessage || canCreateSidebarFromMessage) { result.push('sidebar'); } return result; }, [item.threadCreatedFromMessage, canCreateSidebarFromMessage]); const openRobotextTooltipModal = React.useCallback( (x, y, width, height, pageX, pageY) => { invariant( verticalBounds, 'verticalBounds should be present in openRobotextTooltipModal', ); const coordinates = { x: pageX, y: pageY, width, height }; const messageTop = pageY; const messageBottom = pageY + height; const boundsTop = verticalBounds.y; const boundsBottom = verticalBounds.y + verticalBounds.height; const belowMargin = 20; const belowSpace = fixedTooltipHeight + belowMargin; const { isViewer } = item.messageInfo.creator; const aboveMargin = isViewer ? 30 : 50; const aboveSpace = fixedTooltipHeight + aboveMargin; let margin = 0; if ( messageBottom + belowSpace > boundsBottom && messageTop - aboveSpace > boundsTop ) { margin = aboveMargin; } const currentInputBarHeight = chatContext?.chatInputBarHeights.get(item.threadInfo.id) ?? 0; props.navigation.navigate<'RobotextMessageTooltipModal'>({ name: RobotextMessageTooltipModalRouteName, params: { presentedFrom: props.route.key, initialCoordinates: coordinates, verticalBounds, visibleEntryIDs, tooltipLocation: 'fixed', margin, item, chatInputBarHeight: currentInputBarHeight, }, key: getMessageTooltipKey(item), }); }, [ item, props.navigation, props.route.key, verticalBounds, visibleEntryIDs, chatContext, ], ); const onLongPress = React.useCallback(() => { if (keyboardState && keyboardState.dismissKeyboardIfShowing()) { return; } if (visibleEntryIDs.length === 0) { return; } if (!viewRef.current || !verticalBounds) { return; } if (!focused) { toggleFocus(messageKey(item.messageInfo)); } invariant(overlayContext, 'RobotextMessage should have OverlayContext'); overlayContext.setScrollBlockingModalStatus('open'); viewRef.current?.measure(openRobotextTooltipModal); }, [ focused, item, keyboardState, overlayContext, toggleFocus, verticalBounds, viewRef, visibleEntryIDs, openRobotextTooltipModal, ]); const onLayout = React.useCallback(() => {}, []); const contentAndHeaderOpacity = useContentAndHeaderOpacity(item); return ( {timestamp} {inlineEngagement} ); } const unboundStyles = { sidebar: { marginTop: inlineEngagementCenterStyle.topOffset, marginBottom: -inlineEngagementCenterStyle.topOffset, alignSelf: 'center', }, }; export { RobotextMessage }; diff --git a/native/chat/text-message.react.js b/native/chat/text-message.react.js index 9cd4bcc78..093a2d2aa 100644 --- a/native/chat/text-message.react.js +++ b/native/chat/text-message.react.js @@ -1,296 +1,298 @@ // @flow import invariant from 'invariant'; import * as React from 'react'; import { View } from 'react-native'; import { useCanEditMessage } from 'lib/shared/edit-messages-utils.js'; import { messageKey } from 'lib/shared/message-utils.js'; import { threadHasPermission, useCanCreateSidebarFromMessage, } from 'lib/shared/thread-utils.js'; import { threadPermissions } from 'lib/types/thread-permission-types.js'; import type { ChatNavigationProp } from './chat.react.js'; import ComposedMessage from './composed-message.react.js'; import { InnerTextMessage } from './inner-text-message.react.js'; import { MessagePressResponderContext, type MessagePressResponderContextType, } from './message-press-responder-context.js'; import textMessageSendFailed from './text-message-send-failed.js'; import { getMessageTooltipKey } from './utils.js'; import { ChatContext, type ChatContextType } from '../chat/chat-context.js'; import { MarkdownContext } from '../markdown/markdown-context.js'; import type { AppNavigationProp } from '../navigation/app-navigator.react'; import { OverlayContext, type OverlayContextType, } from '../navigation/overlay-context.js'; import type { NavigationRoute } from '../navigation/route-names.js'; import { TextMessageTooltipModalRouteName } from '../navigation/route-names.js'; import { fixedTooltipHeight } from '../tooltip/tooltip.react.js'; import type { ChatTextMessageInfoItemWithHeight } from '../types/chat-types.js'; import type { VerticalBounds } from '../types/layout-types.js'; import { useShouldRenderEditButton } from '../utils/edit-messages-utils.js'; type BaseProps = { ...React.ElementConfig, +item: ChatTextMessageInfoItemWithHeight, +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, +shouldDisplayPinIndicator: boolean, }; type Props = { ...BaseProps, // Redux state +canCreateSidebarFromMessage: boolean, // withOverlayContext +overlayContext: ?OverlayContextType, // ChatContext +chatContext: ?ChatContextType, // MarkdownContext +isLinkModalActive: boolean, +canEditMessage: boolean, +shouldRenderEditButton: boolean, +canTogglePins: boolean, }; class TextMessage extends React.PureComponent { message: ?React.ElementRef; messagePressResponderContext: MessagePressResponderContextType; constructor(props: Props) { super(props); this.messagePressResponderContext = { onPressMessage: this.onPress, }; } render() { const { item, navigation, route, focused, toggleFocus, verticalBounds, shouldDisplayPinIndicator, overlayContext, chatContext, isLinkModalActive, canCreateSidebarFromMessage, canEditMessage, shouldRenderEditButton, canTogglePins, ...viewProps } = this.props; let swipeOptions = 'none'; const canReply = this.canReply(); const canNavigateToSidebar = this.canNavigateToSidebar(); if (isLinkModalActive) { swipeOptions = 'none'; } else if (canReply && canNavigateToSidebar) { swipeOptions = 'both'; } else if (canReply) { swipeOptions = 'reply'; } else if (canNavigateToSidebar) { swipeOptions = 'sidebar'; } return ( ); } messageRef = (message: ?React.ElementRef) => { this.message = message; }; canReply() { return threadHasPermission( this.props.item.threadInfo, threadPermissions.VOICED, ); } canNavigateToSidebar() { return ( this.props.item.threadCreatedFromMessage || this.props.canCreateSidebarFromMessage ); } visibleEntryIDs() { const result = ['copy']; if (this.canReply()) { result.push('reply'); } if (this.props.canEditMessage && this.props.shouldRenderEditButton) { result.push('edit'); } if (this.props.canTogglePins) { this.props.item.isPinned ? result.push('unpin') : result.push('pin'); } if ( this.props.item.threadCreatedFromMessage || this.props.canCreateSidebarFromMessage ) { result.push('sidebar'); } if (!this.props.item.messageInfo.creator.isViewer) { result.push('report'); } return result; } onPress = () => { const visibleEntryIDs = this.visibleEntryIDs(); if (visibleEntryIDs.length === 0) { return; } const { message, props: { verticalBounds, isLinkModalActive }, } = this; if (!message || !verticalBounds || isLinkModalActive) { return; } const { focused, toggleFocus, item } = this.props; if (!focused) { toggleFocus(messageKey(item.messageInfo)); } const { overlayContext } = this.props; invariant(overlayContext, 'TextMessage should have OverlayContext'); overlayContext.setScrollBlockingModalStatus('open'); message.measure((x, y, width, height, pageX, pageY) => { const coordinates = { x: pageX, y: pageY, width, height }; const messageTop = pageY; const messageBottom = pageY + height; const boundsTop = verticalBounds.y; const boundsBottom = verticalBounds.y + verticalBounds.height; const belowMargin = 20; const belowSpace = fixedTooltipHeight + belowMargin; const { isViewer } = item.messageInfo.creator; const aboveMargin = isViewer ? 30 : 50; const aboveSpace = fixedTooltipHeight + aboveMargin; let margin = belowMargin; if ( messageBottom + belowSpace > boundsBottom && messageTop - aboveSpace > boundsTop ) { margin = aboveMargin; } const currentInputBarHeight = this.props.chatContext?.chatInputBarHeights.get(item.threadInfo.id) ?? 0; this.props.navigation.navigate<'TextMessageTooltipModal'>({ name: TextMessageTooltipModalRouteName, params: { presentedFrom: this.props.route.key, initialCoordinates: coordinates, verticalBounds, visibleEntryIDs, tooltipLocation: 'fixed', margin, item, chatInputBarHeight: currentInputBarHeight, }, key: getMessageTooltipKey(item), }); }); }; } const ConnectedTextMessage: React.ComponentType = React.memo(function ConnectedTextMessage(props: BaseProps) { const overlayContext = React.useContext(OverlayContext); const chatContext = React.useContext(ChatContext); const markdownContext = React.useContext(MarkdownContext); invariant(markdownContext, 'markdownContext should be set'); const { linkModalActive, clearMarkdownContextData } = markdownContext; const key = messageKey(props.item.messageInfo); // We check if there is an key in the object - if not, we // default to false. The likely situation where the former statement // evaluates to null is when the thread is opened for the first time. const isLinkModalActive = linkModalActive[key] ?? false; const canCreateSidebarFromMessage = useCanCreateSidebarFromMessage( props.item.threadInfo, props.item.messageInfo, ); const shouldRenderEditButton = useShouldRenderEditButton(); const canEditMessage = useCanEditMessage( props.item.threadInfo, props.item.messageInfo, ); const canTogglePins = threadHasPermission( props.item.threadInfo, threadPermissions.MANAGE_PINS, ); React.useEffect(() => clearMarkdownContextData, [clearMarkdownContextData]); return ( ); }); export { ConnectedTextMessage as TextMessage }; diff --git a/native/search/message-search.react.js b/native/search/message-search.react.js index 30b2d6602..e93280fef 100644 --- a/native/search/message-search.react.js +++ b/native/search/message-search.react.js @@ -1,75 +1,228 @@ // @flow 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, }; export type MessageSearchProps = { +navigation: ChatNavigationProp<'MessageSearch'>, +route: NavigationRoute<'MessageSearch'>, }; function MessageSearch(props: MessageSearchProps): React.Node { 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 = { content: { height: '100%', backgroundColor: 'panelBackground', }, }; export default MessageSearch;