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, 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;