Page Menu
Home
Phabricator
Search
Configure Global Search
Log In
Files
F3393659
D7380.diff
No One
Temporary
Actions
View File
Edit File
Delete File
View Transforms
Subscribe
Mute Notifications
Award Token
Flag For Later
Size
9 KB
Referenced Files
None
Subscribers
None
D7380.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
@@ -49,6 +49,7 @@
ClientDBMessageInfo,
SimpleMessagesPayload,
ClientDBThreadMessageInfo,
+ FetchPinnedMessagesResult,
} from './message-types.js';
import type { RawReactionMessageInfo } from './messages/reaction.js';
import type { RawTextMessageInfo } from './messages/text.js';
@@ -971,6 +972,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;
+}
+
+.messageResultsContainer {
+ 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 [rawMessageResults, setRawMessageResults] = 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 });
+ setRawMessageResults(result.pinnedMessages);
+ })(),
+ );
+ }, [dispatchActionPromise, callFetchPinnedMessages, threadID]);
+
+ const translatedMessageResults = React.useMemo(() => {
+ const threadInfos = { [threadID]: threadInfo };
+
+ return rawMessageResults
+ .map(messageInfo =>
+ createMessageInfo(messageInfo, null, userInfos, threadInfos),
+ )
+ .filter(Boolean);
+ }, [rawMessageResults, userInfos, threadID, threadInfo]);
+
+ const chatMessageInfos = useSelector(
+ messageListData(threadInfo.id, translatedMessageResults),
+ );
+
+ 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 translatedMessageResults 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 rawMessageResults
+ // 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 < rawMessageResults.length; i++) {
+ sortedChatMessageInfoItems.push(
+ uniqueChatMessageInfoItemsMap.get(rawMessageResults[i].id),
+ );
+ }
+
+ return sortedChatMessageInfoItems;
+ }, [chatMessageInfos, rawMessageResults]);
+
+ 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 messageResultsToDisplay = 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.messageResultsContainer}>
+ {messageResultsToDisplay}
+ </div>
+ </Modal>
+ );
+}
+
+export default MessageResultsModal;
File Metadata
Details
Attached
Mime Type
text/plain
Expires
Sun, Dec 1, 3:28 PM (21 h, 32 m)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
2601959
Default Alt Text
D7380.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