diff --git a/lib/actions/message-actions.js b/lib/actions/message-actions.js --- a/lib/actions/message-actions.js +++ b/lib/actions/message-actions.js @@ -266,6 +266,11 @@ const processMessagesActionType = 'PROCESS_MESSAGES'; const messageStorePruneActionType = 'MESSAGE_STORE_PRUNE'; +const fetchPinnedMessageActionTypes = Object.freeze({ + started: 'FETCH_PINNED_MESSAGES_STARTED', + success: 'FETCH_PINNED_MESSAGES_SUCCESS', + failed: 'FETCH_PINNED_MESSAGES_FAILED', +}); const fetchPinnedMessages = ( callServerEndpoint: CallServerEndpoint, @@ -298,4 +303,5 @@ sendEditMessageActionTypes, sendEditMessage, fetchPinnedMessages, + fetchPinnedMessageActionTypes, }; diff --git a/lib/types/redux-types.js b/lib/types/redux-types.js --- a/lib/types/redux-types.js +++ b/lib/types/redux-types.js @@ -48,6 +48,7 @@ LocallyComposedMessageInfo, ClientDBMessageInfo, SimpleMessagesPayload, + FetchPinnedMessagesResult, } from './message-types.js'; import type { RawReactionMessageInfo } from './messages/reaction.js'; import type { RawTextMessageInfo } from './messages/text.js'; @@ -969,6 +970,22 @@ +error: true, +payload: Error, +loadingInfo: LoadingInfo, + } + | { + +type: 'FETCH_PINNED_MESSAGES_STARTED', + +loadingInfo?: LoadingInfo, + +payload?: void, + } + | { + +type: 'FETCH_PINNED_MESSAGES_SUCCESS', + +payload: FetchPinnedMessagesResult, + +loadingInfo: LoadingInfo, + } + | { + +type: 'FETCH_PINNED_MESSAGES_FAILED', + +error: true, + +payload: Error, + +loadingInfo: LoadingInfo, }; export type ActionPayload = ?(Object | Array<*> | $ReadOnlyArray<*> | string); 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 MessageResultsModal from '../modals/chat/message-results-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,15 @@ 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 +62,13 @@ return (
- + {bannerText}
); - }, [bannerText]); + }, [bannerText, pushThreadPinsModal]); const { uiName } = useResolvedThreadInfo(threadInfo); diff --git a/web/modals/chat/message-results-modal.css b/web/modals/chat/message-results-modal.css new file mode 100644 --- /dev/null +++ b/web/modals/chat/message-results-modal.css @@ -0,0 +1,17 @@ +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; +} + +.loadingIndicator { + text-align: center; + margin-bottom: 10px; +} diff --git a/web/modals/chat/message-results-modal.react.js b/web/modals/chat/message-results-modal.react.js new file mode 100644 --- /dev/null +++ b/web/modals/chat/message-results-modal.react.js @@ -0,0 +1,161 @@ +// @flow + +import * as React from 'react'; + +import { + fetchPinnedMessages, + fetchPinnedMessageActionTypes, +} from 'lib/actions/message-actions.js'; +import { useModalContext } from 'lib/components/modal-provider.react.js'; +import { messageListData } from 'lib/selectors/chat-selectors.js'; +import { createLoadingStatusSelector } from 'lib/selectors/loading-selectors.js'; +import { createMessageInfo } from 'lib/shared/message-utils.js'; +import { type ThreadInfo } from 'lib/types/thread-types.js'; +import { + useServerCall, + useDispatchActionPromise, +} from 'lib/utils/action-utils.js'; + +import css from './message-results-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 MessageResultsModalProps = { + +threadInfo: ThreadInfo, + +modalName: string, +}; + +const loadingStatusSelector = createLoadingStatusSelector( + fetchPinnedMessageActionTypes, +); + +function MessageResultsModal(props: MessageResultsModalProps): React.Node { + const { threadInfo, modalName } = props; + const { id: threadID } = threadInfo; + const { popModal } = useModalContext(); + const [rawPinnedMessages, setRawPinnedMessages] = React.useState([]); + + const callFetchPinnedMessages = useServerCall(fetchPinnedMessages); + const dispatchActionPromise = useDispatchActionPromise(); + + const userInfos = useSelector(state => state.userStore.userInfos); + const loadingStatus = useSelector(loadingStatusSelector); + + React.useEffect(() => { + dispatchActionPromise( + fetchPinnedMessageActionTypes, + (async () => { + const result = await callFetchPinnedMessages({ threadID }); + setRawPinnedMessages(result.pinnedMessages); + })(), + ); + }, [dispatchActionPromise, 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.forEach( + item => + item.messageInfo && + item.messageInfo.id && + uniqueChatMessageInfoItemsMap.set(item.messageInfo.id, item), + ); + + // Push the items in the order they appear in the rawPinnedMessages + // 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 < rawPinnedMessages.length; i++) { + sortedChatMessageInfoItems.push( + uniqueChatMessageInfoItemsMap.get(rawPinnedMessages[i].id), + ); + } + + return sortedChatMessageInfoItems; + }, [chatMessageInfos, rawPinnedMessages]); + + const modifiedItems = React.useMemo( + () => + sortedUniqueChatMessageInfoItems + .map(item => { + if (!item) { + return 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; + }) + .filter(Boolean), + [sortedUniqueChatMessageInfoItems], + ); + + const pinnedMessagesToDisplay = React.useMemo(() => { + if (loadingStatus === 'loading') { + return ( +
+ +
+ ); + } + return modifiedItems.map(item => ( + + )); + }, [modifiedItems, threadInfo, loadingStatus]); + + return ( + +
+
+ {pinnedMessagesToDisplay} +
+
+ ); +} + +export default MessageResultsModal;