diff --git a/web/CommIcon.react.js b/web/CommIcon.react.js index 9877f3c9f..2b8c5375d 100644 --- a/web/CommIcon.react.js +++ b/web/CommIcon.react.js @@ -1,40 +1,41 @@ // @flow import * as React from 'react'; import IcomoonReact from 'react-icomoon'; import iconSet from 'lib/shared/comm-icon-config.json'; const IcomoonIcon = IcomoonReact.default; export type CommIcons = | 'cloud-filled' | 'sidebar' | 'sidebar-filled' | 'reply' | 'reply-filled' | 'megaphone' | 'copy-filled' | 'emote-smile-filled' | 'pin' - | 'unpin'; + | 'unpin' + | 'pin-mirror'; type CommIconProps = { +icon: CommIcons, +size: number | string, +color?: string, +title?: string, +className?: string, +disableFill?: boolean, +removeInlineStyle?: boolean, }; const iconStyle = { stroke: 'none', }; function CommIcon(props: CommIconProps): React.Node { return ; } export default CommIcon; diff --git a/web/chat/chat-message-list.css b/web/chat/chat-message-list.css index 8da35d5b2..c3389303f 100644 --- a/web/chat/chat-message-list.css +++ b/web/chat/chat-message-list.css @@ -1,238 +1,251 @@ div.outerMessageContainer { position: relative; height: calc(100vh - 128px); min-height: 0; display: flex; flex-direction: column; } div.messageContainer { height: 100%; overflow-y: auto; display: flex; flex-direction: column-reverse; } div.mirroredMessageContainer { flex-direction: column !important; transform: scaleY(-1); } div.mirroredMessageContainer > div { transform: scaleY(-1); } div.message { display: flex; flex-direction: column; flex-shrink: 0; } div.loading { text-align: center; padding: 12px; } div.conversationHeader { color: var(--chat-timestamp-color); font-size: var(--xs-font-12); padding: 6px 0; line-height: var(--line-height-text); text-align: center; } div.conversationHeader:last-child { padding-top: 6px; } div.messageTooltipActiveArea { position: absolute; display: flex; top: 0; bottom: 0; align-items: center; padding: 0 12px; } div.viewerMessageTooltipActiveArea { right: 100%; } div.nonViewerMessageActiveArea { left: 100%; } div.messageTooltipActiveArea > div + div { margin-left: 4px; } div.messageTooltipLinkIcon:hover { cursor: pointer; } div.textMessage { padding: 6px 12px; white-space: pre-wrap; word-wrap: break-word; width: 100%; box-sizing: border-box; } div.textMessageDefaultBackground { background-color: var(--text-message-default-background); } div.normalTextMessage { font-size: 16px; } div.emojiOnlyTextMessage { font-size: 32px; font-family: emoji; } span.authorName { color: #777777; font-size: 14px; } span.authorNamePositionAvatar { padding: 4px 56px; } span.authorNamePositionNoAvatar { padding: 4px 24px; } div.darkTextMessage { color: white; } div.lightTextMessage { color: black; } div.content { display: flex; flex-shrink: 0; align-items: center; margin-bottom: 5px; box-sizing: border-box; width: 100%; } div.nonViewerContent { align-self: flex-start; justify-content: flex-start; padding-right: 8px; } div.viewerContent { align-self: flex-end; justify-content: flex-end; padding-right: 4px; } div.iconContainer { margin-right: 1px; } div.iconContainer > svg { height: 16px; } div.messageBoxContainer { position: relative; display: flex; max-width: calc(min(68%, 1000px)); } div.messageBoxContainerPositionAvatar { margin: 0 4px; } div.messageBoxContainerPositionNoAvatar { margin: 0 4px 0 12px; } div.fixedWidthMessageBoxContainer { width: 68%; } div.messageBox { overflow: hidden; display: flex; flex-wrap: wrap; justify-content: space-between; flex-shrink: 0; max-width: 100%; } div.fixedWidthMessageBox { width: 100%; } div.failedSend { display: flex; justify-content: flex-end; flex-shrink: 0; margin-right: 45px; padding-bottom: 6px; } .deliveryFailed { text-transform: uppercase; font-size: 14px; padding: 0 3px; color: var(--fg); } .retryButtonText { text-transform: uppercase; font-size: 14px; } div.messageBox > div.imageGrid { display: grid; width: 100%; grid-template-columns: repeat(6, 1fr); grid-gap: 5px; } div.messageBox span.multimedia > span.multimediaImage { min-height: initial; min-width: initial; } div.messageBox span.multimedia > span.multimediaImage > img { max-height: 600px; } div.imageGrid > span.multimedia { grid-column-end: span 3; } div.imageGrid > span.multimedia:first-child { margin-top: 0; } div.imageGrid > span.multimedia > span.multimediaImage { flex: 1; } div.imageGrid > span.multimedia > span.multimediaImage:after { content: ''; display: block; padding-bottom: calc(min(600px, 100%)); } div.imageGrid > span.multimedia > span.multimediaImage > img { position: absolute; width: 100%; height: 100%; object-fit: cover; } div.imageGrid > span.multimedia:nth-last-child(n + 3):first-child, div.imageGrid > span.multimedia:nth-last-child(n + 3):first-child ~ * { grid-column-end: span 2; } div.imageGrid > span.multimedia:nth-last-child(n + 4):first-child, div.imageGrid > span.multimedia:nth-last-child(n + 4):first-child ~ * { grid-column-end: span 3; } div.imageGrid > span.multimedia:nth-last-child(n + 5):first-child, div.imageGrid > span.multimedia:nth-last-child(n + 5):first-child ~ * { grid-column-end: span 2; } div.sidebarMarginBottom { margin-bottom: 2px; } svg.inlineEngagementIcon { color: #666666; } div.avatarContainer { display: flex; height: 100%; align-items: flex-end; margin: 0 4px 0 12px; } div.avatarOffset { width: 40px; } + +.pinIconContainer { + position: absolute; + top: 1px; +} + +.pinIconLeft { + left: -20px; +} + +.pinIconRight { + right: -20px; +} diff --git a/web/chat/chat-message-list.react.js b/web/chat/chat-message-list.react.js index 2ac190be6..ec53c48b2 100644 --- a/web/chat/chat-message-list.react.js +++ b/web/chat/chat-message-list.react.js @@ -1,343 +1,344 @@ // @flow import classNames from 'classnames'; import { detect as detectBrowser } from 'detect-browser'; import invariant from 'invariant'; import * as React from 'react'; import { fetchMessagesBeforeCursorActionTypes, fetchMessagesBeforeCursor, fetchMostRecentMessagesActionTypes, fetchMostRecentMessages, } from 'lib/actions/message-actions.js'; import { registerFetchKey } from 'lib/reducers/loading-reducer.js'; import { type ChatMessageItem, useMessageListData, } from 'lib/selectors/chat-selectors.js'; import { messageKey } from 'lib/shared/message-utils.js'; import { threadIsPending } from 'lib/shared/thread-utils.js'; import type { FetchMessageInfosPayload } from 'lib/types/message-types.js'; import { type ThreadInfo, threadTypes } from 'lib/types/thread-types.js'; import { type DispatchActionPromise, useServerCall, useDispatchActionPromise, } from 'lib/utils/action-utils.js'; import css from './chat-message-list.css'; import { MessageListContext } from './message-list-types.js'; import Message from './message.react.js'; import RelationshipPrompt from './relationship-prompt/relationship-prompt.js'; import { useTooltipContext } from './tooltip-provider.js'; import { type InputState, InputStateContext } from '../input/input-state.js'; import LoadingIndicator from '../loading-indicator.react.js'; import { useTextMessageRulesFunc } from '../markdown/rules.react.js'; import { useSelector } from '../redux/redux-utils.js'; const browser = detectBrowser(); const supportsReverseFlex = !browser || browser.name !== 'firefox' || parseInt(browser.version) >= 81; type BaseProps = { +threadInfo: ThreadInfo, }; type Props = { ...BaseProps, // Redux state +activeChatThreadID: ?string, +messageListData: ?$ReadOnlyArray, +startReached: boolean, // Redux dispatch functions +dispatchActionPromise: DispatchActionPromise, // async functions that hit server APIs +fetchMessagesBeforeCursor: ( threadID: string, beforeMessageID: string, ) => Promise, +fetchMostRecentMessages: ( threadID: string, ) => Promise, // withInputState +inputState: ?InputState, +clearTooltip: () => mixed, }; type Snapshot = { +scrollTop: number, +scrollHeight: number, }; class ChatMessageList extends React.PureComponent { container: ?HTMLDivElement; messageContainer: ?HTMLDivElement; loadingFromScroll = false; componentDidMount() { this.scrollToBottom(); } getSnapshotBeforeUpdate(prevProps: Props) { if ( ChatMessageList.hasNewMessage(this.props, prevProps) && this.messageContainer ) { const { scrollTop, scrollHeight } = this.messageContainer; return { scrollTop, scrollHeight }; } return null; } static hasNewMessage(props: Props, prevProps: Props) { const { messageListData } = props; if (!messageListData || messageListData.length === 0) { return false; } const prevMessageListData = prevProps.messageListData; if (!prevMessageListData || prevMessageListData.length === 0) { return true; } return ( ChatMessageList.keyExtractor(prevMessageListData[0]) !== ChatMessageList.keyExtractor(messageListData[0]) ); } componentDidUpdate(prevProps: Props, prevState, snapshot: ?Snapshot) { const { messageListData } = this.props; const prevMessageListData = prevProps.messageListData; if ( this.loadingFromScroll && messageListData && (!prevMessageListData || messageListData.length > prevMessageListData.length || this.props.startReached) ) { this.loadingFromScroll = false; } const { messageContainer } = this; if (messageContainer && prevMessageListData !== messageListData) { this.onScroll(); } // We'll scroll to the bottom if the user was already scrolled to the bottom // before the new message, or if the new message was composed locally const hasNewMessage = ChatMessageList.hasNewMessage(this.props, prevProps); if ( this.props.activeChatThreadID !== prevProps.activeChatThreadID || (hasNewMessage && messageListData && messageListData[0].itemType === 'message' && messageListData[0].messageInfo.localID) || (hasNewMessage && snapshot && Math.abs(snapshot.scrollTop) <= 1) ) { this.scrollToBottom(); } else if (hasNewMessage && messageContainer && snapshot) { const { scrollTop, scrollHeight } = messageContainer; if ( scrollHeight > snapshot.scrollHeight && scrollTop === snapshot.scrollTop ) { const newHeight = scrollHeight - snapshot.scrollHeight; const newScrollTop = Math.abs(scrollTop) + newHeight; if (supportsReverseFlex) { messageContainer.scrollTop = -1 * newScrollTop; } else { messageContainer.scrollTop = newScrollTop; } } } } scrollToBottom() { if (this.messageContainer) { this.messageContainer.scrollTop = 0; } } static keyExtractor(item: ChatMessageItem) { if (item.itemType === 'loader') { return 'loader'; } return messageKey(item.messageInfo); } renderItem = item => { if (item.itemType === 'loader') { return ( ); } const { threadInfo } = this.props; invariant(threadInfo, 'ThreadInfo should be set if messageListData is'); return ( ); }; render() { const { messageListData, threadInfo, inputState } = this.props; if (!messageListData) { return ; } invariant(inputState, 'InputState should be set'); const messages = messageListData.map(this.renderItem); let relationshipPrompt = null; if (threadInfo.type === threadTypes.PERSONAL) { relationshipPrompt = ; } const messageContainerStyle = classNames({ [css.messageContainer]: true, [css.mirroredMessageContainer]: !supportsReverseFlex, }); return ( {relationshipPrompt} {messages} ); } messageContainerRef = (messageContainer: ?HTMLDivElement) => { this.messageContainer = messageContainer; // In case we already have all the most recent messages, // but they're not enough this.possiblyLoadMoreMessages(); if (messageContainer) { messageContainer.addEventListener('scroll', this.onScroll); } }; onScroll = () => { if (!this.messageContainer) { return; } this.props.clearTooltip(); this.possiblyLoadMoreMessages(); }; possiblyLoadMoreMessages() { if (!this.messageContainer) { return; } const { scrollTop, scrollHeight, clientHeight } = this.messageContainer; if ( this.props.startReached || Math.abs(scrollTop) + clientHeight + 55 < scrollHeight ) { return; } if (this.loadingFromScroll) { return; } this.loadingFromScroll = true; const threadID = this.props.activeChatThreadID; invariant(threadID, 'should be set'); const oldestMessageServerID = this.oldestMessageServerID(); if (oldestMessageServerID) { this.props.dispatchActionPromise( fetchMessagesBeforeCursorActionTypes, this.props.fetchMessagesBeforeCursor(threadID, oldestMessageServerID), ); } else { this.props.dispatchActionPromise( fetchMostRecentMessagesActionTypes, this.props.fetchMostRecentMessages(threadID), ); } } oldestMessageServerID(): ?string { const data = this.props.messageListData; invariant(data, 'should be set'); for (let i = data.length - 1; i >= 0; i--) { if (data[i].itemType === 'message' && data[i].messageInfo.id) { return data[i].messageInfo.id; } } return null; } } registerFetchKey(fetchMessagesBeforeCursorActionTypes); registerFetchKey(fetchMostRecentMessagesActionTypes); const ConnectedChatMessageList: React.ComponentType = React.memo(function ConnectedChatMessageList( props: BaseProps, ): React.Node { const { threadInfo } = props; const messageListData = useMessageListData({ threadInfo, searching: false, userInfoInputArray: [], }); const startReached = !!useSelector(state => { const activeID = threadInfo.id; if (!activeID) { return null; } if (threadIsPending(activeID)) { return true; } const threadMessageInfo = state.messageStore.threads[activeID]; if (!threadMessageInfo) { return null; } return threadMessageInfo.startReached; }); const dispatchActionPromise = useDispatchActionPromise(); const callFetchMessagesBeforeCursor = useServerCall( fetchMessagesBeforeCursor, ); const callFetchMostRecentMessages = useServerCall(fetchMostRecentMessages); const inputState = React.useContext(InputStateContext); const { clearTooltip } = useTooltipContext(); const getTextMessageMarkdownRules = useTextMessageRulesFunc(threadInfo); const messageListContext = React.useMemo(() => { if (!getTextMessageMarkdownRules) { return undefined; } return { getTextMessageMarkdownRules }; }, [getTextMessageMarkdownRules]); return ( ); }); export default ConnectedChatMessageList; diff --git a/web/chat/composed-message.react.js b/web/chat/composed-message.react.js index bff43aa1e..ca009381c 100644 --- a/web/chat/composed-message.react.js +++ b/web/chat/composed-message.react.js @@ -1,225 +1,248 @@ // @flow import classNames from 'classnames'; import * as React from 'react'; import { Circle as CircleIcon, CheckCircle as CheckCircleIcon, XCircle as XCircleIcon, } from 'react-feather'; import { useStringForUser } from 'lib/hooks/ens-cache.js'; import { type ChatMessageInfoItem } from 'lib/selectors/chat-selectors.js'; import { getMessageLabel } from 'lib/shared/edit-messages-utils.js'; import { assertComposableMessageType } from 'lib/types/message-types.js'; import { type ThreadInfo } from 'lib/types/thread-types.js'; import css from './chat-message-list.css'; import FailedSend from './failed-send.react.js'; import InlineEngagement from './inline-engagement.react.js'; +import CommIcon from '../CommIcon.react.js'; import UserAvatar from '../components/user-avatar.react.js'; import { type InputState, InputStateContext } from '../input/input-state.js'; import { shouldRenderAvatars } from '../utils/avatar-utils.js'; import { useMessageTooltip } from '../utils/tooltip-action-utils.js'; import { tooltipPositions } from '../utils/tooltip-utils.js'; const availableTooltipPositionsForViewerMessage = [ tooltipPositions.LEFT, tooltipPositions.LEFT_BOTTOM, tooltipPositions.LEFT_TOP, tooltipPositions.RIGHT, tooltipPositions.RIGHT_BOTTOM, tooltipPositions.RIGHT_TOP, tooltipPositions.BOTTOM, tooltipPositions.TOP, ]; const availableTooltipPositionsForNonViewerMessage = [ tooltipPositions.RIGHT, tooltipPositions.RIGHT_BOTTOM, tooltipPositions.RIGHT_TOP, tooltipPositions.LEFT, tooltipPositions.LEFT_BOTTOM, tooltipPositions.LEFT_TOP, tooltipPositions.BOTTOM, tooltipPositions.TOP, ]; type BaseProps = { +item: ChatMessageInfoItem, +threadInfo: ThreadInfo, + +shouldDisplayPinIndicator: boolean, +sendFailed: boolean, +children: React.Node, +fixedWidth?: boolean, +borderRadius: number, }; type BaseConfig = React.Config; type Props = { ...BaseProps, // withInputState +inputState: ?InputState, +onMouseLeave: ?() => mixed, +onMouseEnter: (event: SyntheticEvent) => mixed, +containsInlineEngagement: boolean, +stringForUser: ?string, }; class ComposedMessage extends React.PureComponent { static defaultProps: { +borderRadius: number } = { borderRadius: 8, }; render(): React.Node { assertComposableMessageType(this.props.item.messageInfo.type); - const { borderRadius, item, threadInfo } = this.props; - const { hasBeenEdited } = item; + const { borderRadius, item, threadInfo, shouldDisplayPinIndicator } = + this.props; + const { hasBeenEdited, isPinned } = item; const { id, creator } = item.messageInfo; const threadColor = threadInfo.color; const { isViewer } = creator; const contentClassName = classNames({ [css.content]: true, [css.viewerContent]: isViewer, [css.nonViewerContent]: !isViewer, }); const messageBoxContainerClassName = classNames({ [css.messageBoxContainer]: true, [css.fixedWidthMessageBoxContainer]: this.props.fixedWidth, [css.messageBoxContainerPositionAvatar]: shouldRenderAvatars, [css.messageBoxContainerPositionNoAvatar]: !shouldRenderAvatars, }); const messageBoxClassName = classNames({ [css.messageBox]: true, [css.fixedWidthMessageBox]: this.props.fixedWidth, }); const messageBoxStyle = { borderTopRightRadius: isViewer && !item.startsCluster ? 0 : borderRadius, borderBottomRightRadius: isViewer && !item.endsCluster ? 0 : borderRadius, borderTopLeftRadius: !isViewer && !item.startsCluster ? 0 : borderRadius, borderBottomLeftRadius: !isViewer && !item.endsCluster ? 0 : borderRadius, }; let authorName = null; const { stringForUser } = this.props; const authorNameClassName = classNames({ [css.authorName]: true, [css.authorNamePositionAvatar]: shouldRenderAvatars, [css.authorNamePositionNoAvatar]: !shouldRenderAvatars, }); if (stringForUser) { authorName = {stringForUser}; } let deliveryIcon = null; let failedSendInfo = null; if (isViewer) { let deliveryIconSpan; let deliveryIconColor = threadColor; if (id !== null && id !== undefined) { deliveryIconSpan = ; } else if (this.props.sendFailed) { deliveryIconSpan = ; deliveryIconColor = 'FF0000'; failedSendInfo = ; } else { deliveryIconSpan = ; } deliveryIcon = ( {deliveryIconSpan} ); } let inlineEngagement = null; const label = getMessageLabel(hasBeenEdited, threadInfo); if ( (this.props.containsInlineEngagement && item.threadCreatedFromMessage) || Object.keys(item.reactions).length > 0 || label ) { const positioning = isViewer ? 'right' : 'left'; inlineEngagement = ( ); } let avatar; if (!isViewer && item.endsCluster && shouldRenderAvatars) { avatar = ( ); } else if (!isViewer && shouldRenderAvatars) { avatar = ; } + const pinIconPositioning = isViewer ? 'left' : 'right'; + const pinIconName = pinIconPositioning === 'left' ? 'pin-mirror' : 'pin'; + const pinIconContainerClassName = classNames({ + [css.pinIconContainer]: true, + [css.pinIconLeft]: pinIconPositioning === 'left', + [css.pinIconRight]: pinIconPositioning === 'right', + }); + let pinIcon; + if (isPinned && shouldDisplayPinIndicator) { + pinIcon = ( + + + + ); + } + return ( {authorName} {avatar} + {pinIcon} {this.props.children} {deliveryIcon} {failedSendInfo} {inlineEngagement} ); } } type ConnectedConfig = React.Config< BaseProps, typeof ComposedMessage.defaultProps, >; const ConnectedComposedMessage: React.ComponentType = React.memo(function ConnectedComposedMessage(props) { const { item, threadInfo } = props; const inputState = React.useContext(InputStateContext); const { creator } = props.item.messageInfo; const { isViewer } = creator; const availablePositions = isViewer ? availableTooltipPositionsForViewerMessage : availableTooltipPositionsForNonViewerMessage; const containsInlineEngagement = !!item.threadCreatedFromMessage; const { onMouseLeave, onMouseEnter } = useMessageTooltip({ item, threadInfo, availablePositions, }); const shouldShowUsername = !isViewer && item.startsCluster; const stringForUser = useStringForUser(shouldShowUsername ? creator : null); return ( ); }); export default ConnectedComposedMessage; diff --git a/web/chat/message.react.js b/web/chat/message.react.js index 622d525b3..677f62a4b 100644 --- a/web/chat/message.react.js +++ b/web/chat/message.react.js @@ -1,51 +1,64 @@ // @flow import invariant from 'invariant'; import * as React from 'react'; import { type ChatMessageInfoItem } from 'lib/selectors/chat-selectors.js'; import { messageTypes } from 'lib/types/message-types.js'; import { type ThreadInfo } from 'lib/types/thread-types.js'; import { longAbsoluteDate } from 'lib/utils/date-utils.js'; import css from './chat-message-list.css'; import MultimediaMessage from './multimedia-message.react.js'; import RobotextMessage from './robotext-message.react.js'; import TextMessage from './text-message.react.js'; type Props = { +item: ChatMessageInfoItem, +threadInfo: ThreadInfo, + +shouldDisplayPinIndicator: boolean, }; function Message(props: Props): React.Node { const { item } = props; let conversationHeader = null; if (item.startsConversation) { conversationHeader = ( {longAbsoluteDate(item.messageInfo.time)} ); } let message; if (item.messageInfo.type === messageTypes.TEXT) { - message = ; + message = ( + + ); } else if ( item.messageInfo.type === messageTypes.IMAGES || item.messageInfo.type === messageTypes.MULTIMEDIA ) { - message = ; + message = ( + + ); } else { invariant(item.robotext, "Flow can't handle our fancy types :("); message = ; } return ( {conversationHeader} {message} ); } export default Message; diff --git a/web/chat/multimedia-message.react.js b/web/chat/multimedia-message.react.js index 84bf024ac..fb7f085a4 100644 --- a/web/chat/multimedia-message.react.js +++ b/web/chat/multimedia-message.react.js @@ -1,94 +1,96 @@ // @flow import invariant from 'invariant'; import * as React from 'react'; import { type ChatMessageInfoItem } from 'lib/selectors/chat-selectors.js'; import { messageTypes } from 'lib/types/message-types.js'; import { type ThreadInfo } from 'lib/types/thread-types.js'; import css from './chat-message-list.css'; import ComposedMessage from './composed-message.react.js'; import sendFailed from './multimedia-message-send-failed.js'; import { type InputState, InputStateContext } from '../input/input-state.js'; import Multimedia from '../media/multimedia.react.js'; type BaseProps = { +item: ChatMessageInfoItem, +threadInfo: ThreadInfo, + +shouldDisplayPinIndicator: boolean, }; type Props = { ...BaseProps, // withInputState +inputState: ?InputState, }; class MultimediaMessage extends React.PureComponent { render() { const { item, inputState } = this.props; invariant( item.messageInfo.type === messageTypes.IMAGES || item.messageInfo.type === messageTypes.MULTIMEDIA, 'MultimediaMessage should only be used for multimedia messages', ); const { localID, media } = item.messageInfo; invariant(inputState, 'inputState should be set in MultimediaMessage'); const pendingUploads = localID ? inputState.assignedUploads[localID] : null; const multimedia = []; for (const singleMedia of media) { const pendingUpload = pendingUploads ? pendingUploads.find(upload => upload.localID === singleMedia.id) : null; let mediaSource; if (singleMedia.type === 'photo' || singleMedia.type === 'video') { mediaSource = { type: singleMedia.type, uri: singleMedia.uri, }; } else { mediaSource = { type: singleMedia.type, holder: singleMedia.holder, encryptionKey: singleMedia.encryptionKey, }; } multimedia.push( , ); } invariant(multimedia.length > 0, 'should be at least one multimedia...'); const content = multimedia.length > 1 ? ( {multimedia} ) : ( multimedia ); return ( 1} borderRadius={16} > {content} ); } } const ConnectedMultimediaMessage: React.ComponentType = React.memo(function ConnectedMultimediaMessage(props) { const inputState = React.useContext(InputStateContext); return ; }); export default ConnectedMultimediaMessage; diff --git a/web/chat/text-message.react.js b/web/chat/text-message.react.js index 55b245323..1b03ffced 100644 --- a/web/chat/text-message.react.js +++ b/web/chat/text-message.react.js @@ -1,67 +1,69 @@ // @flow import classNames from 'classnames'; import invariant from 'invariant'; import * as React from 'react'; import type { ChatMessageInfoItem } from 'lib/selectors/chat-selectors.js'; import { colorIsDark } from 'lib/shared/color-utils.js'; import { onlyEmojiRegex } from 'lib/shared/emojis.js'; import { messageTypes } from 'lib/types/message-types.js'; import { type ThreadInfo } from 'lib/types/thread-types.js'; import css from './chat-message-list.css'; import ComposedMessage from './composed-message.react.js'; import { MessageListContext } from './message-list-types.js'; import textMessageSendFailed from './text-message-send-failed.js'; import Markdown from '../markdown/markdown.react.js'; type Props = { +item: ChatMessageInfoItem, +threadInfo: ThreadInfo, + +shouldDisplayPinIndicator: boolean, }; function TextMessage(props: Props): React.Node { invariant( props.item.messageInfo.type === messageTypes.TEXT, 'TextMessage should only be used for messageTypes.TEXT', ); const { text, creator: { isViewer }, } = props.item.messageInfo; const messageStyle = {}; let darkColor = true; if (isViewer) { const threadColor = props.threadInfo.color; darkColor = colorIsDark(threadColor); messageStyle.backgroundColor = `#${threadColor}`; } const onlyEmoji = onlyEmojiRegex.test(text); const messageClassName = classNames({ [css.textMessage]: true, [css.textMessageDefaultBackground]: !isViewer, [css.normalTextMessage]: !onlyEmoji, [css.emojiOnlyTextMessage]: onlyEmoji, [css.darkTextMessage]: darkColor, [css.lightTextMessage]: !darkColor, }); const messageListContext = React.useContext(MessageListContext); invariant(messageListContext, 'DummyTextNode should have MessageListContext'); const rules = messageListContext.getTextMessageMarkdownRules(darkColor); return ( {text} ); } export default TextMessage; diff --git a/web/components/message-result.react.js b/web/components/message-result.react.js index 716098693..0858eae41 100644 --- a/web/components/message-result.react.js +++ b/web/components/message-result.react.js @@ -1,57 +1,58 @@ // @flow 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, }; function MessageResult(props: MessageResultProps): React.Node { const { item, threadInfo } = 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, ); return ( {username} {longAbsoluteDate(item.messageInfo.time)} ); } export default MessageResult;