diff --git a/src/chat/ChatScreen.js b/src/chat/ChatScreen.js index 23c37ec9076..8ec2ef5bf9a 100644 --- a/src/chat/ChatScreen.js +++ b/src/chat/ChatScreen.js @@ -20,6 +20,7 @@ import { canSendToNarrow } from '../utils/narrow'; import { getLoading, getSession } from '../directSelectors'; import { getFetchingForNarrow } from './fetchingSelectors'; import { getShownMessagesForNarrow, isNarrowValid as getIsNarrowValid } from './narrowsSelectors'; +import { getFirstUnreadIdInNarrow } from '../message/messageSelectors'; type Props = $ReadOnly<{| navigation: AppNavigationProp<'chat'>, @@ -42,7 +43,7 @@ const componentStyles = createStyleSheet({ * more details, including how Redux is kept up-to-date during the * whole process. */ -const useFetchMessages = args => { +const useMessagesWithFetch = args => { const { narrow } = args; const dispatch = useDispatch(); @@ -52,9 +53,9 @@ const useFetchMessages = args => { const loading = useSelector(getLoading); const fetching = useSelector(state => getFetchingForNarrow(state, narrow)); const isFetching = fetching.older || fetching.newer || loading; - const haveNoMessages = useSelector( - state => getShownMessagesForNarrow(state, narrow).length === 0, - ); + const messages = useSelector(state => getShownMessagesForNarrow(state, narrow)); + const haveNoMessages = messages.length === 0; + const firstUnreadIdInNarrow = useSelector(state => getFirstUnreadIdInNarrow(state, narrow)); // This could live in state, but then we'd risk pointless rerenders; // we only use it in our `useEffect` callbacks. Using `useRef` is @@ -93,7 +94,7 @@ const useFetchMessages = args => { // `eventQueueId` needed here because it affects `shouldFetchWhenNextFocused`. }, [isFocused, eventQueueId, fetch]); - return { fetchError, isFetching, haveNoMessages }; + return { fetchError, isFetching, messages, haveNoMessages, firstUnreadIdInNarrow }; }; export default function ChatScreen(props: Props) { @@ -106,7 +107,13 @@ export default function ChatScreen(props: Props) { const isNarrowValid = useSelector(state => getIsNarrowValid(state, narrow)); - const { fetchError, isFetching, haveNoMessages } = useFetchMessages({ narrow }); + const { + fetchError, + isFetching, + messages, + haveNoMessages, + firstUnreadIdInNarrow, + } = useMessagesWithFetch({ narrow }); const showMessagePlaceholders = haveNoMessages && isFetching; const sayNoMessages = haveNoMessages && !isFetching; @@ -128,6 +135,8 @@ export default function ChatScreen(props: Props) { return ( diff --git a/src/message/messageSelectors.js b/src/message/messageSelectors.js index 5f4a7def85e..9ecda9d3996 100644 --- a/src/message/messageSelectors.js +++ b/src/message/messageSelectors.js @@ -1,7 +1,7 @@ /* @flow strict-local */ -import { createSelector } from 'reselect'; +import { createSelector, defaultMemoize } from 'reselect'; -import type { Message, Narrow, HtmlPieceDescriptor, Selector } from '../types'; +import type { Message, Outbox, Narrow, Selector } from '../types'; import { getAllNarrows, getFlags, @@ -61,13 +61,9 @@ export const getPrivateMessages: Selector = createSelector( }, ); -export const getHtmlPieceDescriptorsForShownMessages: Selector< - HtmlPieceDescriptor[], - Narrow, -> = createSelector( - (state, narrow) => narrow, - getShownMessagesForNarrow, - (narrow, messages) => getHtmlPieceDescriptors(messages, narrow), +export const getHtmlPieceDescriptorsForMessages = defaultMemoize( + (messages: $ReadOnlyArray, narrow: Narrow) => + getHtmlPieceDescriptors(messages, narrow), ); export const getFirstUnreadIdInNarrow: Selector = createSelector( diff --git a/src/react-native-action-sheet.js b/src/react-native-action-sheet.js new file mode 100644 index 00000000000..16ec503fc19 --- /dev/null +++ b/src/react-native-action-sheet.js @@ -0,0 +1,29 @@ +/* @flow strict-local */ +import type { ComponentType, ElementConfig } from 'react'; +import { connectActionSheet as connectActionSheetInner } from '@expo/react-native-action-sheet'; + +import type { BoundedDiff } from './generics'; + +/* eslint-disable flowtype/generic-spacing */ + +export type ShowActionSheetWithOptions = ( + { options: string[], cancelButtonIndex: number }, + (number) => void, +) => void; + +/** + * Exactly like the `connectActionSheet` in + * `react-native-action-sheet` upstream, but more typed. + */ +export function connectActionSheet>( + WrappedComponent: C, +): ComponentType< + $ReadOnly< + BoundedDiff< + $Exact>, + {| showActionSheetWithOptions: ShowActionSheetWithOptions |}, + >, + >, +> { + return connectActionSheetInner(WrappedComponent); +} diff --git a/src/search/SearchMessagesCard.js b/src/search/SearchMessagesCard.js index d4a8912c886..8380f7aa36f 100644 --- a/src/search/SearchMessagesCard.js +++ b/src/search/SearchMessagesCard.js @@ -8,8 +8,6 @@ import { createStyleSheet } from '../styles'; import { LoadingIndicator, SearchEmptyState } from '../common'; import { HOME_NARROW } from '../utils/narrow'; import MessageList from '../webview/MessageList'; -import getHtmlPieceDescriptors from '../message/getHtmlPieceDescriptors'; -import { NULL_ARRAY } from '../nullObjects'; const styles = createStyleSheet({ results: { @@ -23,8 +21,6 @@ type Props = $ReadOnly<{| |}>; export default class SearchMessagesCard extends PureComponent { - static NOT_FETCHING = { older: false, newer: false }; - render() { const { isFetching, messages } = this.props; @@ -44,18 +40,19 @@ export default class SearchMessagesCard extends PureComponent { return ; } - const htmlPieceDescriptors = getHtmlPieceDescriptors(messages, HOME_NARROW); + // TODO: This is kind of a hack. + const narrow = HOME_NARROW; return ( undefined} /> ); diff --git a/src/webview/MessageList.js b/src/webview/MessageList.js index e38e0e485fc..8bd402080c0 100644 --- a/src/webview/MessageList.js +++ b/src/webview/MessageList.js @@ -1,10 +1,10 @@ /* @flow strict-local */ -import React, { Component } from 'react'; +import React, { Component, type ComponentType } from 'react'; import { Platform, NativeModules } from 'react-native'; import { WebView } from 'react-native-webview'; import type { WebViewNavigation } from 'react-native-webview'; -import { connectActionSheet } from '@expo/react-native-action-sheet'; +import { connectActionSheet } from '../react-native-action-sheet'; import type { AlertWordsState, Auth, @@ -34,20 +34,18 @@ import { getAllImageEmojiById, getCurrentTypingUsers, getDebug, - getHtmlPieceDescriptorsForShownMessages, getFlags, getFetchingForNarrow, - getFirstUnreadIdInNarrow, getMute, getMutedUsers, getOwnUser, getSettings, getSubscriptions, - getShownMessagesForNarrow, getRealm, } from '../selectors'; import { withGetText } from '../boot/TranslationProvider'; import type { ShowActionSheetWithOptions } from '../message/messageActionSheet'; +import { getHtmlPieceDescriptorsForMessages } from '../message/messageSelectors'; import type { WebViewInboundEvent } from './generateInboundEvents'; import type { WebViewOutboundEvent } from './handleOutboundEvents'; import getHtml from './html/html'; @@ -86,6 +84,14 @@ export type BackgroundData = $ReadOnly<{| twentyFourHourTime: boolean, |}>; +type OuterProps = {| + narrow: Narrow, + messages: $ReadOnlyArray, + initialScrollMessageId: number | null, + showMessagePlaceholders: boolean, + startEditMessage: (editMessage: EditMessage) => void, +|}; + type SelectorProps = {| // Data independent of the particular narrow or messages we're displaying. backgroundData: BackgroundData, @@ -93,18 +99,13 @@ type SelectorProps = {| // The remaining props contain data specific to the particular narrow or // particular messages we're displaying. Data that's independent of those // should go in `BackgroundData`, above. - initialScrollMessageId: number | null, fetching: Fetching, - messages: $ReadOnlyArray, htmlPieceDescriptorsForShownMessages: HtmlPieceDescriptor[], typingUsers: $ReadOnlyArray, |}; -// TODO get a type for `connectActionSheet` so this gets fully type-checked. export type Props = $ReadOnly<{| - narrow: Narrow, - showMessagePlaceholders: boolean, - startEditMessage: (editMessage: EditMessage) => void, + ...OuterProps, dispatch: Dispatch, ...SelectorProps, @@ -145,7 +146,7 @@ const assetsUrl = */ const webviewAssetsUrl = new URL('webview/', assetsUrl); -class MessageList extends Component { +class MessageListInner extends Component { static contextType = ThemeContext; context: ThemeData; @@ -299,52 +300,36 @@ class MessageList extends Component { } } -type OuterProps = {| - narrow: Narrow, - showMessagePlaceholders: boolean, - - /* Remaining props are derived from `narrow` by default. */ - - messages?: Message[], - htmlPieceDescriptorsForShownMessages?: HtmlPieceDescriptor[], - initialScrollMessageId?: number | null, - - /* Passing these two from the parent is kind of a hack; search uses it - to hard-code some behavior. */ - fetching?: Fetching, - typingUsers?: UserOrBot[], -|}; - -export default connect((state, props: OuterProps) => { - // TODO Ideally this ought to be a caching selector that doesn't change - // when the inputs don't. Doesn't matter in a practical way here, because - // we have a `shouldComponentUpdate` that doesn't look at this prop... but - // it'd be better to set an example of the right general pattern. - const backgroundData: BackgroundData = { - alertWords: state.alertWords, - allImageEmojiById: getAllImageEmojiById(state), - auth: getAuth(state), - debug: getDebug(state), - flags: getFlags(state), - mute: getMute(state), - mutedUsers: getMutedUsers(state), - ownUser: getOwnUser(state), - subscriptions: getSubscriptions(state), - theme: getSettings(state).theme, - twentyFourHourTime: getRealm(state).twentyFourHourTime, - }; - - return { - backgroundData, - initialScrollMessageId: - props.initialScrollMessageId !== undefined - ? props.initialScrollMessageId - : getFirstUnreadIdInNarrow(state, props.narrow), - fetching: props.fetching || getFetchingForNarrow(state, props.narrow), - messages: props.messages || getShownMessagesForNarrow(state, props.narrow), - htmlPieceDescriptorsForShownMessages: - props.htmlPieceDescriptorsForShownMessages - || getHtmlPieceDescriptorsForShownMessages(state, props.narrow), - typingUsers: props.typingUsers || getCurrentTypingUsers(state, props.narrow), - }; -})(connectActionSheet(withGetText(MessageList))); +const MessageList: ComponentType = connect( + (state, props: OuterProps) => { + // TODO Ideally this ought to be a caching selector that doesn't change + // when the inputs don't. Doesn't matter in a practical way here, because + // we have a `shouldComponentUpdate` that doesn't look at this prop... but + // it'd be better to set an example of the right general pattern. + const backgroundData: BackgroundData = { + alertWords: state.alertWords, + allImageEmojiById: getAllImageEmojiById(state), + auth: getAuth(state), + debug: getDebug(state), + flags: getFlags(state), + mute: getMute(state), + mutedUsers: getMutedUsers(state), + ownUser: getOwnUser(state), + subscriptions: getSubscriptions(state), + theme: getSettings(state).theme, + twentyFourHourTime: getRealm(state).twentyFourHourTime, + }; + + return { + backgroundData, + fetching: getFetchingForNarrow(state, props.narrow), + htmlPieceDescriptorsForShownMessages: getHtmlPieceDescriptorsForMessages( + props.messages, + props.narrow, + ), + typingUsers: getCurrentTypingUsers(state, props.narrow), + }; + }, +)(connectActionSheet(withGetText(MessageListInner))); + +export default MessageList;