diff --git a/web/chat/thread-top-bar.react.js b/web/chat/thread-top-bar.react.js --- a/web/chat/thread-top-bar.react.js +++ b/web/chat/thread-top-bar.react.js @@ -3,6 +3,7 @@ import * as React from 'react'; import { ChevronRight } from 'react-feather'; +import { useModalContext } from 'lib/components/modal-provider.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'; @@ -10,6 +11,8 @@ import ThreadMenu from './thread-menu.react.js'; import css from './thread-top-bar.css'; import ThreadAvatar from '../components/thread-avatar.react.js'; +import { InputStateContext } from '../input/input-state.js'; +import ThreadPinnedMessagesModal from '../modals/chat/thread-pinned-messages-modal.react.js'; import { shouldRenderAvatars } from '../utils/avatar-utils.js'; type ThreadTopBarProps = { @@ -17,6 +20,7 @@ }; function ThreadTopBar(props: ThreadTopBarProps): React.Node { const { threadInfo } = props; + const { pushModal } = useModalContext(); const threadBackgroundColorStyle = React.useMemo( () => ({ background: `#${threadInfo.color}`, @@ -42,6 +46,18 @@ return `${threadInfo.pinnedCount} pinned ${messageNoun}`; }, [threadInfo.pinnedCount]); + const inputState = React.useContext(InputStateContext); + const pushThreadPinsModal = React.useCallback(() => { + pushModal( + + + , + ); + }, [pushModal, inputState, threadInfo, bannerText]); + const pinnedCountBanner = React.useMemo(() => { if (!bannerText) { return null; @@ -49,13 +65,13 @@ return (
- + {bannerText}
); - }, [bannerText]); + }, [bannerText, pushThreadPinsModal]); const { uiName } = useResolvedThreadInfo(threadInfo); diff --git a/web/modals/chat/thread-pinned-messages-modal.css b/web/modals/chat/thread-pinned-messages-modal.css new file mode 100644 --- /dev/null +++ b/web/modals/chat/thread-pinned-messages-modal.css @@ -0,0 +1,12 @@ +hr.separator { + border: 0; + margin: 20px 0; + width: 100%; + height: 2px; + border: none; + border-top: var(--shades-black-70) solid 1px; +} + +.pinnedMessagesContainer { + overflow-y: scroll; +} diff --git a/web/modals/chat/thread-pinned-messages-modal.react.js b/web/modals/chat/thread-pinned-messages-modal.react.js new file mode 100644 --- /dev/null +++ b/web/modals/chat/thread-pinned-messages-modal.react.js @@ -0,0 +1,137 @@ +// @flow + +import invariant from 'invariant'; +import * as React from 'react'; + +import { fetchPinnedMessages } from 'lib/actions/message-actions.js'; +import { useModalContext } from 'lib/components/modal-provider.react.js'; +import { messageListData } from 'lib/selectors/chat-selectors.js'; +import { createMessageInfo } from 'lib/shared/message-utils.js'; +import { type ThreadInfo } from 'lib/types/thread-types.js'; +import { useServerCall } from 'lib/utils/action-utils.js'; + +import css from './thread-pinned-messages-modal.css'; +import PinnedMessage from '../../components/pinned-message.react.js'; +import { useSelector } from '../../redux/redux-utils.js'; +import Modal from '../modal.react.js'; + +type ThreadPinnedMessagesModalProps = { + +threadInfo: ThreadInfo, + +modalName: string, +}; + +function ThreadPinnedMessagesModal( + props: ThreadPinnedMessagesModalProps, +): React.Node { + const { threadInfo, modalName } = props; + const { id: threadID } = threadInfo; + const { popModal } = useModalContext(); + const [rawPinnedMessages, setRawPinnedMessages] = React.useState([]); + const callFetchPinnedMessages = useServerCall(fetchPinnedMessages); + + const userInfos = useSelector(state => state.userStore.userInfos); + + React.useEffect(() => { + (async () => { + const result = await callFetchPinnedMessages({ threadID }); + setRawPinnedMessages(result.pinnedMessages); + })(); + }, [callFetchPinnedMessages, threadID]); + + const translatedPinnedMessageInfos = React.useMemo(() => { + const threadInfos = { [threadID]: threadInfo }; + + return rawPinnedMessages + .map(messageInfo => + createMessageInfo(messageInfo, null, userInfos, threadInfos), + ) + .filter(Boolean); + }, [rawPinnedMessages, userInfos, threadID, threadInfo]); + + const chatMessageInfos = useSelector( + messageListData(threadInfo.id, translatedPinnedMessageInfos), + ); + + const sortedUniqueChatMessageInfoItems = React.useMemo(() => { + if (!chatMessageInfos) { + return []; + } + + const chatMessageInfoItems = chatMessageInfos.filter( + item => item.itemType === 'message' && item.isPinned, + ); + + // By the nature of using messageListData and passing in + // the desired translatedPinnedMessageInfos as additional + // messages, we will have duplicate ChatMessageInfoItems. + const uniqueChatMessageInfoItemsMap = new Map(); + chatMessageInfoItems.map( + item => + item.messageInfo && + item.messageInfo.id && + uniqueChatMessageInfoItemsMap.set(item.messageInfo.id, item), + ); + + // Sort uniqueChatMessageInfoItems based on the order of + // their appearance in rawPinnedMessages (since the messages from the server + // are already sorted by their pin_time in descending order). + const sortedChatMessageInfoItems = []; + for (let i = 0; i < rawPinnedMessages.length; i++) { + sortedChatMessageInfoItems.push( + uniqueChatMessageInfoItemsMap.get(rawPinnedMessages[i].id), + ); + } + + return sortedChatMessageInfoItems; + }, [chatMessageInfos, rawPinnedMessages]); + + const modifiedItems = React.useMemo( + () => + sortedUniqueChatMessageInfoItems.map(item => { + invariant(item, 'item should not be null'); + + // We need to modify the item to make sure that the message does + // not render with the date header and that the creator + // is not considered the viewer. + let modifiedItem = item; + if (item.messageInfoType === 'composable') { + modifiedItem = { + ...item, + startsConversation: false, + messageInfo: { + ...item.messageInfo, + creator: { + ...item.messageInfo.creator, + isViewer: false, + }, + }, + }; + } + return modifiedItem; + }), + [sortedUniqueChatMessageInfoItems], + ); + + const pinnedMessagesToDisplay = React.useMemo( + () => + modifiedItems.map(item => ( + + )), + [modifiedItems, threadInfo], + ); + + return ( + +
+
+ {pinnedMessagesToDisplay} +
+
+ ); +} + +export default ThreadPinnedMessagesModal;