Page Menu
Home
Phorge
Search
Configure Global Search
Log In
Files
F33374920
D7380.1768957083.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.1768957083.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 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(
+ <InputStateContext.Provider value={inputState}>
+ <MessageResultsModal threadInfo={threadInfo} modalName={bannerText} />
+ </InputStateContext.Provider>,
+ );
+ }, [pushModal, inputState, threadInfo, bannerText]);
+
const pinnedCountBanner = React.useMemo(() => {
if (!bannerText) {
return null;
@@ -49,13 +62,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/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 (
+ <div className={css.loadingIndicator}>
+ <LoadingIndicator status="loading" size="medium" />
+ </div>
+ );
+ }
+ return modifiedItems.map(item => (
+ <MessageResult
+ 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 MessageResultsModal;
File Metadata
Details
Attached
Mime Type
text/plain
Expires
Wed, Jan 21, 12:58 AM (9 h, 30 m)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
5965346
Default Alt Text
D7380.1768957083.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