diff --git a/lib/selectors/chat-selectors.js b/lib/selectors/chat-selectors.js index edbf4bd5a..50306fa62 100644 --- a/lib/selectors/chat-selectors.js +++ b/lib/selectors/chat-selectors.js @@ -1,502 +1,500 @@ // @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 * as React from 'react'; import { useSelector } from 'react-redux'; import { createSelector } from 'reselect'; import { createObjectSelector } from 'reselect-map'; import { messageKey, robotextForMessageInfo, createMessageInfo, getMostRecentNonLocalMessageID, sortMessageInfoList, } from '../shared/message-utils'; import { threadIsPending, threadIsTopLevel, threadInChatList, } 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, threadTypes, } from '../types/thread-types'; import type { UserInfo, AccountUserInfo } from '../types/user-types'; import { threeDays } from '../utils/date-utils'; import memoize2 from '../utils/memoize'; import { threadInfoSelector, sidebarInfoSelector, threadInfoFromSourceMessageIDSelector, } from './thread-selectors'; export type SidebarItem = | { ...SidebarInfo, +type: 'sidebar', } | { +type: 'seeMore', +unread: boolean, - +showingSidebarsInline: boolean, } | { +type: 'spacer' }; 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 isEmptyMediaMessage(messageInfo: MessageInfo): boolean { return ( (messageInfo.type === messageTypes.MULTIMEDIA || messageInfo.type === messageTypes.IMAGES) && messageInfo.media.length === 0 ); } 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 || isEmptyMediaMessage(messageInfo)) { continue; } 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--; } } const numReadButRecentSidebars = allSidebarItems.filter( sidebar => !sidebar.threadInfo.currentUser.unread && sidebar.lastUpdatedTime > threeDaysAgo, ).length; if ( sidebarItems.length < numUnreadSidebars + numReadButRecentSidebars || (sidebarItems.length < allSidebarItems.length && sidebarItems.length > 0) ) { sidebarItems.push({ type: 'seeMore', unread: numUnreadSidebars > maxUnreadSidebars, - showingSidebarsInline: sidebarItems.length !== 0, }); } if (sidebarItems.length !== 0) { sidebarItems.push({ type: 'spacer', }); } return { type: 'chatThreadItem', threadInfo, mostRecentMessageInfo, mostRecentNonLocalMessage, lastUpdatedTime, lastUpdatedTimeIncludingSidebars, sidebars: sidebarItems, }; } const chatListData: ( state: BaseAppState<*>, ) => $ReadOnlyArray = createSelector( threadInfoSelector, (state: BaseAppState<*>) => state.messageStore, messageInfoSelector, sidebarInfoSelector, ( threadInfos: { +[id: string]: ThreadInfo }, messageStore: MessageStore, messageInfos: { +[id: string]: ?MessageInfo }, sidebarInfos: { +[id: string]: $ReadOnlyArray }, ): $ReadOnlyArray => getChatThreadItems( threadInfos, messageStore, messageInfos, sidebarInfos, threadIsTopLevel, ), ); function useFlattenedChatListData(): $ReadOnlyArray { return useFilteredChatListData(threadInChatList); } function useFilteredChatListData( filterFunction: (threadInfo: ?(ThreadInfo | RawThreadInfo)) => boolean, ): $ReadOnlyArray { 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, filterFunction, ), [messageInfos, messageStore, sidebarInfos, filterFunction, threadInfos], ); } function getChatThreadItems( threadInfos: { +[id: string]: ThreadInfo }, messageStore: MessageStore, messageInfos: { +[id: string]: ?MessageInfo }, sidebarInfos: { +[id: string]: $ReadOnlyArray }, filterFunction: (threadInfo: ?(ThreadInfo | RawThreadInfo)) => boolean, ): $ReadOnlyArray { 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 }, additionalMessages: $ReadOnlyArray, ): ChatMessageItem[] { const thread = messageStore.threads[threadID]; const threadMessageInfos = (thread?.messageIDs ?? []) .map((messageID: string) => messageInfos[messageID]) .filter(Boolean); const messages = additionalMessages.length > 0 ? sortMessageInfoList([...threadMessageInfos, ...additionalMessages]) : threadMessageInfos; const chatMessageItems = []; let lastMessageInfo = null; for (let i = messages.length - 1; i >= 0; i--) { const messageInfo = messages[i]; const originalMessageInfo = messageInfo.type === messageTypes.SIDEBAR_SOURCE ? messageInfo.sourceMessage : messageInfo; if (isEmptyMediaMessage(originalMessageInfo)) { continue; } 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 && threadInfos[threadID]?.type !== threadTypes.SIDEBAR ? 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(); const hideSpinner = thread ? thread.startReached : threadIsPending(threadID); if (hideSpinner) { return chatMessageItems; } return [...chatMessageItems, ({ itemType: 'loader' }: ChatMessageItem)]; } const baseMessageListData = ( threadID: ?string, additionalMessages: $ReadOnlyArray, ) => createSelector( (state: BaseAppState<*>) => state.messageStore, messageInfoSelector, threadInfoSelector, threadInfoFromSourceMessageIDSelector, ( messageStore: MessageStore, messageInfos: { +[id: string]: ?MessageInfo }, threadInfos: { +[id: string]: ThreadInfo }, threadInfoFromSourceMessageID: { +[id: string]: ThreadInfo }, ): ?(ChatMessageItem[]) => { if (!threadID) { return null; } return createChatMessageItems( threadID, messageStore, messageInfos, threadInfos, threadInfoFromSourceMessageID, additionalMessages, ); }, ); type MessageListData = ?(ChatMessageItem[]); const messageListData: ( threadID: ?string, additionalMessages: $ReadOnlyArray, ) => (state: BaseAppState<*>) => MessageListData = memoize2( baseMessageListData, ); type UseMessageListDataArgs = { +searching: boolean, +userInfoInputArray: $ReadOnlyArray, +threadInfo: ?ThreadInfo, }; function useMessageListData({ searching, userInfoInputArray, threadInfo, }: UseMessageListDataArgs): MessageListData { const pendingSidebarSourceMessageInfo = useSelector(state => { const sourceMessageID = threadInfo?.sourceMessageID; if ( !threadInfo || threadInfo.type !== threadTypes.SIDEBAR || !sourceMessageID ) { return null; } const thread = state.messageStore.threads[threadInfo.id]; const messageInfos = messageInfoSelector(state); const shouldSourceBeAdded = !thread || (thread.startReached && thread.messageIDs.every( id => messageInfos[id]?.type !== messageTypes.SIDEBAR_SOURCE, )); return shouldSourceBeAdded ? messageInfos[sourceMessageID] : null; }); invariant( !pendingSidebarSourceMessageInfo || pendingSidebarSourceMessageInfo.type !== messageTypes.SIDEBAR_SOURCE, 'sidebars can not be created from sidebar_source message', ); const additionalMessages = React.useMemo( () => pendingSidebarSourceMessageInfo ? [pendingSidebarSourceMessageInfo] : [], [pendingSidebarSourceMessageInfo], ); const boundMessageListData = useSelector( messageListData(threadInfo?.id, additionalMessages), ); return React.useMemo(() => { if (searching && userInfoInputArray.length === 0) { return []; } return boundMessageListData; }, [searching, userInfoInputArray.length, boundMessageListData]); } export { messageInfoSelector, createChatThreadItem, chatListData, createChatMessageItems, messageListData, useFlattenedChatListData, useFilteredChatListData, useMessageListData, }; diff --git a/native/chat/chat-thread-list-item.react.js b/native/chat/chat-thread-list-item.react.js index 4b51a325a..98c116427 100644 --- a/native/chat/chat-thread-list-item.react.js +++ b/native/chat/chat-thread-list-item.react.js @@ -1,214 +1,213 @@ // @flow import * as React from 'react'; import { Text, View } from 'react-native'; import type { ChatThreadItem } from 'lib/selectors/chat-selectors'; import type { ThreadInfo } from 'lib/types/thread-types'; import type { UserInfo } from 'lib/types/user-types'; import { shortAbsoluteDate } from 'lib/utils/date-utils'; import Button from '../components/button.react'; import ColorSplotch from '../components/color-splotch.react'; import { SingleLine } from '../components/single-line.react'; import ThreadAncestorsLabel from '../components/thread-ancestors-label.react'; import UnreadDot from '../components/unread-dot.react'; import { useColors, useStyles } from '../themes/colors'; import ChatThreadListSeeMoreSidebars from './chat-thread-list-see-more-sidebars.react'; import ChatThreadListSidebar from './chat-thread-list-sidebar.react'; import MessagePreview from './message-preview.react'; import SwipeableThread from './swipeable-thread.react'; type Props = { +data: ChatThreadItem, +onPressItem: ( threadInfo: ThreadInfo, pendingPersonalThreadUserInfo?: UserInfo, ) => void, +onPressSeeMoreSidebars: (threadInfo: ThreadInfo) => void, +onSwipeableWillOpen: (threadInfo: ThreadInfo) => void, +currentlyOpenedSwipeableId: string, }; function ChatThreadListItem({ data, onPressItem, onPressSeeMoreSidebars, onSwipeableWillOpen, currentlyOpenedSwipeableId, }: Props): React.Node { const styles = useStyles(unboundStyles); const colors = useColors(); const lastMessage = React.useMemo(() => { const mostRecentMessageInfo = data.mostRecentMessageInfo; if (!mostRecentMessageInfo) { return ( No messages ); } return ( ); }, [data.mostRecentMessageInfo, data.threadInfo, styles]); const sidebars = data.sidebars.map(sidebarItem => { if (sidebarItem.type === 'sidebar') { const { type, ...sidebarInfo } = sidebarItem; return ( ); } else if (sidebarItem.type === 'seeMore') { return ( ); } else { return ; } }); const onPress = React.useCallback(() => { onPressItem(data.threadInfo, data.pendingPersonalThreadUserInfo); }, [onPressItem, data.threadInfo, data.pendingPersonalThreadUserInfo]); const threadNameStyle = React.useMemo(() => { if (!data.threadInfo.currentUser.unread) { return styles.threadName; } return [styles.threadName, styles.unreadThreadName]; }, [ data.threadInfo.currentUser.unread, styles.threadName, styles.unreadThreadName, ]); const lastActivity = shortAbsoluteDate(data.lastUpdatedTime); const lastActivityStyle = React.useMemo(() => { if (!data.threadInfo.currentUser.unread) { return styles.lastActivity; } return [styles.lastActivity, styles.unreadLastActivity]; }, [ data.threadInfo.currentUser.unread, styles.lastActivity, styles.unreadLastActivity, ]); return ( <> {sidebars} ); } const chatThreadListItemHeight = 70; const spacerHeight = 6; const unboundStyles = { container: { height: chatThreadListItemHeight, justifyContent: 'center', backgroundColor: 'listBackground', }, content: { flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center', }, colorSplotch: { marginLeft: 6, marginBottom: 12, }, threadDetails: { paddingLeft: 12, paddingRight: 18, justifyContent: 'center', flex: 1, marginTop: 5, }, lastActivity: { color: 'listForegroundTertiaryLabel', fontSize: 14, marginLeft: 10, }, unreadLastActivity: { color: 'listForegroundLabel', fontWeight: 'bold', }, noMessages: { color: 'listForegroundTertiaryLabel', flex: 1, fontSize: 14, fontStyle: 'italic', }, row: { flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center', }, threadName: { color: 'listForegroundSecondaryLabel', flex: 1, fontSize: 21, }, unreadThreadName: { color: 'listForegroundLabel', fontWeight: '500', }, spacer: { height: spacerHeight, }, }; export { ChatThreadListItem, chatThreadListItemHeight, spacerHeight }; diff --git a/native/chat/chat-thread-list-see-more-sidebars.react.js b/native/chat/chat-thread-list-see-more-sidebars.react.js index 9593ca9f4..59735364a 100644 --- a/native/chat/chat-thread-list-see-more-sidebars.react.js +++ b/native/chat/chat-thread-list-see-more-sidebars.react.js @@ -1,72 +1,70 @@ // @flow import * as React from 'react'; import { Text } from 'react-native'; import Icon from 'react-native-vector-icons/Ionicons'; import type { ThreadInfo } from 'lib/types/thread-types'; import Button from '../components/button.react'; import { useColors, useStyles } from '../themes/colors'; import { sidebarHeight } from './sidebar-item.react'; type Props = { +threadInfo: ThreadInfo, +unread: boolean, - +showingSidebarsInline: boolean, +onPress: (threadInfo: ThreadInfo) => void, }; function ChatThreadListSeeMoreSidebars(props: Props): React.Node { - const { onPress, threadInfo, unread, showingSidebarsInline } = props; + const { onPress, threadInfo, unread } = props; const onPressButton = React.useCallback(() => onPress(threadInfo), [ onPress, threadInfo, ]); const colors = useColors(); const styles = useStyles(unboundStyles); const unreadStyle = unread ? styles.unread : null; - const buttonText = showingSidebarsInline ? 'See more...' : 'See threads...'; return ( ); } const unboundStyles = { unread: { color: 'listForegroundLabel', fontWeight: 'bold', }, button: { height: sidebarHeight, flexDirection: 'row', display: 'flex', paddingLeft: 28, paddingRight: 18, alignItems: 'center', backgroundColor: 'listBackground', }, icon: { paddingLeft: 5, color: 'listForegroundSecondaryLabel', width: 35, }, text: { color: 'listForegroundSecondaryLabel', flex: 1, fontSize: 16, paddingLeft: 3, paddingBottom: 2, }, }; export default ChatThreadListSeeMoreSidebars; diff --git a/web/chat/chat-thread-list-item.react.js b/web/chat/chat-thread-list-item.react.js index d81a5d07e..468b75cf3 100644 --- a/web/chat/chat-thread-list-item.react.js +++ b/web/chat/chat-thread-list-item.react.js @@ -1,166 +1,165 @@ // @flow import classNames from 'classnames'; import * as React from 'react'; import type { ChatThreadItem } from 'lib/selectors/chat-selectors'; import { useAncestorThreads } from 'lib/shared/ancestor-threads'; import { shortAbsoluteDate } from 'lib/utils/date-utils'; import { useSelector } from '../redux/redux-utils'; import { useOnClickThread, useThreadIsActive, } from '../selectors/nav-selectors'; import SWMansionIcon from '../SWMansionIcon.react'; import ChatThreadListItemMenu from './chat-thread-list-item-menu.react'; import ChatThreadListSeeMoreSidebars from './chat-thread-list-see-more-sidebars.react'; import ChatThreadListSidebar from './chat-thread-list-sidebar.react'; import css from './chat-thread-list.css'; import MessagePreview from './message-preview.react'; type Props = { +item: ChatThreadItem, }; function ChatThreadListItem(props: Props): React.Node { const { item } = props; const { threadInfo, lastUpdatedTimeIncludingSidebars, mostRecentNonLocalMessage, mostRecentMessageInfo, } = item; const { id: threadID, currentUser } = threadInfo; const ancestorThreads = useAncestorThreads(threadInfo); const onClick = useOnClickThread(item.threadInfo); const timeZone = useSelector(state => state.timeZone); const lastActivity = shortAbsoluteDate( lastUpdatedTimeIncludingSidebars, timeZone, ); const active = useThreadIsActive(threadID); const containerClassName = React.useMemo( () => classNames({ [css.thread]: true, [css.activeThread]: active, }), [active], ); const { unread } = currentUser; const titleClassName = React.useMemo( () => classNames({ [css.title]: true, [css.unread]: unread, }), [unread], ); const lastActivityClassName = React.useMemo( () => classNames({ [css.lastActivity]: true, [css.unread]: unread, [css.dark]: !unread, }), [unread], ); const breadCrumbsClassName = React.useMemo( () => classNames(css.breadCrumbs, { [css.unread]: unread, }), [unread], ); let unreadDot; if (unread) { unreadDot =
; } const { color } = item.threadInfo; const colorSplotchStyle = React.useMemo( () => ({ backgroundColor: `#${color}` }), [color], ); const sidebars = item.sidebars.map((sidebarItem, index) => { if (sidebarItem.type === 'sidebar') { const { type, ...sidebarInfo } = sidebarItem; return ( 0} key={sidebarInfo.threadInfo.id} /> ); } else if (sidebarItem.type === 'seeMore') { return ( ); } else { return
; } }); const ancestorPath = ancestorThreads.map((thread, idx) => { const isNotLast = idx !== ancestorThreads.length - 1; const chevron = isNotLast && ( ); return ( {thread.uiName} {chevron} ); }); return ( <>
{unreadDot}

{ancestorPath}

{threadInfo.uiName}
{lastActivity}
{sidebars} ); } export default ChatThreadListItem; diff --git a/web/chat/chat-thread-list-see-more-sidebars.react.js b/web/chat/chat-thread-list-see-more-sidebars.react.js index e9f83eff5..130742f75 100644 --- a/web/chat/chat-thread-list-see-more-sidebars.react.js +++ b/web/chat/chat-thread-list-see-more-sidebars.react.js @@ -1,45 +1,43 @@ // @flow import classNames from 'classnames'; import * as React from 'react'; import type { ThreadInfo } from 'lib/types/thread-types'; import SidebarListModal from '../modals/chat/sidebar-list-modal.react'; import { useModalContext } from '../modals/modal-provider.react'; import css from './chat-thread-list.css'; type Props = { +threadInfo: ThreadInfo, +unread: boolean, - +showingSidebarsInline: boolean, }; function ChatThreadListSeeMoreSidebars(props: Props): React.Node { - const { unread, showingSidebarsInline, threadInfo } = props; + const { unread, threadInfo } = props; const { pushModal } = useModalContext(); const onClick = React.useCallback( () => pushModal(), [pushModal, threadInfo], ); - const buttonText = showingSidebarsInline ? 'See more...' : 'See threads...'; return ( ); } export default ChatThreadListSeeMoreSidebars;