Page Menu
Home
Phorge
Search
Configure Global Search
Log In
Files
F33417491
D7380.1768998535.diff
No One
Temporary
Actions
View File
Edit File
Delete File
View Transforms
Subscribe
Flag For Later
Award Token
Size
9 KB
Referenced Files
None
Subscribers
None
D7380.1768998535.diff
View Options
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(
+ <InputStateContext.Provider value={inputState}>
+ <ThreadPinnedMessagesModal
+ threadInfo={threadInfo}
+ modalName={bannerText}
+ />
+ </InputStateContext.Provider>,
+ );
+ }, [pushModal, inputState, threadInfo, bannerText]);
+
const pinnedCountBanner = React.useMemo(() => {
if (!bannerText) {
return null;
@@ -49,13 +65,13 @@
return (
<div className={css.pinnedCountBanner}>
- <a className={css.pinnedCountText}>
+ <a className={css.pinnedCountText} onClick={pushThreadPinsModal}>
{bannerText}
<ChevronRight size={14} className={css.chevronRight} />
</a>
</div>
);
- }, [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,163 @@
+// @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 './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 => {
+ 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 (
+ <div className={css.loadingIndicator}>
+ <LoadingIndicator status="loading" size="medium" />
+ </div>
+ );
+ }
+ return modifiedItems.map(item => (
+ <PinnedMessage
+ key={item.messageInfo.id}
+ item={item}
+ threadInfo={threadInfo}
+ />
+ ));
+ }, [modifiedItems, threadInfo, loadingStatus]);
+
+ return (
+ <Modal name={modalName} onClose={popModal} size="large">
+ <hr className={css.separator} />
+ <div className={css.pinnedMessagesContainer}>
+ {pinnedMessagesToDisplay}
+ </div>
+ </Modal>
+ );
+}
+
+export default ThreadPinnedMessagesModal;
File Metadata
Details
Attached
Mime Type
text/plain
Expires
Wed, Jan 21, 12:28 PM (1 h, 37 m)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
5968626
Default Alt Text
D7380.1768998535.diff (9 KB)
Attached To
Mode
D7380: [web] Display the pinned messages for the thread when the banner is clicked
Attached
Detach File
Event Timeline
Log In to Comment