From 615722434b59ec6c63c458f6c519380d5b87b4d2 Mon Sep 17 00:00:00 2001 From: Audric Ackermann Date: Tue, 31 Oct 2023 11:01:13 +1100 Subject: [PATCH] feat: merge expiring stopwatch and messagestatus together --- _locales/en/messages.json | 3 + ts/components/conversation/ExpireTimer.tsx | 13 +- .../conversation/TimerNotification.tsx | 11 +- .../MessageContentWithStatus.tsx | 31 ++- .../message/message-content/MessageStatus.tsx | 186 ++++++++++++++++-- .../message-content/OutgoingMessageStatus.tsx | 75 ------- .../DataExtractionNotification.tsx | 4 +- .../message-item/ExpirableReadableMessage.tsx | 57 +++--- .../message-item/GroupUpdateMessage.tsx | 10 +- .../message/message-item/ReadableMessage.tsx | 2 +- .../notification-bubble/CallNotification.tsx | 2 +- .../NotificationBubble.tsx | 2 +- .../conversation-list-item/MessageItem.tsx | 37 +++- ts/types/LocalizerKeys.ts | 3 + 14 files changed, 274 insertions(+), 162 deletions(-) delete mode 100644 ts/components/conversation/message/message-content/OutgoingMessageStatus.tsx diff --git a/_locales/en/messages.json b/_locales/en/messages.json index ee95562c0..587350f1c 100644 --- a/_locales/en/messages.json +++ b/_locales/en/messages.json @@ -109,10 +109,13 @@ "from": "From:", "to": "To:", "sent": "Sent", + "sending": "Sending", "received": "Received", "sendMessage": "Message", "groupMembers": "Members", "moreInformation": "More information", + "failed": "Failed", + "read": "Read", "resend": "Resend", "deleteConversationConfirmation": "Permanently delete the messages in this conversation?", "clear": "Clear", diff --git a/ts/components/conversation/ExpireTimer.tsx b/ts/components/conversation/ExpireTimer.tsx index 2675ee4bd..ff6a03ad9 100644 --- a/ts/components/conversation/ExpireTimer.tsx +++ b/ts/components/conversation/ExpireTimer.tsx @@ -12,13 +12,14 @@ const ExpireTimerBucket = styled.div` letter-spacing: 0.3px; text-transform: uppercase; user-select: none; - color: var(--text-primary-color); + color: var(--text-secondary-color); + align-self: center; `; type Props = { - expirationDurationMs: number; - expirationTimestamp: number | null; - style: CSSProperties; + expirationDurationMs?: number; + expirationTimestamp?: number | null; + style?: CSSProperties; }; export const ExpireTimer = (props: Props) => { @@ -43,13 +44,11 @@ export const ExpireTimer = (props: Props) => { return null; } - const expireTimerColor = 'var(--primary-text-color)'; - const bucket = getTimerBucketIcon(expirationTimestamp, expirationDurationMs); return ( - + ); }; diff --git a/ts/components/conversation/TimerNotification.tsx b/ts/components/conversation/TimerNotification.tsx index d35760681..ace4c66fe 100644 --- a/ts/components/conversation/TimerNotification.tsx +++ b/ts/components/conversation/TimerNotification.tsx @@ -5,8 +5,7 @@ import { assertUnreachable } from '../../types/sqlSharedTypes'; import { isLegacyDisappearingModeEnabled } from '../../session/disappearing_messages/legacy'; import { Flex } from '../basic/Flex'; -import { SpacerSM, Text } from '../basic/Text'; -import { SessionIcon } from '../icon'; +import { Text } from '../basic/Text'; import { ExpirableReadableMessage } from './message/message-item/ExpirableReadableMessage'; export const TimerNotification = (props: PropsForExpirationTimer) => { @@ -48,7 +47,7 @@ export const TimerNotification = (props: PropsForExpirationTimer) => { return ( @@ -59,13 +58,11 @@ export const TimerNotification = (props: PropsForExpirationTimer) => { justifyContent="center" width="90%" maxWidth="700px" - margin="10px auto" + margin="5px auto 10px auto" // top margin is smaller that bottom one to make the stopwatch icon of expirable message closer to its content padding="5px 10px" style={{ textAlign: 'center' }} > - - - + ); diff --git a/ts/components/conversation/message/message-content/MessageContentWithStatus.tsx b/ts/components/conversation/message/message-content/MessageContentWithStatus.tsx index 8876b4b93..a4881a1fb 100644 --- a/ts/components/conversation/message/message-content/MessageContentWithStatus.tsx +++ b/ts/components/conversation/message/message-content/MessageContentWithStatus.tsx @@ -12,6 +12,7 @@ import { isMessageSelectionMode, } from '../../../../state/selectors/conversations'; import { Reactions } from '../../../../util/reactions'; +import { Flex } from '../../../basic/Flex'; import { ExpirableReadableMessage } from '../message-item/ExpirableReadableMessage'; import { MessageAuthorText } from './MessageAuthorText'; import { MessageAvatar } from './MessageAvatar'; @@ -33,11 +34,11 @@ type Props = { enableReactions: boolean; }; -const StyledMessageContentContainer = styled.div<{ direction: 'left' | 'right' }>` +const StyledMessageContentContainer = styled.div<{ isIncoming: boolean }>` display: flex; flex-direction: column; justify-content: flex-start; - align-items: ${props => (props.direction === 'left' ? 'flex-start' : 'flex-end')}; + align-items: ${props => (props.isIncoming ? 'flex-start' : 'flex-end')}; width: 100%; ${StyledMessageReactions} { @@ -46,7 +47,7 @@ const StyledMessageContentContainer = styled.div<{ direction: 'left' | 'right' } `; const StyledMessageWithAuthor = styled.div<{ isIncoming: boolean }>` - max-width: ${props => (props.isIncoming ? '100%' : 'calc(100% - 17px)')}; + max-width: '100%'; display: flex; flex-direction: column; min-width: 0; @@ -118,7 +119,7 @@ export const MessageContentWithStatuses = (props: Props) => { return ( { setPopupReaction(''); }} @@ -134,20 +135,14 @@ export const MessageContentWithStatuses = (props: Props) => { - - - - - - + + + + + + + + {!isDeleted && ( ; - export const MessageStatus = (props: Props) => { - const { isCorrectSide, dataTestId } = props; - const direction = useMessageDirection(props.messageId); + const { dataTestId, messageId } = props; const status = useMessageStatus(props.messageId); + const selected = useMessageExpirationPropsById(props.messageId); - if (!props.messageId) { + if (!props.messageId || !selected) { return null; } + const isIncoming = selected.direction === 'incoming'; - if (!isCorrectSide) { - return null; + if (isIncoming) { + if (selected.isUnread || !selected.expirationDurationMs || !selected.expirationTimestamp) { + return null; + } + return ( + + ); } - const isIncoming = direction === 'incoming'; - const showStatus = !isIncoming && Boolean(status); - if (!showStatus) { + // 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 ; + case 'sent': + return ; + case 'read': + return ; + case 'error': + return ; + default: + return null; + } +}; + +const MessageStatusContainer = styled.div<{ reserveDirection?: boolean }>` + display: inline-block; + align-self: flex-end; + 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` + color: var(--text-secondary-color); + font-size: small; +`; + +const TextDetails = ({ text }: { text: string }) => { + return ( + <> + {text} + + + ); +}; + +function IconDanger({ iconType }: { iconType: SessionIconType }) { + return ; +} + +function IconNormal({ + iconType, + rotateDuration, +}: { + iconType: SessionIconType; + rotateDuration?: number | undefined; +}) { + return ( + + ); +} + +function useIsExpiring(messageId: string) { + const selected = useMessageExpirationPropsById(messageId); + return ( + selected && selected.expirationDurationMs && selected.expirationTimestamp && !selected.isExpired + ); +} + +function MessageStatusExpireTimer(props: Props) { + const selected = useMessageExpirationPropsById(props.messageId); + if ( + !selected || + !selected.expirationDurationMs || + !selected.expirationTimestamp || + selected.isExpired + ) { return null; } + return ( + + ); +} + +const MessageStatusSending = ({ dataTestId }: Props) => { + // while sending, we do not display the expire timer at all. + return ( + + + + + ); +}; + +const MessageStatusSent = ({ dataTestId, messageId }: Props) => { + const isExpiring = useIsExpiring(messageId); + + return ( + + + {isExpiring ? ( + + ) : ( + + )} + + ); +}; + +const MessageStatusRead = ({ + dataTestId, + messageId, + reserveDirection, +}: Props & { reserveDirection?: boolean }) => { + const isExpiring = useIsExpiring(messageId); + return ( + + + {isExpiring ? ( + + ) : ( + + )} + + ); +}; + +const MessageStatusError = ({ dataTestId }: Props) => { + const showDebugLog = () => { + ipcRenderer.send('show-debug-log'); + }; + // when on errro, we do not display the expire timer at all. - return ; + return ( + + + + + ); }; diff --git a/ts/components/conversation/message/message-content/OutgoingMessageStatus.tsx b/ts/components/conversation/message/message-content/OutgoingMessageStatus.tsx deleted file mode 100644 index c71d4097e..000000000 --- a/ts/components/conversation/message/message-content/OutgoingMessageStatus.tsx +++ /dev/null @@ -1,75 +0,0 @@ -import { ipcRenderer } from 'electron'; -import React from 'react'; -import styled from 'styled-components'; -import { LastMessageStatusType } from '../../../../state/ducks/conversations'; -import { SessionIcon } from '../../../icon'; - -const MessageStatusSendingContainer = styled.div` - display: inline-block; - align-self: flex-end; - margin-bottom: 2px; - margin-inline-start: 5px; - cursor: pointer; -`; - -const iconColor = 'var(--text-primary-color)'; - -const MessageStatusSending = ({ dataTestId }: { dataTestId?: string }) => { - return ( - - - - ); -}; - -const MessageStatusSent = ({ dataTestId }: { dataTestId?: string }) => { - return ( - - - - ); -}; - -const MessageStatusRead = ({ dataTestId }: { dataTestId?: string }) => { - return ( - - - - ); -}; - -const MessageStatusError = ({ dataTestId }: { dataTestId?: string }) => { - const showDebugLog = () => { - ipcRenderer.send('show-debug-log'); - }; - - return ( - - - - ); -}; - -export const OutgoingMessageStatus = (props: { - status: LastMessageStatusType | null; - dataTestId?: string; -}) => { - const { status, dataTestId } = props; - switch (status) { - case 'sending': - return ; - case 'sent': - return ; - case 'read': - return ; - case 'error': - return ; - default: - return null; - } -}; diff --git a/ts/components/conversation/message/message-item/DataExtractionNotification.tsx b/ts/components/conversation/message/message-item/DataExtractionNotification.tsx index 57ccaaa53..c33b8e3c5 100644 --- a/ts/components/conversation/message/message-item/DataExtractionNotification.tsx +++ b/ts/components/conversation/message/message-item/DataExtractionNotification.tsx @@ -21,7 +21,7 @@ export const DataExtractionNotification = (props: PropsForDataExtractionNotifica messageId={messageId} dataTestId="data-extraction-notification" key={`readable-message-${messageId}`} - isCentered={true} + isControlMessage={true} > ` display: flex; - justify-content: ${props => (props.isIncoming ? 'flex-start' : 'flex-end')}; - align-items: center; + justify-content: flex-end; // ${props => (props.isIncoming ? 'flex-start' : 'flex-end')}; + align-items: ${props => (props.isIncoming ? 'flex-start' : 'flex-end')}; width: 100%; + flex-direction: column; `; export interface ExpirableReadableMessageProps extends Omit { messageId: string; - isCentered?: boolean; + isControlMessage?: boolean; +} + +function ExpireTimerControlMessage({ + expirationTimestamp, + expirationDurationMs, + isControlMessage, +}: { + expirationDurationMs: number | null | undefined; + expirationTimestamp: number | null | undefined; + isControlMessage: boolean | undefined; +}) { + if (!isControlMessage) { + return null; + } + return ( + + ); } export const ExpirableReadableMessage = (props: ExpirableReadableMessageProps) => { const selected = useMessageExpirationPropsById(props.messageId); - const { isCentered, onClick, onDoubleClickCapture, role, dataTestId } = props; + const { isControlMessage, onClick, onDoubleClickCapture, role, dataTestId } = props; const { isExpired } = useIsExpired({ convoId: selected?.convoId, @@ -126,30 +147,12 @@ export const ExpirableReadableMessage = (props: ExpirableReadableMessageProps) = key={`readable-message-${messageId}`} dataTestId={dataTestId} > - {expirationDurationMs && expirationTimestamp ? ( - - ) : null} + {props.children} - {expirationDurationMs && expirationTimestamp ? ( - - ) : null} ); }; diff --git a/ts/components/conversation/message/message-item/GroupUpdateMessage.tsx b/ts/components/conversation/message/message-item/GroupUpdateMessage.tsx index 5e6d0cc81..c16f31d46 100644 --- a/ts/components/conversation/message/message-item/GroupUpdateMessage.tsx +++ b/ts/components/conversation/message/message-item/GroupUpdateMessage.tsx @@ -1,14 +1,14 @@ import React from 'react'; +import { useConversationsUsernameWithQuoteOrFullPubkey } from '../../../../hooks/useParamSelector'; +import { arrayContainsUsOnly } from '../../../../models/message'; import { PropsForGroupUpdate, PropsForGroupUpdateType, } from '../../../../state/ducks/conversations'; -import { NotificationBubble } from './notification-bubble/NotificationBubble'; -import { ExpirableReadableMessage } from './ExpirableReadableMessage'; -import { arrayContainsUsOnly } from '../../../../models/message'; -import { useConversationsUsernameWithQuoteOrFullPubkey } from '../../../../hooks/useParamSelector'; import { assertUnreachable } from '../../../../types/sqlSharedTypes'; +import { ExpirableReadableMessage } from './ExpirableReadableMessage'; +import { NotificationBubble } from './notification-bubble/NotificationBubble'; // This component is used to display group updates in the conversation view. @@ -80,7 +80,7 @@ export const GroupUpdateMessage = (props: PropsForGroupUpdate) => { messageId={messageId} key={`readable-message-${messageId}`} dataTestId="group-update-message" - isCentered={true} + isControlMessage={true} > diff --git a/ts/components/conversation/message/message-item/ReadableMessage.tsx b/ts/components/conversation/message/message-item/ReadableMessage.tsx index d20c25259..ab93457bb 100644 --- a/ts/components/conversation/message/message-item/ReadableMessage.tsx +++ b/ts/components/conversation/message/message-item/ReadableMessage.tsx @@ -41,6 +41,7 @@ export type ReadableMessageProps = { role?: AriaRole; dataTestId: string; onContextMenu?: (e: React.MouseEvent) => void; + isControlMessage?: boolean; }; const debouncedTriggerLoadMoreTop = debounce( @@ -98,7 +99,6 @@ export const ReadableMessage = (props: ReadableMessageProps) => { // if this unread-indicator is rendered, // we want to scroll here only if the conversation was not opened to a specific message - // eslint-disable-next-line react-hooks/exhaustive-deps useLayoutEffect(() => { if ( diff --git a/ts/components/conversation/message/message-item/notification-bubble/CallNotification.tsx b/ts/components/conversation/message/message-item/notification-bubble/CallNotification.tsx index 646cf438a..ea983a896 100644 --- a/ts/components/conversation/message/message-item/notification-bubble/CallNotification.tsx +++ b/ts/components/conversation/message/message-item/notification-bubble/CallNotification.tsx @@ -63,7 +63,7 @@ export const CallNotification = (props: PropsForCallNotification) => { messageId={messageId} key={`readable-message-${messageId}`} dataTestId={`call-notification-${notificationType}`} - isCentered={true} + isControlMessage={true} > { )} {!isSearchingMode && lastMessage && lastMessage.status && !isMessageRequest ? ( - + ) : null} ); }; + +function IconMessageStatus({ status }: { status: LastMessageStatusType }) { + const nonErrorIconColor = 'var(--text-secondary-color'; + switch (status) { + case 'error': + return ; + case 'read': + return ( + + ); + case 'sending': + return ( + + ); + case 'sent': + return ; + case undefined: + return null; + default: + assertUnreachable(status, 'missing case error'); + } +} diff --git a/ts/types/LocalizerKeys.ts b/ts/types/LocalizerKeys.ts index 89f6d81c9..6ccb5b4ac 100644 --- a/ts/types/LocalizerKeys.ts +++ b/ts/types/LocalizerKeys.ts @@ -188,6 +188,7 @@ export type LocalizerKeys = | 'error' | 'establishingConnection' | 'expandedReactionsText' + | 'failed' | 'failedResolveOns' | 'failedToAddAsModerator' | 'failedToRemoveFromModerator' @@ -364,6 +365,7 @@ export type LocalizerKeys = | 'reactionPopupOne' | 'reactionPopupThree' | 'reactionPopupTwo' + | 'read' | 'readReceiptSettingDescription' | 'readReceiptSettingTitle' | 'received' @@ -404,6 +406,7 @@ export type LocalizerKeys = | 'sendMessage' | 'sendRecoveryPhraseMessage' | 'sendRecoveryPhraseTitle' + | 'sending' | 'sent' | 'sessionMessenger' | 'set'