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'