diff --git a/lib/selectors/chat-selectors.js b/lib/selectors/chat-selectors.js index 941f797c5..a37291172 100644 --- a/lib/selectors/chat-selectors.js +++ b/lib/selectors/chat-selectors.js @@ -1,436 +1,488 @@ // @flow import invariant from 'invariant'; import _filter from 'lodash/fp/filter'; import _flow from 'lodash/fp/flow'; import _map from 'lodash/fp/map'; import _orderBy from 'lodash/fp/orderBy'; import _memoize from 'lodash/memoize'; import * as React from 'react'; import { useSelector } from 'react-redux'; import { createSelector } from 'reselect'; import { createObjectSelector } from 'reselect-map'; import { messageKey, robotextForMessageInfo, createMessageInfo, getMostRecentNonLocalMessageID, } from '../shared/message-utils'; -import { threadIsTopLevel, threadInChatList } from '../shared/thread-utils'; +import { + threadIsTopLevel, + threadInChatList, + useSidebarCandidate, +} from '../shared/thread-utils'; import { type MessageInfo, type MessageStore, type ComposableMessageInfo, type RobotextMessageInfo, type LocalMessageInfo, messageTypes, isComposableMessageType, } from '../types/message-types'; import type { BaseAppState } from '../types/redux-types'; import { type ThreadInfo, type RawThreadInfo, type SidebarInfo, maxReadSidebars, maxUnreadSidebars, } from '../types/thread-types'; -import type { UserInfo } from '../types/user-types'; +import type { UserInfo, AccountUserInfo } from '../types/user-types'; import { threeDays } from '../utils/date-utils'; import { threadInfoSelector, sidebarInfoSelector, threadInfoFromSourceMessageIDSelector, } from './thread-selectors'; type SidebarItem = | {| ...SidebarInfo, +type: 'sidebar', |} | {| +type: 'seeMore', +unread: boolean, +showingSidebarsInline: boolean, |}; export type ChatThreadItem = {| +type: 'chatThreadItem', +threadInfo: ThreadInfo, +mostRecentMessageInfo: ?MessageInfo, +mostRecentNonLocalMessage: ?string, +lastUpdatedTime: number, +lastUpdatedTimeIncludingSidebars: number, +sidebars: $ReadOnlyArray, +pendingPersonalThreadUserInfo?: UserInfo, |}; const messageInfoSelector: ( state: BaseAppState<*>, ) => { [id: string]: MessageInfo } = createObjectSelector( (state: BaseAppState<*>) => state.messageStore.messages, (state: BaseAppState<*>) => state.currentUserInfo && state.currentUserInfo.id, (state: BaseAppState<*>) => state.userStore.userInfos, threadInfoSelector, createMessageInfo, ); function getMostRecentMessageInfo( threadInfo: ThreadInfo, messageStore: MessageStore, messages: { [id: string]: MessageInfo }, ): ?MessageInfo { const thread = messageStore.threads[threadInfo.id]; if (!thread) { return null; } for (const messageID of thread.messageIDs) { const messageInfo = messages[messageID]; if (messageInfo) { return messageInfo; } } return null; } function getLastUpdatedTime( threadInfo: ThreadInfo, mostRecentMessageInfo: ?MessageInfo, ): number { return mostRecentMessageInfo ? mostRecentMessageInfo.time : threadInfo.creationTime; } function createChatThreadItem( threadInfo: ThreadInfo, messageStore: MessageStore, messages: { [id: string]: MessageInfo }, sidebarInfos: ?$ReadOnlyArray, ): ChatThreadItem { const mostRecentMessageInfo = getMostRecentMessageInfo( threadInfo, messageStore, messages, ); const mostRecentNonLocalMessage = getMostRecentNonLocalMessageID( threadInfo.id, messageStore, ); const lastUpdatedTime = getLastUpdatedTime(threadInfo, mostRecentMessageInfo); const sidebars = sidebarInfos ?? []; const allSidebarItems = sidebars.map((sidebarInfo) => ({ type: 'sidebar', ...sidebarInfo, })); const lastUpdatedTimeIncludingSidebars = allSidebarItems.length > 0 ? Math.max(lastUpdatedTime, allSidebarItems[0].lastUpdatedTime) : lastUpdatedTime; const numUnreadSidebars = allSidebarItems.filter( (sidebar) => sidebar.threadInfo.currentUser.unread, ).length; let numReadSidebarsToShow = maxReadSidebars - numUnreadSidebars; const threeDaysAgo = Date.now() - threeDays; const sidebarItems = []; for (const sidebar of allSidebarItems) { if (sidebarItems.length >= maxUnreadSidebars) { break; } else if (sidebar.threadInfo.currentUser.unread) { sidebarItems.push(sidebar); } else if ( sidebar.lastUpdatedTime > threeDaysAgo && numReadSidebarsToShow > 0 ) { sidebarItems.push(sidebar); numReadSidebarsToShow--; } } if (sidebarItems.length < allSidebarItems.length) { sidebarItems.push({ type: 'seeMore', unread: numUnreadSidebars > maxUnreadSidebars, showingSidebarsInline: sidebarItems.length !== 0, }); } return { type: 'chatThreadItem', threadInfo, mostRecentMessageInfo, mostRecentNonLocalMessage, lastUpdatedTime, lastUpdatedTimeIncludingSidebars, sidebars: sidebarItems, }; } const chatListData: ( state: BaseAppState<*>, ) => ChatThreadItem[] = createSelector( threadInfoSelector, (state: BaseAppState<*>) => state.messageStore, messageInfoSelector, sidebarInfoSelector, ( threadInfos: { [id: string]: ThreadInfo }, messageStore: MessageStore, messageInfos: { [id: string]: MessageInfo }, sidebarInfos: { [id: string]: $ReadOnlyArray }, ): ChatThreadItem[] => getChatThreadItems( threadInfos, messageStore, messageInfos, sidebarInfos, threadIsTopLevel, ), ); function useFlattenedChatListData(): ChatThreadItem[] { const threadInfos = useSelector(threadInfoSelector); const messageInfos = useSelector(messageInfoSelector); const sidebarInfos = useSelector(sidebarInfoSelector); const messageStore = useSelector((state) => state.messageStore); return React.useMemo( () => getChatThreadItems( threadInfos, messageStore, messageInfos, sidebarInfos, threadInChatList, ), [messageInfos, messageStore, sidebarInfos, threadInfos], ); } function getChatThreadItems( threadInfos: { [id: string]: ThreadInfo }, messageStore: MessageStore, messageInfos: { [id: string]: MessageInfo }, sidebarInfos: { [id: string]: $ReadOnlyArray }, filterFunction: (threadInfo: ?(ThreadInfo | RawThreadInfo)) => boolean, ): ChatThreadItem[] { return _flow( _filter(filterFunction), _map((threadInfo: ThreadInfo): ChatThreadItem => createChatThreadItem( threadInfo, messageStore, messageInfos, sidebarInfos[threadInfo.id], ), ), _orderBy('lastUpdatedTimeIncludingSidebars')('desc'), )(threadInfos); } export type RobotextChatMessageInfoItem = {| +itemType: 'message', +messageInfo: RobotextMessageInfo, +startsConversation: boolean, +startsCluster: boolean, endsCluster: boolean, +robotext: string, +threadCreatedFromMessage: ?ThreadInfo, |}; export type ChatMessageInfoItem = | RobotextChatMessageInfoItem | {| +itemType: 'message', +messageInfo: ComposableMessageInfo, +localMessageInfo: ?LocalMessageInfo, +startsConversation: boolean, +startsCluster: boolean, endsCluster: boolean, +threadCreatedFromMessage: ?ThreadInfo, |}; export type ChatMessageItem = {| itemType: 'loader' |} | ChatMessageInfoItem; const msInFiveMinutes = 5 * 60 * 1000; function createChatMessageItems( threadID: string, messageStore: MessageStore, messageInfos: { [id: string]: MessageInfo }, threadInfos: { [id: string]: ThreadInfo }, threadInfoFromSourceMessageID: { [id: string]: ThreadInfo }, ): ChatMessageItem[] { const thread = messageStore.threads[threadID]; if (!thread) { return []; } const threadMessageInfos = thread.messageIDs .map((messageID: string) => messageInfos[messageID]) .filter(Boolean); const chatMessageItems = []; let lastMessageInfo = null; for (let i = threadMessageInfos.length - 1; i >= 0; i--) { const messageInfo = threadMessageInfos[i]; const originalMessageInfo = messageInfo.type === messageTypes.SIDEBAR_SOURCE ? messageInfo.sourceMessage : messageInfo; let startsConversation = true; let startsCluster = true; if ( lastMessageInfo && lastMessageInfo.time + msInFiveMinutes > originalMessageInfo.time ) { startsConversation = false; if ( isComposableMessageType(lastMessageInfo.type) && isComposableMessageType(originalMessageInfo.type) && lastMessageInfo.creator.id === originalMessageInfo.creator.id ) { startsCluster = false; } } if (startsCluster && chatMessageItems.length > 0) { const lastMessageItem = chatMessageItems[chatMessageItems.length - 1]; invariant(lastMessageItem.itemType === 'message', 'should be message'); lastMessageItem.endsCluster = true; } const threadCreatedFromMessage = messageInfo.id ? threadInfoFromSourceMessageID[messageInfo.id] : undefined; if (isComposableMessageType(originalMessageInfo.type)) { // We use these invariants instead of just checking the messageInfo.type // directly in the conditional above so that isComposableMessageType can // be the source of truth invariant( originalMessageInfo.type === messageTypes.TEXT || originalMessageInfo.type === messageTypes.IMAGES || originalMessageInfo.type === messageTypes.MULTIMEDIA, "Flow doesn't understand isComposableMessageType above", ); const localMessageInfo = messageStore.local[messageKey(originalMessageInfo)]; chatMessageItems.push({ itemType: 'message', messageInfo: originalMessageInfo, localMessageInfo, startsConversation, startsCluster, endsCluster: false, threadCreatedFromMessage, }); } else { invariant( originalMessageInfo.type !== messageTypes.TEXT && originalMessageInfo.type !== messageTypes.IMAGES && originalMessageInfo.type !== messageTypes.MULTIMEDIA, "Flow doesn't understand isComposableMessageType above", ); const robotext = robotextForMessageInfo( originalMessageInfo, threadInfos[threadID], ); chatMessageItems.push({ itemType: 'message', messageInfo: originalMessageInfo, startsConversation, startsCluster, endsCluster: false, threadCreatedFromMessage, robotext, }); } lastMessageInfo = originalMessageInfo; } if (chatMessageItems.length > 0) { const lastMessageItem = chatMessageItems[chatMessageItems.length - 1]; invariant(lastMessageItem.itemType === 'message', 'should be message'); lastMessageItem.endsCluster = true; } chatMessageItems.reverse(); if (thread.startReached) { return chatMessageItems; } return [...chatMessageItems, ({ itemType: 'loader' }: ChatMessageItem)]; } const baseMessageListData = (threadID: string) => createSelector( (state: BaseAppState<*>) => state.messageStore, messageInfoSelector, threadInfoSelector, threadInfoFromSourceMessageIDSelector, ( messageStore: MessageStore, messageInfos: { [id: string]: MessageInfo }, threadInfos: { [id: string]: ThreadInfo }, threadInfoFromSourceMessageID: { [id: string]: ThreadInfo }, ): ChatMessageItem[] => createChatMessageItems( threadID, messageStore, messageInfos, threadInfos, threadInfoFromSourceMessageID, ), ); const messageListData: ( threadID: string, ) => (state: BaseAppState<*>) => ChatMessageItem[] = _memoize( baseMessageListData, ); function getSourceMessageChatItemForPendingSidebar( messageInfo: ComposableMessageInfo | RobotextMessageInfo, threadInfos: { [id: string]: ThreadInfo }, ): ChatMessageInfoItem { if (isComposableMessageType(messageInfo.type)) { invariant( messageInfo.type === messageTypes.TEXT || messageInfo.type === messageTypes.IMAGES || messageInfo.type === messageTypes.MULTIMEDIA, "Flow doesn't understand isComposableMessageType above", ); const messageItem = { itemType: 'message', messageInfo: messageInfo, startsConversation: true, startsCluster: true, endsCluster: false, localMessageInfo: null, threadCreatedFromMessage: undefined, }; return messageItem; } else { invariant( messageInfo.type !== messageTypes.TEXT && messageInfo.type !== messageTypes.IMAGES && messageInfo.type !== messageTypes.MULTIMEDIA, "Flow doesn't understand isComposableMessageType above", ); const robotext = robotextForMessageInfo( messageInfo, threadInfos[messageInfo.threadID], ); const messageItem = { itemType: 'message', messageInfo: messageInfo, startsConversation: true, startsCluster: true, endsCluster: false, threadCreatedFromMessage: undefined, robotext, }; return messageItem; } } +type UseMessageListDataArgs = {| + +boundMessageListData: $ReadOnlyArray, + +sourceMessageID: ?string, + +searching: boolean, + +userInfoInputArray: $ReadOnlyArray, +|}; + +function useMessageListData({ + boundMessageListData, + sourceMessageID, + searching, + userInfoInputArray, +}: UseMessageListDataArgs) { + const threadInfos = useSelector(threadInfoSelector); + const sidebarCandidate = useSidebarCandidate(sourceMessageID); + const sidebarSourceMessageInfo = useSelector((state) => + sourceMessageID && !sidebarCandidate + ? messageInfoSelector(state)[sourceMessageID] + : null, + ); + invariant( + !sidebarSourceMessageInfo || + sidebarSourceMessageInfo.type !== messageTypes.SIDEBAR_SOURCE, + 'sidebars can not be created from sidebar_source message', + ); + + return React.useMemo(() => { + if (searching && userInfoInputArray.length === 0) { + return []; + } else if (sidebarSourceMessageInfo) { + return [ + getSourceMessageChatItemForPendingSidebar( + sidebarSourceMessageInfo, + threadInfos, + ), + ]; + } + return boundMessageListData; + }, [ + searching, + userInfoInputArray.length, + sidebarSourceMessageInfo, + boundMessageListData, + threadInfos, + ]); +} + export { messageInfoSelector, createChatThreadItem, chatListData, createChatMessageItems, messageListData, useFlattenedChatListData, getSourceMessageChatItemForPendingSidebar, + useMessageListData, }; diff --git a/native/chat/message-list-container.react.js b/native/chat/message-list-container.react.js index 5a3932f56..ee127cf89 100644 --- a/native/chat/message-list-container.react.js +++ b/native/chat/message-list-container.react.js @@ -1,439 +1,409 @@ // @flow import invariant from 'invariant'; import * as React from 'react'; import { View } from 'react-native'; import { type ChatMessageItem, messageListData as messageListDataSelector, - messageInfoSelector, - getSourceMessageChatItemForPendingSidebar, + useMessageListData, } from 'lib/selectors/chat-selectors'; -import { threadInfoSelector } from 'lib/selectors/thread-selectors'; import { userInfoSelectorForPotentialMembers, userSearchIndexForPotentialMembers, } from 'lib/selectors/user-selectors'; import { messageID } from 'lib/shared/message-utils'; import { getPotentialMemberItems } from 'lib/shared/search-utils'; -import { - useLatestThreadInfo, - useSidebarCandidate, -} from 'lib/shared/thread-utils'; +import { useLatestThreadInfo } from 'lib/shared/thread-utils'; import { messageTypes } from 'lib/types/message-types'; import type { ThreadInfo } from 'lib/types/thread-types'; import type { AccountUserInfo, UserListItem } from 'lib/types/user-types'; import ContentLoading from '../components/content-loading.react'; import NodeHeightMeasurer from '../components/node-height-measurer.react'; import { type InputState, InputStateContext } from '../input/input-state'; import { OverlayContext, type OverlayContextType, } from '../navigation/overlay-context'; import type { NavigationRoute } from '../navigation/route-names'; import { useSelector } from '../redux/redux-utils'; import { type Colors, useColors, useStyles } from '../themes/colors'; import ChatInputBar from './chat-input-bar.react'; import { chatMessageItemKey } from './chat-list.react'; import type { ChatNavigationProp } from './chat.react'; import { composedMessageMaxWidthSelector } from './composed-message-width'; import { dummyNodeForRobotextMessageHeightMeasurement } from './inner-robotext-message.react'; import { dummyNodeForTextMessageHeightMeasurement } from './inner-text-message.react'; import MessageListThreadSearch from './message-list-thread-search.react'; import { MessageListContext, useMessageListContext, } from './message-list-types'; import MessageList from './message-list.react'; import type { ChatMessageInfoItemWithHeight } from './message.react'; import { multimediaMessageContentSizes } from './multimedia-message.react'; export type ChatMessageItemWithHeight = | {| itemType: 'loader' |} | ChatMessageInfoItemWithHeight; type BaseProps = {| +navigation: ChatNavigationProp<'MessageList'>, +route: NavigationRoute<'MessageList'>, |}; type Props = {| ...BaseProps, // Redux state +usernameInputText: string, +updateUsernameInput: (text: string) => void, +userInfoInputArray: $ReadOnlyArray, +updateTagInput: (items: $ReadOnlyArray) => void, +otherUserInfos: { [id: string]: AccountUserInfo }, +userSearchResults: $ReadOnlyArray, +threadInfo: ThreadInfo, +messageListData: $ReadOnlyArray, +composedMessageMaxWidth: number, +colors: Colors, +styles: typeof unboundStyles, // withInputState +inputState: ?InputState, // withOverlayContext +overlayContext: ?OverlayContextType, |}; type State = {| +listDataWithHeights: ?$ReadOnlyArray, |}; class MessageListContainer extends React.PureComponent { state: State = { listDataWithHeights: null, }; pendingListDataWithHeights: ?$ReadOnlyArray; get frozen() { const { overlayContext } = this.props; invariant( overlayContext, 'MessageListContainer should have OverlayContext', ); return overlayContext.scrollBlockingModalStatus !== 'closed'; } componentDidUpdate(prevProps: Props) { const oldListData = prevProps.messageListData; const newListData = this.props.messageListData; if (!newListData && oldListData) { this.setState({ listDataWithHeights: null }); } if (!this.frozen && this.pendingListDataWithHeights) { this.setState({ listDataWithHeights: this.pendingListDataWithHeights }); this.pendingListDataWithHeights = undefined; } } render() { const { threadInfo, styles } = this.props; const { listDataWithHeights } = this.state; const { searching } = this.props.route.params; let searchComponent = null; if (searching) { searchComponent = ( ); } const showMessageList = !searching || this.props.userInfoInputArray.length > 0; let threadContent = null; if (showMessageList) { let messageList; if (listDataWithHeights) { messageList = ( ); } else { messageList = ( ); } threadContent = ( {messageList} ); } return ( {searchComponent} {threadContent} ); } heightMeasurerID = (item: ChatMessageItem) => { return chatMessageItemKey(item); }; heightMeasurerKey = (item: ChatMessageItem) => { if (item.itemType !== 'message') { return null; } const { messageInfo } = item; if (messageInfo.type === messageTypes.TEXT) { return messageInfo.text; } else if (item.robotext && typeof item.robotext === 'string') { return item.robotext; } return null; }; heightMeasurerDummy = (item: ChatMessageItem) => { invariant( item.itemType === 'message', 'NodeHeightMeasurer asked for dummy for non-message item', ); const { messageInfo } = item; if (messageInfo.type === messageTypes.TEXT) { return dummyNodeForTextMessageHeightMeasurement(messageInfo.text); } else if (item.robotext && typeof item.robotext === 'string') { return dummyNodeForRobotextMessageHeightMeasurement(item.robotext); } invariant(false, 'NodeHeightMeasurer asked for dummy for non-text message'); }; heightMeasurerMergeItem = (item: ChatMessageItem, height: ?number) => { if (item.itemType !== 'message') { return item; } const { messageInfo } = item; invariant( messageInfo.type !== messageTypes.SIDEBAR_SOURCE, 'Sidebar source messages should be replaced by sourceMessage before being measured', ); const { threadInfo } = this.props; if ( messageInfo.type === messageTypes.IMAGES || messageInfo.type === messageTypes.MULTIMEDIA ) { const { inputState } = this.props; // Conditional due to Flow... const localMessageInfo = item.localMessageInfo ? item.localMessageInfo : null; const id = messageID(messageInfo); const pendingUploads = inputState && inputState.pendingUploads && inputState.pendingUploads[id]; const sizes = multimediaMessageContentSizes( messageInfo, this.props.composedMessageMaxWidth, ); return { itemType: 'message', messageShapeType: 'multimedia', messageInfo, localMessageInfo, threadInfo, startsConversation: item.startsConversation, startsCluster: item.startsCluster, endsCluster: item.endsCluster, threadCreatedFromMessage: item.threadCreatedFromMessage, pendingUploads, ...sizes, }; } invariant(height !== null && height !== undefined, 'height should be set'); if (messageInfo.type === messageTypes.TEXT) { // Conditional due to Flow... const localMessageInfo = item.localMessageInfo ? item.localMessageInfo : null; return { itemType: 'message', messageShapeType: 'text', messageInfo, localMessageInfo, threadInfo, startsConversation: item.startsConversation, startsCluster: item.startsCluster, endsCluster: item.endsCluster, threadCreatedFromMessage: item.threadCreatedFromMessage, contentHeight: height, }; } else { invariant( typeof item.robotext === 'string', "Flow can't handle our fancy types :(", ); return { itemType: 'message', messageShapeType: 'robotext', messageInfo, threadInfo, startsConversation: item.startsConversation, startsCluster: item.startsCluster, endsCluster: item.endsCluster, threadCreatedFromMessage: item.threadCreatedFromMessage, robotext: item.robotext, contentHeight: height, }; } }; allHeightsMeasured = ( listDataWithHeights: $ReadOnlyArray, ) => { if (this.frozen) { this.pendingListDataWithHeights = listDataWithHeights; } else { this.setState({ listDataWithHeights }); } }; } const unboundStyles = { container: { backgroundColor: 'listBackground', flex: 1, }, threadContent: { flex: 1, }, }; export default React.memo(function ConnectedMessageListContainer( props: BaseProps, ) { const [usernameInputText, setUsernameInputText] = React.useState(''); const [userInfoInputArray, setUserInfoInputArray] = React.useState< $ReadOnlyArray, >([]); const updateTagInput = React.useCallback( (input: $ReadOnlyArray) => setUserInfoInputArray(input), [], ); const updateUsernameInput = React.useCallback( (text: string) => setUsernameInputText(text), [], ); const otherUserInfos = useSelector(userInfoSelectorForPotentialMembers); const userSearchIndex = useSelector(userSearchIndexForPotentialMembers); const userSearchResults = React.useMemo( () => getPotentialMemberItems( usernameInputText, otherUserInfos, userSearchIndex, userInfoInputArray.map((userInfo) => userInfo.id), ), [usernameInputText, otherUserInfos, userSearchIndex, userInfoInputArray], ); - const threadInfos = useSelector(threadInfoSelector); const threadInfoRef = React.useRef(props.route.params.threadInfo); const [originalThreadInfo, setOriginalThreadInfo] = React.useState( props.route.params.threadInfo, ); const { searching } = props.route.params; const inputState = React.useContext(InputStateContext); const hideSearch = React.useCallback(() => { setOriginalThreadInfo(threadInfoRef.current); props.navigation.setParams({ searching: false, }); }, [props.navigation]); React.useEffect(() => { if (!searching) { return; } inputState?.registerSendCallback(hideSearch); return () => inputState?.unregisterSendCallback(hideSearch); }, [hideSearch, inputState, searching]); const { sourceMessageID } = props.route.params; const latestThreadInfo = useLatestThreadInfo( originalThreadInfo, searching, userInfoInputArray, sourceMessageID, ); if (latestThreadInfo) { threadInfoRef.current = latestThreadInfo; } const threadInfo = threadInfoRef.current; const { setParams } = props.navigation; React.useEffect(() => { setParams({ threadInfo }); }, [setParams, threadInfo]); const threadID = threadInfoRef.current.id; - const sidebarCandidate = useSidebarCandidate(sourceMessageID); const boundMessageListData = useSelector(messageListDataSelector(threadID)); - const sidebarSourceMessageInfo = useSelector((state) => - sourceMessageID && !sidebarCandidate - ? messageInfoSelector(state)[sourceMessageID] - : null, - ); - invariant( - !sidebarSourceMessageInfo || - sidebarSourceMessageInfo.type !== messageTypes.SIDEBAR_SOURCE, - 'sidebars can not be created from sidebar_source message', - ); - const messageListData = React.useMemo(() => { - if (searching && userInfoInputArray.length === 0) { - return []; - } else if (sidebarSourceMessageInfo) { - return [ - getSourceMessageChatItemForPendingSidebar( - sidebarSourceMessageInfo, - threadInfos, - ), - ]; - } - return boundMessageListData; - }, [ - searching, - userInfoInputArray.length, - sidebarSourceMessageInfo, + const messageListData = useMessageListData({ boundMessageListData, - threadInfos, - ]); + sourceMessageID, + searching: !!searching, + userInfoInputArray, + }); const composedMessageMaxWidth = useSelector(composedMessageMaxWidthSelector); const colors = useColors(); const styles = useStyles(unboundStyles); const overlayContext = React.useContext(OverlayContext); const messageListContext = useMessageListContext(threadID); return ( ); });