diff --git a/native/chat/message-list.react.js b/native/chat/message-list.react.js index 5082d6a4c..723e028ec 100644 --- a/native/chat/message-list.react.js +++ b/native/chat/message-list.react.js @@ -1,372 +1,372 @@ // @flow import invariant from 'invariant'; import _find from 'lodash/fp/find.js'; import * as React from 'react'; import { View, TouchableWithoutFeedback } from 'react-native'; import { createSelector } from 'reselect'; import { fetchMessagesBeforeCursorActionTypes, fetchMessagesBeforeCursor, fetchMostRecentMessagesActionTypes, fetchMostRecentMessages, } from 'lib/actions/message-actions.js'; import { useOldestMessageServerID } from 'lib/hooks/message-hooks.js'; import { registerFetchKey } from 'lib/reducers/loading-reducer.js'; import { messageKey } from 'lib/shared/message-utils.js'; import { useWatchThread } from 'lib/shared/thread-utils.js'; import type { FetchMessageInfosPayload } from 'lib/types/message-types.js'; import { threadTypes } from 'lib/types/thread-types-enum.js'; import { type ThreadInfo } from 'lib/types/thread-types.js'; import { type DispatchActionPromise, useServerCall, useDispatchActionPromise, } from 'lib/utils/action-utils.js'; import ChatList from './chat-list.react.js'; import type { ChatNavigationProp } from './chat.react.js'; -import { Message } from './message.react.js'; +import Message from './message.react.js'; import RelationshipPrompt from './relationship-prompt.react.js'; import ListLoadingIndicator from '../components/list-loading-indicator.react.js'; import { type KeyboardState, KeyboardContext, } from '../keyboard/keyboard-state.js'; import { defaultStackScreenOptions } from '../navigation/options.js'; import { OverlayContext, type OverlayContextType, } from '../navigation/overlay-context.js'; import type { NavigationRoute } from '../navigation/route-names.js'; import { useSelector } from '../redux/redux-utils.js'; import { useStyles, type IndicatorStyle, useIndicatorStyle, } from '../themes/colors.js'; import type { ChatMessageInfoItemWithHeight, ChatMessageItemWithHeight, } from '../types/chat-types.js'; import type { VerticalBounds } from '../types/layout-types.js'; import type { ViewableItemsChange } from '../types/react-native.js'; type BaseProps = { +threadInfo: ThreadInfo, +messageListData: $ReadOnlyArray, +navigation: ChatNavigationProp<'MessageList'>, +route: NavigationRoute<'MessageList'>, }; type Props = { ...BaseProps, +startReached: boolean, +styles: typeof unboundStyles, +indicatorStyle: IndicatorStyle, +dispatchActionPromise: DispatchActionPromise, +fetchMessagesBeforeCursor: ( threadID: string, beforeMessageID: string, ) => Promise, +fetchMostRecentMessages: ( threadID: string, ) => Promise, +overlayContext: ?OverlayContextType, +keyboardState: ?KeyboardState, +oldestMessageServerID: ?string, }; type State = { +focusedMessageKey: ?string, +messageListVerticalBounds: ?VerticalBounds, +loadingFromScroll: boolean, }; type PropsAndState = { ...Props, ...State, }; type FlatListExtraData = { messageListVerticalBounds: ?VerticalBounds, focusedMessageKey: ?string, navigation: ChatNavigationProp<'MessageList'>, route: NavigationRoute<'MessageList'>, }; class MessageList extends React.PureComponent { state: State = { focusedMessageKey: null, messageListVerticalBounds: null, loadingFromScroll: false, }; flatListContainer: ?React.ElementRef; flatListExtraDataSelector = createSelector( (propsAndState: PropsAndState) => propsAndState.messageListVerticalBounds, (propsAndState: PropsAndState) => propsAndState.focusedMessageKey, (propsAndState: PropsAndState) => propsAndState.navigation, (propsAndState: PropsAndState) => propsAndState.route, ( messageListVerticalBounds: ?VerticalBounds, focusedMessageKey: ?string, navigation: ChatNavigationProp<'MessageList'>, route: NavigationRoute<'MessageList'>, ) => ({ messageListVerticalBounds, focusedMessageKey, navigation, route, }), ); get flatListExtraData(): FlatListExtraData { return this.flatListExtraDataSelector({ ...this.props, ...this.state }); } static getOverlayContext(props: Props) { const { overlayContext } = props; invariant(overlayContext, 'MessageList should have OverlayContext'); return overlayContext; } static scrollDisabled(props: Props) { const overlayContext = MessageList.getOverlayContext(props); return overlayContext.scrollBlockingModalStatus !== 'closed'; } static modalOpen(props: Props) { const overlayContext = MessageList.getOverlayContext(props); return overlayContext.scrollBlockingModalStatus === 'open'; } componentDidUpdate(prevProps: Props) { const modalIsOpen = MessageList.modalOpen(this.props); const modalWasOpen = MessageList.modalOpen(prevProps); if (!modalIsOpen && modalWasOpen) { this.setState({ focusedMessageKey: null }); } if (defaultStackScreenOptions.gestureEnabled) { const scrollIsDisabled = MessageList.scrollDisabled(this.props); const scrollWasDisabled = MessageList.scrollDisabled(prevProps); if (!scrollWasDisabled && scrollIsDisabled) { this.props.navigation.setOptions({ gestureEnabled: false }); } else if (scrollWasDisabled && !scrollIsDisabled) { this.props.navigation.setOptions({ gestureEnabled: true }); } } } dismissKeyboard = () => { const { keyboardState } = this.props; keyboardState && keyboardState.dismissKeyboard(); }; renderItem = (row: { item: ChatMessageItemWithHeight, ... }) => { if (row.item.itemType === 'loader') { return ( ); } const messageInfoItem: ChatMessageInfoItemWithHeight = row.item; const { messageListVerticalBounds, focusedMessageKey, navigation, route } = this.flatListExtraData; const focused = messageKey(messageInfoItem.messageInfo) === focusedMessageKey; return ( ); }; toggleMessageFocus = (inMessageKey: string) => { if (this.state.focusedMessageKey === inMessageKey) { this.setState({ focusedMessageKey: null }); } else { this.setState({ focusedMessageKey: inMessageKey }); } }; // Actually header, it's just that our FlatList is inverted ListFooterComponent = () => ; render() { const { messageListData, startReached } = this.props; const footer = startReached ? this.ListFooterComponent : undefined; let relationshipPrompt = null; if (this.props.threadInfo.type === threadTypes.PERSONAL) { relationshipPrompt = ( ); } return ( {relationshipPrompt} ); } flatListContainerRef = ( flatListContainer: ?React.ElementRef, ) => { this.flatListContainer = flatListContainer; }; onFlatListContainerLayout = () => { const { flatListContainer } = this; if (!flatListContainer) { return; } flatListContainer.measure((x, y, width, height, pageX, pageY) => { if ( height === null || height === undefined || pageY === null || pageY === undefined ) { return; } this.setState({ messageListVerticalBounds: { height, y: pageY } }); }); }; onViewableItemsChanged = (info: ViewableItemsChange) => { if (this.state.focusedMessageKey) { let focusedMessageVisible = false; for (const token of info.viewableItems) { if ( token.item.itemType === 'message' && messageKey(token.item.messageInfo) === this.state.focusedMessageKey ) { focusedMessageVisible = true; break; } } if (!focusedMessageVisible) { this.setState({ focusedMessageKey: null }); } } const loader = _find({ key: 'loader' })(info.viewableItems); if (!loader || this.state.loadingFromScroll) { return; } this.setState({ loadingFromScroll: true }); const { oldestMessageServerID } = this.props; const threadID = this.props.threadInfo.id; (async () => { try { if (oldestMessageServerID) { await this.props.dispatchActionPromise( fetchMessagesBeforeCursorActionTypes, this.props.fetchMessagesBeforeCursor( threadID, oldestMessageServerID, ), ); } else { await this.props.dispatchActionPromise( fetchMostRecentMessagesActionTypes, this.props.fetchMostRecentMessages(threadID), ); } } finally { this.setState({ loadingFromScroll: false }); } })(); }; } const unboundStyles = { container: { backgroundColor: 'listBackground', flex: 1, }, header: { height: 12, }, listLoadingIndicator: { flex: 1, }, }; registerFetchKey(fetchMessagesBeforeCursorActionTypes); registerFetchKey(fetchMostRecentMessagesActionTypes); const ConnectedMessageList: React.ComponentType = React.memo(function ConnectedMessageList(props: BaseProps) { const keyboardState = React.useContext(KeyboardContext); const overlayContext = React.useContext(OverlayContext); const threadID = props.threadInfo.id; const startReached = useSelector( state => !!( state.messageStore.threads[threadID] && state.messageStore.threads[threadID].startReached ), ); const styles = useStyles(unboundStyles); const indicatorStyle = useIndicatorStyle(); const dispatchActionPromise = useDispatchActionPromise(); const callFetchMessagesBeforeCursor = useServerCall( fetchMessagesBeforeCursor, ); const callFetchMostRecentMessages = useServerCall(fetchMostRecentMessages); const oldestMessageServerID = useOldestMessageServerID(threadID); useWatchThread(props.threadInfo); return ( ); }); export default ConnectedMessageList; diff --git a/native/chat/message-result.react.js b/native/chat/message-result.react.js index c30b0c4b8..f5e5c2bb8 100644 --- a/native/chat/message-result.react.js +++ b/native/chat/message-result.react.js @@ -1,95 +1,95 @@ // @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.js'; import { MessageListContextProvider } from './message-list-types.js'; -import { Message } from './message.react.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<'MessageSearch'>, +route: | NavigationRoute<'TogglePinModal'> | NavigationRoute<'MessageResultsScreen'> | NavigationRoute<'MessageSearch'>, +messageVerticalBounds: ?VerticalBounds, +scrollable: boolean, }; function MessageResult(props: MessageResultProps): React.Node { const styles = useStyles(unboundStyles); const onToggleFocus = React.useCallback(() => {}, []); const item = React.useMemo( () => modifyItemForResultScreen(props.item), [props.item], ); const containerStyle = React.useMemo( () => props.scrollable ? [styles.container, styles.containerOverflow] : styles.container, [props.scrollable, styles.container, styles.containerOverflow], ); return ( {longAbsoluteDate(props.item.messageInfo.time)} ); } const unboundStyles = { container: { marginTop: 5, backgroundColor: 'panelForeground', }, containerOverflow: { 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 c1f44ea1b..8bd887b36 100644 --- a/native/chat/message.react.js +++ b/native/chat/message.react.js @@ -1,153 +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 { 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 = { +type Props = { +item: ChatMessageInfoItemWithHeight, +focused: boolean, +navigation: | ChatNavigationProp<'MessageList'> | AppNavigationProp<'TogglePinModal'> | ChatNavigationProp<'MessageResultsScreen'> | ChatNavigationProp<'MessageSearch'>, +route: | NavigationRoute<'MessageList'> | NavigationRoute<'TogglePinModal'> | 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); - } +function Message(props: Props): React.Node { + const { + focused, + item, + navigation, + route, + toggleFocus, + verticalBounds, + shouldDisplayPinIndicator, + } = props; - componentDidUpdate(prevProps: Props) { - if ( - (prevProps.focused || prevProps.item.startsConversation) !== - (this.props.focused || this.props.item.startsConversation) - ) { - LayoutAnimation.easeInEaseOut(); - } - } + const focusedOrStartsConversation = focused || item.startsConversation; + React.useEffect(() => { + LayoutAnimation.easeInEaseOut(); + }, [focusedOrStartsConversation]); + + const keyboardState = React.useContext(KeyboardContext); + const dismissKeyboard = keyboardState?.dismissKeyboard; + const onMessagePress = React.useCallback( + () => dismissKeyboard?.(), + [dismissKeyboard], + ); + + const onLayout = React.useCallback( + (event: LayoutEvent) => { + if (focused) { + return; + } + + const measuredHeight = event.nativeEvent.layout.height; + const expectedHeight = messageItemHeight(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; - render() { - let message; - if (this.props.item.messageShapeType === 'text') { - message = ( + console.log( + `Message height for ${item.messageShapeType} ` + + `${messageKey(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!!', + ); + }, + [focused, item], + ); + + const innerMessageNode = React.useMemo(() => { + if (item.messageShapeType === 'text') { + return ( ); - } else if (this.props.item.messageShapeType === 'multimedia') { - message = ( + } else if (item.messageShapeType === 'multimedia') { + return ( ); } else { - message = ( + return ( ); } + }, [ + focused, + item, + navigation, + route, + shouldDisplayPinIndicator, + toggleFocus, + verticalBounds, + ]); - const onLayout = __DEV__ ? this.onLayout : undefined; - return ( + const message = React.useMemo( + () => ( - {message} + {innerMessageNode} - ); - } - - 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(); - }; + ), + [innerMessageNode, onLayout, onMessagePress], + ); + return message; } -const ConnectedMessage: React.ComponentType = React.memo( - function ConnectedMessage(props: BaseProps) { - const keyboardState = React.useContext(KeyboardContext); - return ; - }, -); - -export { ConnectedMessage as Message }; +export default Message;