diff --git a/web/components/message-result.css b/web/components/message-result.css index c655a60ab..785c9fa94 100644 --- a/web/components/message-result.css +++ b/web/components/message-result.css @@ -1,28 +1,31 @@ .messageContainer { - overflow-y: scroll; border: 1px solid var(--pin-message-modal-border-color); border-radius: 7px; - max-height: 400px; margin: 0 32px 16px 32px; } +.messageContainerOverflow { + overflow-y: scroll; + max-height: 400px; +} + .messageDate { color: var(--chat-timestamp-color); font-size: var(--xs-font-12); padding: 0px 0px 6px 0px; line-height: var(--line-height-text); text-align: left; margin-left: 16px; } .creator { font-size: small; color: var(--shades-white-60); font-size: var(--s-font-14); padding: 4px 24px; text-align: left; } .messageContent { margin-bottom: 1px; } diff --git a/web/components/message-result.react.js b/web/components/message-result.react.js index 0858eae41..ae0ecd4cf 100644 --- a/web/components/message-result.react.js +++ b/web/components/message-result.react.js @@ -1,58 +1,65 @@ // @flow +import classNames from 'classnames'; import * as React from 'react'; import { useStringForUser } from 'lib/hooks/ens-cache.js'; import type { ChatMessageInfoItem } from 'lib/selectors/chat-selectors.js'; import type { ThreadInfo } from 'lib/types/thread-types.js'; import { longAbsoluteDate } from 'lib/utils/date-utils.js'; import css from './message-result.css'; import { MessageListContext } from '../chat/message-list-types.js'; import Message from '../chat/message.react.js'; import { useTextMessageRulesFunc } from '../markdown/rules.react.js'; type MessageResultProps = { +item: ChatMessageInfoItem, +threadInfo: ThreadInfo, + +scrollable: boolean, }; function MessageResult(props: MessageResultProps): React.Node { - const { item, threadInfo } = props; + const { item, threadInfo, scrollable } = props; const getTextMessageMarkdownRules = useTextMessageRulesFunc(threadInfo); const messageListContext = React.useMemo(() => { if (!getTextMessageMarkdownRules) { return undefined; } return { getTextMessageMarkdownRules }; }, [getTextMessageMarkdownRules]); const shouldShowUsername = !item.startsConversation && !item.startsCluster; const username = useStringForUser( shouldShowUsername ? item.messageInfo.creator : null, ); + const messageContainerClassNames = classNames({ + [css.messageContainer]: true, + [css.messageContainerOverflow]: scrollable, + }); + return ( -
+
{username}
{longAbsoluteDate(item.messageInfo.time)}
); } export default MessageResult; diff --git a/web/modals/chat/message-results-modal.react.js b/web/modals/chat/message-results-modal.react.js index a079ef967..bea2be3c5 100644 --- a/web/modals/chat/message-results-modal.react.js +++ b/web/modals/chat/message-results-modal.react.js @@ -1,152 +1,153 @@ // @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, modifyItemForResultScreen, } 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 pinnedMessageIDs = new Set(); translatedMessageResults.forEach(item => pinnedMessageIDs.add(item.id)); const chatMessageInfoItems = chatMessageInfos.filter( item => item.itemType === 'message' && pinnedMessageIDs.has(item.messageInfo.id), ); // 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; }, [translatedMessageResults, chatMessageInfos, rawMessageResults]); const modifiedItems = React.useMemo( () => sortedUniqueChatMessageInfoItems .filter(Boolean) .map(item => modifyItemForResultScreen(item)), [sortedUniqueChatMessageInfoItems], ); const messageResultsToDisplay = React.useMemo(() => { const items = modifiedItems.map(item => ( )); return <>{items}; }, [modifiedItems, threadInfo]); const loadingIndicator = React.useMemo(() => { if (loadingStatus === 'loading') { return (
); } return null; }, [loadingStatus]); return (
{loadingIndicator}
{messageResultsToDisplay}
); } export default MessageResultsModal; diff --git a/web/modals/chat/toggle-pin-modal.react.js b/web/modals/chat/toggle-pin-modal.react.js index a645ba7af..4816746eb 100644 --- a/web/modals/chat/toggle-pin-modal.react.js +++ b/web/modals/chat/toggle-pin-modal.react.js @@ -1,123 +1,127 @@ // @flow import invariant from 'invariant'; import * as React from 'react'; import { toggleMessagePin, toggleMessagePinActionTypes, } from 'lib/actions/thread-actions.js'; import { useModalContext } from 'lib/components/modal-provider.react.js'; import type { ChatMessageInfoItem } from 'lib/selectors/chat-selectors.js'; import { modifyItemForResultScreen } 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 './toggle-pin-modal.css'; import Button, { buttonThemes } from '../../components/button.react.js'; import MessageResult from '../../components/message-result.react.js'; import Modal from '../modal.react.js'; type TogglePinModalProps = { +item: ChatMessageInfoItem, +threadInfo: ThreadInfo, }; function TogglePinModal(props: TogglePinModalProps): React.Node { const { item, threadInfo } = props; const { messageInfo, isPinned } = item; const { popModal } = useModalContext(); const callToggleMessagePin = useServerCall(toggleMessagePin); const dispatchActionPromise = useDispatchActionPromise(); const modalInfo = React.useMemo(() => { if (isPinned) { return { name: 'Remove Pinned Message', action: 'unpin', confirmationText: 'Are you sure you want to remove this pinned message?', buttonText: 'Remove Pinned Message', buttonColor: buttonThemes.danger, }; } return { name: 'Pin Message', action: 'pin', confirmationText: `You may pin this message to the channel you are currently viewing. To unpin a message, select the pinned messages icon in the channel.`, buttonText: 'Pin Message', buttonColor: buttonThemes.standard, }; }, [isPinned]); // We want to remove inline engagement (threadCreatedFromMessage / reactions) // and the message header (startsConversation). We also want to set isViewer // to false so that the message is left-aligned and uncolored. const modifiedItem = React.useMemo(() => { if (item.messageInfoType !== 'composable') { return item; } const strippedItem = { ...item, threadCreatedFromMessage: undefined, reactions: {}, }; return modifyItemForResultScreen(strippedItem); }, [item]); const onClick = React.useCallback(() => { const createToggleMessagePinPromise = async () => { invariant(messageInfo.id, 'messageInfo.id should be defined'); const result = await callToggleMessagePin({ messageID: messageInfo.id, action: modalInfo.action, }); return { newMessageInfos: result.newMessageInfos, threadID: result.threadID, }; }; dispatchActionPromise( toggleMessagePinActionTypes, createToggleMessagePinPromise(), ); popModal(); }, [ modalInfo, callToggleMessagePin, dispatchActionPromise, messageInfo.id, popModal, ]); return (
{modalInfo.confirmationText}
- +
Cancel
); } export default TogglePinModal;