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
@@ -6,17 +6,21 @@
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 { useModalContext } from 'lib/components/modal-provider.react.js';
import ThreadMenu from './thread-menu.react.js';
+import ThreadPinnedMessagesModal from '../modals/chat/thread-pinned-messages-modal.react.js';
import css from './thread-top-bar.css';
import ThreadAvatar from '../components/thread-avatar.react.js';
import { shouldRenderAvatars } from '../utils/avatar-utils.js';
+import { InputStateContext } from '../input/input-state.js';
type ThreadTopBarProps = {
+threadInfo: ThreadInfo,
};
function ThreadTopBar(props: ThreadTopBarProps): React.Node {
const { threadInfo } = props;
+ const { pushModal } = useModalContext();
const threadBackgroundColorStyle = React.useMemo(
() => ({
background: `#${threadInfo.color}`,
@@ -29,6 +33,16 @@
threadMenu = ;
}
+ const inputState = React.useContext(InputStateContext);
+
+ const pushThreadPinsModal = React.useCallback(() => {
+ pushModal(
+
+
+ ,
+ );
+ }, [pushModal, inputState, threadInfo]);
+
const pinnedCountBanner = React.useMemo(() => {
if (!threadInfo.pinnedCount || threadInfo.pinnedCount === 0) {
return null;
@@ -39,13 +53,13 @@
return (
-
+
{threadInfo.pinnedCount} pinned {singleOrPlural}
);
- }, [threadInfo.pinnedCount]);
+ }, [threadInfo.pinnedCount, 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,11 @@
+hr.separator {
+ border: 0;
+ margin: 20px 0;
+ width: 100%;
+ height: 2px;
+ background: #666666;
+}
+
+.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,147 @@
+// @flow
+
+import invariant from 'invariant';
+import * as React from 'react';
+
+import { type ThreadInfo } from 'lib/types/thread-types.js';
+import { fetchPinnedMessages } from 'lib/actions/message-actions.js';
+import { createMessageInfo } from 'lib/shared/message-utils.js';
+import { useServerCall } from 'lib/utils/action-utils.js';
+import { useModalContext } from 'lib/components/modal-provider.react.js';
+import { messageListData } from 'lib/selectors/chat-selectors.js';
+
+import Modal from '../modal.react.js';
+import css from './thread-pinned-messages-modal.css';
+import PinnedMessage from '../../components/pinned-message.react.js';
+import { useSelector } from '../../redux/redux-utils.js';
+
+type ThreadPinnedMessagesModalProps = {
+ +threadInfo: ThreadInfo,
+};
+
+function ThreadPinnedMessagesModal(
+ props: ThreadPinnedMessagesModalProps,
+): React.Node {
+ const { threadInfo } = props;
+ const { id: threadID } = threadInfo;
+ const { popModal } = useModalContext();
+ const [rawPinnedMessages, setRawPinnedMessages] = React.useState([]);
+ const callFetchPinnedMessages = useServerCall(fetchPinnedMessages);
+
+ const userInfos = useSelector(state => state.userStore.userInfos);
+
+ invariant(threadInfo.pinnedCount, 'pinnedCount should be a defined property');
+ const singleOrPlural = threadInfo.pinnedCount === 1 ? 'message' : 'messages';
+ const modalName = `${threadInfo.pinnedCount} pinned ${singleOrPlural}`;
+
+ 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 => {
+ return 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.messageInfoType === 'composable' &&
+ item.isPinned,
+ );
+
+ // By the nature of using messageListData and passing in
+ // the desired translatedPinnedMessageInfos as additional
+ // messages, we will have duplicate ChatMessageInfoItems. This
+ // removes any duplicates (identified by messageInfo.id).
+ const uniqueChatMessageInfoItems = chatMessageInfoItems.filter(
+ (item, index, self) =>
+ index ===
+ self.findIndex(
+ otherItem =>
+ otherItem.messageInfo &&
+ otherItem.messageInfo.id === item.messageInfo?.id,
+ ),
+ );
+
+ // Sort uniqueChatMessageInfoItems by messageInfo.id 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).
+ return uniqueChatMessageInfoItems.sort((a, b) => {
+ const aIndex = rawPinnedMessages.findIndex(
+ message => message.id === a.messageInfo?.id,
+ );
+
+ const bIndex = rawPinnedMessages.findIndex(
+ message => message.id === b.messageInfo?.id,
+ );
+
+ return aIndex - bIndex;
+ });
+ }, [chatMessageInfos, rawPinnedMessages]);
+
+ const modifiedItems = React.useMemo(() => {
+ return sortedUniqueChatMessageInfoItems.map(item => {
+ invariant(item.itemType !== 'loader', 'loader should not be displayed');
+
+ // 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(() => {
+ return modifiedItems.map(item => {
+ return (
+
+ );
+ });
+ }, [modifiedItems, threadInfo]);
+
+ return (
+
+
+
+ {pinnedMessagesToDisplay}
+
+
+ );
+}
+
+export default ThreadPinnedMessagesModal;