From b2d22b2a73ae5e1655323eb2a780a3e42cf27c64 Mon Sep 17 00:00:00 2001 From: Audric Ackermann Date: Tue, 31 Oct 2023 13:57:50 +1100 Subject: [PATCH] fix: hide known message status except for last message --- .../message-content/MessageContent.tsx | 10 +- .../message/message-content/MessageStatus.tsx | 111 ++++++++++++------ 2 files changed, 80 insertions(+), 41 deletions(-) diff --git a/ts/components/conversation/message/message-content/MessageContent.tsx b/ts/components/conversation/message/message-content/MessageContent.tsx index d6a5249d8..a35457ae2 100644 --- a/ts/components/conversation/message/message-content/MessageContent.tsx +++ b/ts/components/conversation/message/message-content/MessageContent.tsx @@ -5,7 +5,7 @@ import React, { createContext, useCallback, useContext, useLayoutEffect, useStat import { InView } from 'react-intersection-observer'; import { useSelector } from 'react-redux'; import styled, { css, keyframes } from 'styled-components'; -import { MessageModelType, MessageRenderingProps } from '../../../../models/messageType'; +import { MessageRenderingProps } from '../../../../models/messageType'; import { StateType } from '../../../../state/reducer'; import { useMessageIsDeleted } from '../../../../state/selectors'; import { @@ -77,14 +77,14 @@ export const StyledMessageHighlighter = styled.div<{ `; const StyledMessageOpaqueContent = styled(StyledMessageHighlighter)<{ - messageDirection: MessageModelType; + isIncoming: boolean; highlight: boolean; }>` background: ${props => - props.messageDirection === 'incoming' + props.isIncoming ? 'var(--message-bubbles-received-background-color)' : 'var(--message-bubbles-sent-background-color)'}; - align-self: ${props => (props.messageDirection === 'incoming' ? 'flex-start' : 'flex-end')}; + align-self: ${props => (props.isIncoming ? 'flex-start' : 'flex-end')}; padding: var(--padding-message-content); border-radius: var(--border-radius-message-box); width: 100%; @@ -183,7 +183,7 @@ export const MessageContent = (props: Props) => { > {hasContentBeforeAttachment && ( - + {!isDeleted && ( <> diff --git a/ts/components/conversation/message/message-content/MessageStatus.tsx b/ts/components/conversation/message/message-content/MessageStatus.tsx index b00cb72fe..f36d336d2 100644 --- a/ts/components/conversation/message/message-content/MessageStatus.tsx +++ b/ts/components/conversation/message/message-content/MessageStatus.tsx @@ -1,9 +1,11 @@ import { ipcRenderer } from 'electron'; -import React from 'react'; +import React, { useCallback } from 'react'; +import { useSelector } from 'react-redux'; import styled from 'styled-components'; import { useMessageExpirationPropsById } from '../../../../hooks/useParamSelector'; import { useMessageStatus } from '../../../../state/selectors'; +import { getMostRecentMessageId } from '../../../../state/selectors/conversations'; import { SpacerXS } from '../../../basic/Text'; import { SessionIcon, SessionIconType } from '../../../icon'; import { ExpireTimer } from '../../ExpireTimer'; @@ -13,6 +15,19 @@ type Props = { dataTestId?: string | undefined; }; +/** + * MessageStatus is used to display the status of an outgoing OR incoming message. + * There are 3 parts to this status: a status text, a status icon and a expiring stopwatch. + * At all times, we either display `text + icon` OR `text + stopwatch`. + * + * The logic to display the text is : + * - if the message is expiring: + * - if the message is incoming: display its 'read' state and the stopwatch icon (1) + * - if the message is outgoing: display its status and the stopwatch, unless when the status is error or sending (just display icon and text in this case, no stopwatch) (2) + * - if the message is not expiring: + * - if the message is incoming: do not show anything (3) + * - if the message is outgoing: show the text for the last message, or a message sending, or in the error state. (4) + */ export const MessageStatus = (props: Props) => { const { dataTestId, messageId } = props; const status = useMessageStatus(props.messageId); @@ -25,40 +40,39 @@ export const MessageStatus = (props: Props) => { if (isIncoming) { if (selected.isUnread || !selected.expirationDurationMs || !selected.expirationTimestamp) { - return null; + return null; // incoming and not expiring, this is case (3) above } - return ( - - ); + // incoming and expiring, this is case (1) above + return ; } - // this is the outgoing state: we display the text and the icon or the text and the expiretimer stopwatch when the message is expiring switch (status) { case 'sending': - return ; + return ; // we always show sending state case 'sent': return ; case 'read': - return ; + return ; // read is used for both incoming and outgoing messages, but not with the same UI case 'error': - return ; + return ; // we always show error state default: return null; } }; -const MessageStatusContainer = styled.div<{ reserveDirection?: boolean }>` +const MessageStatusContainer = styled.div<{ isIncoming: boolean }>` display: inline-block; - align-self: flex-end; + align-self: ${props => (props.isIncoming ? 'flex-start' : 'flex-end')}; + flex-direction: ${props => + props.isIncoming + ? 'row-reverse' + : 'row'}; // we want {icon}{text} for incoming read messages, but {text}{icon} for outgoing messages + margin-bottom: 2px; margin-inline-start: 5px; cursor: pointer; display: flex; align-items: baseline; - flex-direction: ${props => - props.reserveDirection - ? 'row-reverse' - : 'row'}; // we want {icon}{text} for incoming read messages, but {text}{icon} for outgoing messages `; const StyledStatusText = styled.div` @@ -103,6 +117,12 @@ function useIsExpiring(messageId: string) { ); } +function useIsMostRecentMessage(messageId: string) { + const mostRecentMessageId = useSelector(getMostRecentMessageId); + const isMostRecentMessage = mostRecentMessageId === messageId; + return isMostRecentMessage; +} + function MessageStatusExpireTimer(props: Props) { const selected = useMessageExpirationPropsById(props.messageId); if ( @@ -124,24 +144,42 @@ function MessageStatusExpireTimer(props: Props) { const MessageStatusSending = ({ dataTestId }: Props) => { // while sending, we do not display the expire timer at all. return ( - + ); }; +/** + * Returns the correct expiring stopwatch icon if this message is expiring, or a normal status icon otherwise. + * Only to be used with the status "read" and "sent" + */ +function IconForExpiringMessageId({ + messageId, + iconType, +}: Pick & { iconType: SessionIconType }) { + const isExpiring = useIsExpiring(messageId); + + return isExpiring ? ( + + ) : ( + + ); +} + const MessageStatusSent = ({ dataTestId, messageId }: Props) => { const isExpiring = useIsExpiring(messageId); + const isMostRecentMessage = useIsMostRecentMessage(messageId); + // we hide a "sent" message status which is not expiring except for the most recent message + if (!isExpiring && !isMostRecentMessage) { + return null; + } return ( - + - {isExpiring ? ( - - ) : ( - - )} + ); }; @@ -149,29 +187,29 @@ const MessageStatusSent = ({ dataTestId, messageId }: Props) => { const MessageStatusRead = ({ dataTestId, messageId, - reserveDirection, -}: Props & { reserveDirection?: boolean }) => { + isIncoming, +}: Props & { isIncoming: boolean }) => { const isExpiring = useIsExpiring(messageId); + + const isMostRecentMessage = useIsMostRecentMessage(messageId); + + // we hide an outgoing "read" message status which is not expiring except for the most recent message + if (!isIncoming && !isExpiring && !isMostRecentMessage) { + return null; + } + return ( - + - {isExpiring ? ( - - ) : ( - - )} + ); }; const MessageStatusError = ({ dataTestId }: Props) => { - const showDebugLog = () => { + const showDebugLog = useCallback(() => { ipcRenderer.send('show-debug-log'); - }; + }, []); // when on errro, we do not display the expire timer at all. return ( @@ -180,6 +218,7 @@ const MessageStatusError = ({ dataTestId }: Props) => { data-testtype="failed" onClick={showDebugLog} title={window.i18n('sendFailed')} + isIncoming={false} >