diff --git a/web/chat/thread-top-bar.react.js b/web/chat/thread-top-bar.react.js index e0034de90..a5fdfe29f 100644 --- a/web/chat/thread-top-bar.react.js +++ b/web/chat/thread-top-bar.react.js @@ -1,96 +1,95 @@ // @flow import * as React from 'react'; import { ChevronRight } from 'react-feather'; import { useModalContext } from 'lib/components/modal-provider.react.js'; import SWMansionIcon from 'lib/components/SWMansionIcon.react.js'; import { threadIsPending } from 'lib/shared/thread-utils.js'; import type { ThreadInfo } from 'lib/types/thread-types.js'; import { useResolvedThreadInfo } from 'lib/utils/entity-helpers.js'; import { pinnedMessageCountText } from 'lib/utils/message-pinning-utils.js'; import ThreadMenu from './thread-menu.react.js'; import css from './thread-top-bar.css'; import ThreadAvatar from '../avatars/thread-avatar.react.js'; import Button from '../components/button.react.js'; import { InputStateContext } from '../input/input-state.js'; import PinnedMessagesModal from '../modals/chat/pinned-messages-modal.react.js'; import MessageSearchModal from '../modals/search/message-search-modal.react.js'; type ThreadTopBarProps = { +threadInfo: ThreadInfo, }; function ThreadTopBar(props: ThreadTopBarProps): React.Node { const { threadInfo } = props; const { pushModal } = useModalContext(); let threadMenu = null; if (!threadIsPending(threadInfo.id)) { threadMenu = ; } - const bannerText = threadInfo.pinnedCount - ? pinnedMessageCountText(threadInfo.pinnedCount) - : ''; + const bannerText = + !!threadInfo.pinnedCount && pinnedMessageCountText(threadInfo.pinnedCount); const inputState = React.useContext(InputStateContext); const pushThreadPinsModal = React.useCallback(() => { pushModal( - + , ); - }, [pushModal, inputState, threadInfo, bannerText]); + }, [pushModal, inputState, threadInfo]); const pinnedCountBanner = React.useMemo(() => { if (!bannerText) { return null; } return (
{bannerText}
); }, [bannerText, pushThreadPinsModal]); const onClickSearch = React.useCallback( () => pushModal( , ), [inputState, pushModal, threadInfo], ); const { uiName } = useResolvedThreadInfo(threadInfo); return ( <>
{uiName}
{threadMenu}
{pinnedCountBanner} ); } export default ThreadTopBar; diff --git a/web/modals/chat/pinned-messages-modal.css b/web/modals/chat/pinned-messages-modal.css index e0613625a..a02cd5bc4 100644 --- a/web/modals/chat/pinned-messages-modal.css +++ b/web/modals/chat/pinned-messages-modal.css @@ -1,28 +1,35 @@ hr.separator { border: 0; margin: 20px 0 0 0; width: 100%; height: 2px; border: none; border-top: var(--shades-black-60) solid 1px; } .messageResultsContainer { overflow-y: scroll; padding: 0 32px 8px 32px; } .messageResultsContainer > * { margin-bottom: 16px; } .loadingIndicator { text-align: center; } .topSpace { height: 48px; align-items: center; justify-content: center; display: flex; } + +.noPinnedMessages { + color: var(--text-background-tertiary-default); + display: flex; + flex: 1; + justify-content: center; +} diff --git a/web/modals/chat/pinned-messages-modal.react.js b/web/modals/chat/pinned-messages-modal.react.js index 31d45f706..5d4799032 100644 --- a/web/modals/chat/pinned-messages-modal.react.js +++ b/web/modals/chat/pinned-messages-modal.react.js @@ -1,162 +1,172 @@ // @flow import * as React from 'react'; import { fetchPinnedMessageActionTypes, useFetchPinnedMessages, } from 'lib/actions/message-actions.js'; import { useModalContext } from 'lib/components/modal-provider.react.js'; import { messageListData, type ChatMessageInfoItem, } from 'lib/selectors/chat-selectors.js'; import { createLoadingStatusSelector } from 'lib/selectors/loading-selectors.js'; import { createMessageInfo, isInvalidPinSourceForThread, modifyItemForResultScreen, } from 'lib/shared/message-utils.js'; import type { RawMessageInfo } from 'lib/types/message-types.js'; import { type ThreadInfo } from 'lib/types/thread-types.js'; import { useDispatchActionPromise } from 'lib/utils/action-utils.js'; +import { pinnedMessageCountText } from 'lib/utils/message-pinning-utils.js'; import css from './pinned-messages-modal.css'; import MessageResult from '../../components/message-result.react.js'; import LoadingIndicator from '../../loading-indicator.react.js'; import { useSelector } from '../../redux/redux-utils.js'; import Modal from '../modal.react.js'; type Props = { +threadInfo: ThreadInfo, - +modalName: string, }; const loadingStatusSelector = createLoadingStatusSelector( fetchPinnedMessageActionTypes, ); function PinnedMessagesModal(props: Props): React.Node { - const { threadInfo, modalName } = props; + const { threadInfo } = props; const { id: threadID } = threadInfo; const { popModal } = useModalContext(); const [rawMessageResults, setRawMessageResults] = React.useState< $ReadOnlyArray, >([]); const callFetchPinnedMessages = useFetchPinnedMessages(); const dispatchActionPromise = useDispatchActionPromise(); const userInfos = useSelector(state => state.userStore.userInfos); const loadingStatus = useSelector(loadingStatusSelector); React.useEffect(() => { void dispatchActionPromise( fetchPinnedMessageActionTypes, (async () => { const result = await callFetchPinnedMessages({ threadID }); setRawMessageResults(result.pinnedMessages); })(), ); }, [dispatchActionPromise, callFetchPinnedMessages, threadID]); const translatedMessageResults = React.useMemo(() => { const threadInfos = { [threadID]: threadInfo }; return rawMessageResults .map(messageInfo => createMessageInfo(messageInfo, null, userInfos, threadInfos), ) .filter(Boolean); }, [rawMessageResults, userInfos, threadID, threadInfo]); const chatMessageInfos = useSelector( messageListData(threadInfo.id, translatedMessageResults), ); const sortedUniqueChatMessageInfoItems = React.useMemo(() => { if (!chatMessageInfos) { return ([]: ChatMessageInfoItem[]); } const chatMessageInfoItems = chatMessageInfos.filter( item => item.itemType === 'message' && item.isPinned && !isInvalidPinSourceForThread(item.messageInfo, threadInfo), ); // By the nature of using messageListData and passing in // the desired translatedMessageResults as additional // messages, we will have duplicate ChatMessageInfoItems. const uniqueChatMessageInfoItemsMap = new Map< string, ChatMessageInfoItem, >(); chatMessageInfoItems.forEach( item => item.messageInfo && item.messageInfo.id && uniqueChatMessageInfoItemsMap.set(item.messageInfo.id, item), ); // Push the items in the order they appear in the rawMessageResults // since the messages fetched from the server are already sorted // in the order of pin_time (newest first). const sortedChatMessageInfoItems = []; for (let i = 0; i < rawMessageResults.length; i++) { const rawMessageID = rawMessageResults[i].id; if (!rawMessageID) { continue; } sortedChatMessageInfoItems.push( uniqueChatMessageInfoItemsMap.get(rawMessageID), ); } return sortedChatMessageInfoItems; }, [chatMessageInfos, rawMessageResults, threadInfo]); const modifiedItems = React.useMemo( () => sortedUniqueChatMessageInfoItems .filter(Boolean) .map(item => modifyItemForResultScreen(item)), [sortedUniqueChatMessageInfoItems], ); const messageResultsToDisplay = React.useMemo(() => { + if (modifiedItems.length === 0) { + return ( +
+ No pinned messages in this thread. +
+ ); + } + const items = modifiedItems.map(item => ( )); return <>{items}; }, [modifiedItems, threadInfo]); const loadingIndicator = React.useMemo(() => { if (loadingStatus === 'loading') { return (
); } return null; }, [loadingStatus]); + const modalName = pinnedMessageCountText(modifiedItems.length); + return (
{loadingIndicator}
{messageResultsToDisplay}
); } export default PinnedMessagesModal;