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 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,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;
+}
\ No newline at end of file
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,160 @@
+// @flow
+
+import invariant from 'invariant';
+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 './thread-pinned-messages-modal.css';
+import PinnedMessage from '../../components/pinned-message.react.js';
+import LoadingIndicator from '../../loading-indicator.react.js';
+import { useSelector } from '../../redux/redux-utils.js';
+import Modal from '../modal.react.js';
+
+type ThreadPinnedMessagesModalProps = {
+ +threadInfo: ThreadInfo,
+ +modalName: string,
+};
+
+const loadingStatusSelector = createLoadingStatusSelector(
+ fetchPinnedMessageActionTypes,
+);
+
+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 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 => {
+ 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(() => {
+ if (loadingStatus === 'loading') {
+ return (
+
+
+
+ );
+ }
+ return modifiedItems.map(item => (
+
+ ));
+ }, [modifiedItems, threadInfo, loadingStatus]);
+
+ return (
+
+
+
+ {pinnedMessagesToDisplay}
+
+
+ );
+}
+
+export default ThreadPinnedMessagesModal;