Improve styles for ConversationListItem
- Display last message
- Fix user talking to themself
Change-Id: Ia5bb3f9cd86a389f94bbfb3e279e7a82878f98ed
diff --git a/client/src/App.tsx b/client/src/App.tsx
index afd9817..dff99b7 100644
--- a/client/src/App.tsx
+++ b/client/src/App.tsx
@@ -15,6 +15,8 @@
* License along with this program. If not, see
* <https://www.gnu.org/licenses/>.
*/
+import './dayjsInitializer'; // Initialized once and globally to ensure available locales are always imported
+
import axios from 'axios';
import { useState } from 'react';
import { json, LoaderFunctionArgs, Outlet, redirect } from 'react-router-dom';
diff --git a/client/src/components/ConversationListItem.tsx b/client/src/components/ConversationListItem.tsx
index 710fe0b..985ee2c 100644
--- a/client/src/components/ConversationListItem.tsx
+++ b/client/src/components/ConversationListItem.tsx
@@ -15,7 +15,8 @@
* License along with this program. If not, see
* <https://www.gnu.org/licenses/>.
*/
-import { Box, ListItem, ListItemAvatar, ListItemText } from '@mui/material';
+import { Box, ListItemButton, Stack, Typography } from '@mui/material';
+import dayjs from 'dayjs';
import { IConversationSummary } from 'jami-web-common';
import { QRCodeCanvas } from 'qrcode.react';
import { useCallback, useContext, useMemo, useState } from 'react';
@@ -29,6 +30,8 @@
import { setRefreshFromSlice } from '../redux/appSlice';
import { useAppDispatch } from '../redux/hooks';
import { ConversationRouteParams } from '../router';
+import { getMessageCallText, getMessageMemberText } from '../utils/chatmessages';
+import { formatRelativeDate, formatTime } from '../utils/dates×';
import ContextMenu, { ContextMenuHandler, useContextMenuHandler } from './ContextMenu';
import ConversationAvatar from './ConversationAvatar';
import { ConfirmationDialog, DialogContentList, InfosDialog, useDialogHandler } from './Dialog';
@@ -40,13 +43,14 @@
};
export default function ConversationListItem({ conversationSummary }: ConversationListItemProps) {
+ const { account } = useAuthContext();
const {
urlParams: { conversationId: selectedConversationId },
} = useUrlParams<ConversationRouteParams>();
const contextMenuHandler = useContextMenuHandler();
const callContext = useCallContext(true);
const { callData } = useContext(CallManagerContext);
- const { t } = useTranslation();
+ const { t, i18n } = useTranslation();
const navigate = useNavigate();
const conversationId = conversationSummary.id;
@@ -58,9 +62,41 @@
}
}, [navigate, conversationId]);
- const secondaryText = useMemo(() => {
+ const timeIndicator = useMemo(() => {
+ const message = conversationSummary.lastMessage;
+ const time = dayjs.unix(Number(message.timestamp));
+ if (time.isToday()) {
+ return formatTime(time, i18n);
+ } else {
+ return formatRelativeDate(time, i18n);
+ }
+ }, [conversationSummary, i18n]);
+
+ const lastMessageText = useMemo(() => {
if (!callContext || !callData || callData.conversationId !== conversationSummary.id) {
- return conversationSummary.lastMessage.body;
+ const message = conversationSummary.lastMessage;
+ switch (message.type) {
+ case 'initial': {
+ return t('message_swarm_created');
+ }
+ case 'application/data-transfer+json': {
+ return message.fileId;
+ }
+ case 'application/call-history+json': {
+ const isAccountMessage = message.author === account.getUri();
+ return getMessageCallText(isAccountMessage, message, i18n);
+ }
+ case 'member': {
+ return getMessageMemberText(message, i18n);
+ }
+ case 'text/plain': {
+ return message.body;
+ }
+ default: {
+ console.error(`${ConversationListItem.name} received an unexpected lastMessage type: ${message.type}`);
+ return '';
+ }
+ }
}
if (callContext.callStatus === CallStatus.InCall) {
@@ -72,11 +108,11 @@
}
return callContext.callRole === 'caller' ? t('outgoing_call') : t('incoming_call');
- }, [conversationSummary, callContext, callData, t]);
+ }, [account, conversationSummary, callContext, callData, t, i18n]);
const conversationName = useMemo(
- () => conversationSummary.title ?? conversationSummary.membersNames.join(', '),
- [conversationSummary]
+ () => conversationSummary.title || conversationSummary.membersNames.join(', ') || account.getDisplayName(),
+ [account, conversationSummary]
);
return (
@@ -88,18 +124,20 @@
isSelected={isSelected}
contextMenuProps={contextMenuHandler.props}
/>
- <ListItem
- button
- alignItems="flex-start"
- selected={isSelected}
- onClick={onClick}
- onContextMenu={contextMenuHandler.handleAnchorPosition}
- >
- <ListItemAvatar>
+ <ListItemButton alignItems="flex-start" selected={isSelected} onClick={onClick}>
+ <Stack direction="row" spacing="10px">
<ConversationAvatar displayName={conversationName} />
- </ListItemAvatar>
- <ListItemText primary={conversationName} secondary={secondaryText} />
- </ListItem>
+ <Stack>
+ <Typography variant="body1">{conversationName}</Typography>
+ <Stack direction="row" spacing="5px">
+ <Typography variant="body2" fontWeight={isSelected ? 'bold' : 'normal'}>
+ {timeIndicator}
+ </Typography>
+ <Typography variant="body2">{lastMessageText}</Typography>
+ </Stack>
+ </Stack>
+ </Stack>
+ </ListItemButton>
</Box>
);
}
diff --git a/client/src/components/Message.tsx b/client/src/components/Message.tsx
index 8807295..a87d628 100644
--- a/client/src/components/Message.tsx
+++ b/client/src/components/Message.tsx
@@ -17,14 +17,15 @@
*/
import { Box, Chip, Divider, Stack, Tooltip, Typography } from '@mui/material';
import { styled } from '@mui/material/styles';
-import { Dayjs } from 'dayjs';
+import dayjs, { Dayjs } from 'dayjs';
import { Message } from 'jami-web-common';
import { ReactElement, ReactNode, useCallback, useMemo, useState } from 'react';
import { useTranslation } from 'react-i18next';
-import dayjs from '../dayjsInitializer';
import { Account } from '../models/account';
import { Contact } from '../models/contact';
+import { getMessageCallText, getMessageMemberText } from '../utils/chatmessages';
+import { formatRelativeDate, formatTime } from '../utils/dates×';
import { EmojiButton, MoreButton, ReplyMessageButton } from './Button';
import ConversationAvatar from './ConversationAvatar';
import PopoverList, { PopoverListItemData } from './PopoverList';
@@ -93,48 +94,44 @@
const MessageCall = ({ message, isAccountMessage, isFirstOfGroup, isLastOfGroup }: MessageCallProps) => {
const position = isAccountMessage ? 'end' : 'start';
- const { t } = useTranslation();
+ const { i18n } = useTranslation();
const { bubbleColor, Icon, text, textColor } = useMemo(() => {
+ const text = getMessageCallText(isAccountMessage, message, i18n);
const callDuration = dayjs.duration(parseInt(message?.duration || ''));
if (callDuration.asSeconds() === 0) {
if (isAccountMessage) {
return {
- text: t('message_call_outgoing_missed'),
+ text,
Icon: ArrowLeftCurved,
textColor: 'white',
bubbleColor: '#005699' + '80', // opacity 50%
};
} else {
return {
- text: t('message_call_incoming_missed'),
+ text,
Icon: ArrowLeftCurved,
textColor: 'black',
bubbleColor: '#C6C6C6',
};
}
} else {
- const minutes = Math.floor(callDuration.asMinutes()).toString().padStart(2, '0');
- const seconds = callDuration.format('ss');
- const interpolations = {
- duration: `${minutes}:${seconds}`,
- };
if (isAccountMessage) {
return {
- text: t('message_call_outgoing', interpolations),
+ text,
Icon: ArrowRightUp,
textColor: 'white',
bubbleColor: '#005699',
};
} else {
return {
- text: t('message_call_incoming', interpolations),
+ text,
Icon: ArrowLeftDown,
textcolor: 'black',
bubbleColor: '#E5E5E5',
};
}
}
- }, [isAccountMessage, message, t]);
+ }, [isAccountMessage, message, i18n]);
return (
<Bubble position={position} isFirstOfGroup={isFirstOfGroup} isLastOfGroup={isLastOfGroup} bubbleColor={bubbleColor}>
@@ -174,13 +171,15 @@
}
const MessageMember = ({ message }: MessageMemberProps) => {
- const { t } = useTranslation();
+ const { i18n } = useTranslation();
+
+ const text = getMessageMemberText(message, i18n);
return (
<Chip
sx={{
width: 'fit-content',
}}
- label={t('message_user_joined', { user: message.author })}
+ label={text}
/>
);
};
@@ -218,16 +217,7 @@
const DateIndicator = ({ time }: DateIndicatorProps) => {
const { i18n } = useTranslation();
-
- const textDate = useMemo(() => {
- if (time.isToday()) {
- return new Intl.RelativeTimeFormat(i18n.language, { numeric: 'auto' }).format(0, 'day');
- } else if (time.isYesterday()) {
- return new Intl.RelativeTimeFormat(i18n.language, { numeric: 'auto' }).format(-1, 'day');
- } else {
- return dayjs(time).locale(i18n.language).format('L');
- }
- }, [i18n, time]);
+ const textDate = useMemo(() => formatRelativeDate(time, i18n), [time, i18n]);
return (
<Box marginTop="30px">
@@ -267,10 +257,7 @@
const TimeIndicator = ({ time, hasDateOnTop }: TimeIndicatorProps) => {
const { i18n } = useTranslation();
-
- const textTime = useMemo(() => {
- return dayjs(time).locale(i18n.language).format('LT');
- }, [i18n, time]);
+ const textTime = useMemo(() => formatTime(time, i18n), [time, i18n]);
return (
<Stack direction="row" justifyContent="center" marginTop={hasDateOnTop ? '20px' : '30px'}>
diff --git a/client/src/contexts/ConversationProvider.tsx b/client/src/contexts/ConversationProvider.tsx
index b6d2f4a..9e805e3 100644
--- a/client/src/contexts/ConversationProvider.tsx
+++ b/client/src/contexts/ConversationProvider.tsx
@@ -63,7 +63,7 @@
const conversationInfos = conversationInfosQuery.data;
const members = membersQuery.data;
- const conversationDisplayName = useConversationDisplayName(account, conversationId, conversationInfos, members);
+ const conversationDisplayName = useConversationDisplayName(account, conversationInfos, members);
useEffect(() => {
if (!conversationInfos || !conversationId || !webSocket) {
diff --git a/client/src/dayjsInitializer.ts b/client/src/dayjsInitializer.ts
index cff6ed1..6d01566 100644
--- a/client/src/dayjsInitializer.ts
+++ b/client/src/dayjsInitializer.ts
@@ -32,5 +32,3 @@
dayjs.extend(isToday);
dayjs.extend(isYesterday);
dayjs.extend(localizedFormat);
-
-export default dayjs;
diff --git a/client/src/hooks/useConversationDisplayName.ts b/client/src/hooks/useConversationDisplayName.ts
index c8aabae..c668493 100644
--- a/client/src/hooks/useConversationDisplayName.ts
+++ b/client/src/hooks/useConversationDisplayName.ts
@@ -25,9 +25,8 @@
export const useConversationDisplayName = (
account: Account,
- conversationId: string | undefined,
conversationInfos: ConversationInfos | undefined,
- members: ConversationMember[] | undefined
+ members: ConversationMember[] | undefined = []
) => {
const { t } = useTranslation();
@@ -38,10 +37,6 @@
return adminTitle;
}
- if (!members) {
- return conversationId;
- }
-
const options: TranslateEnumerationOptions<ConversationMember> = {
elementPartialKey: 'member',
getElementValue: (member) => member.getDisplayName(),
@@ -58,5 +53,5 @@
};
return translateEnumeration<ConversationMember>(members, options);
- }, [account, adminTitle, conversationId, members, t]);
+ }, [account, adminTitle, members, t]);
};
diff --git a/client/src/locale/en/translation.json b/client/src/locale/en/translation.json
index 9634028..36d276f 100644
--- a/client/src/locale/en/translation.json
+++ b/client/src/locale/en/translation.json
@@ -1,6 +1,12 @@
{
"severity": "",
"change_picture": "Change the picture",
+ "message_swarm_created": "Swarm created",
+ "message_call_outgoing_missed": "Missed outgoing call",
+ "message_call_incoming_missed": "Missed incoming call",
+ "message_call_outgoing": "Outgoing call - {{duration}}",
+ "message_call_incoming": "Incoming call - {{duration}}",
+ "message_member_joined": "{{user}} joined",
"ongoing_call_unmuted": "Ongoing call",
"ongoing_call_muted": "Ongoing call (muted)",
"connecting_call": "Connecting...",
@@ -11,7 +17,6 @@
"conversation_start_videocall": "Start video call",
"conversation_close": "Close this conversation",
"conversation_details": "Conversation details",
- "conversation_block": "Block conversation",
"conversation_delete": "Remove conversation",
"conversation_details_name": "Title",
"conversation_details_identifier": "Identifier",
@@ -21,15 +26,8 @@
"conversation_details_is_swarm_false": "False",
"conversation_details_informations": "Informations",
"dialog_confirm_title_default": "Confirm action",
- "conversation_ask_confirm_block": "Would you really like to block this conversation?",
- "conversation_confirm_block": "Block",
"conversation_ask_confirm_remove": "Would you really like to remove this conversation?",
"conversation_confirm_remove": "Remove",
- "conversation_title_one": "{{member0}}",
- "conversation_title_two": "{{member0}} and {{member1}}",
- "conversation_title_three": "{{member0}}, {{member1}} and {{member2}}",
- "conversation_title_four": "{{member0}}, {{member1}}, {{member2}}, +1 other member",
- "conversation_title_more": "{{member0}}, {{member1}}, {{member2}}, +{{excess}} other members",
"select_placeholder": "Select an option",
"dialog_close": "Close",
"dialog_cancel": "Cancel",
@@ -62,12 +60,10 @@
"username_rule_three": "The username may contain hyphens (-).",
"username_rule_four": "The username may contain underscores (_).",
"welcome_text": "Welcome to Jami!",
- "message_call_outgoing_missed": "Missed outgoing call",
- "message_call_incoming_missed": "Missed incoming call",
- "message_call_outgoing": "Outgoing call - {{duration}}",
- "message_call_incoming": "Incoming call - {{duration}}",
- "message_swarm_created": "Swarm created",
- "message_user_joined": "{{user}} joined",
+ "message_member_invited": "{{user}} was invited to join",
+ "message_member_left": "{{user}} left",
+ "message_member_banned": "{{user}} was kicked",
+ "message_member_unbanned": "{{user}} was re-added",
"messages_scroll_to_end": "Scroll to end of conversation",
"conversation_add_contact_form": "Add a contact",
"message_input_placeholder_one": "Write to {{member0}}",
@@ -76,6 +72,11 @@
"message_input_placeholder_four": "Write to {{member0}}, {{member1}}, {{member2}}, +1 other member",
"message_input_placeholder_more": "Write to {{member0}}, {{member1}}, {{member2}}, +{{excess}} other members",
"missed_incoming_call": "Missed incoming call from conversation {{conversationId}}",
+ "conversation_title_one": "{{member0}}",
+ "conversation_title_two": "{{member0}} and {{member1}}",
+ "conversation_title_three": "{{member0}}, {{member1}} and {{member2}}",
+ "conversation_title_four": "{{member0}}, {{member1}}, {{member2}}, +1 other member",
+ "conversation_title_more": "{{member0}}, {{member1}}, {{member2}}, +{{excess}} other members",
"conversation_add_contact": "Add contact",
"loading": "Loading...",
"calling": "Calling {{member0}}",
@@ -115,6 +116,9 @@
"setup_login_password_placeholder_creation": "New password",
"setup_login_password_placeholder_repeat": "Repeat password",
"admin_creation_submit_button": "Create admin account",
+ "conversation_block": "Block conversation",
+ "conversation_ask_confirm_block": "Would you really like to block this conversation?",
+ "conversation_confirm_block": "Block",
"share_screen": "Share your screen",
"share_window": "Share window",
"share_screen_area": "Share an area of your screen",
diff --git a/client/src/locale/fr/translation.json b/client/src/locale/fr/translation.json
index 44d0517..9b8ad6d 100644
--- a/client/src/locale/fr/translation.json
+++ b/client/src/locale/fr/translation.json
@@ -1,6 +1,7 @@
{
"severity": "",
"change_picture": "Modifier l'image",
+ "message_swarm_created": "Le Swarm a été créé",
"ongoing_call_unmuted": "Appel en cours",
"ongoing_call_muted": "Appel en cours (muet)",
"connecting_call": "Connexion...",
@@ -11,7 +12,6 @@
"conversation_start_videocall": "Démarrer appel vidéo",
"conversation_close": "Fermer la conversation",
"conversation_details": "Détails de la conversation",
- "conversation_block": "Bloquer la conversation",
"conversation_delete": "Supprimer la conversation",
"conversation_details_name": "Titre",
"conversation_details_identifier": "Identifiant",
@@ -21,15 +21,8 @@
"conversation_details_is_swarm_false": "Non",
"conversation_details_informations": "Informations",
"dialog_confirm_title_default": "Merci de confirmer",
- "conversation_ask_confirm_block": "Souhaitez-vous vraiment bloquer cette conversation ?",
- "conversation_confirm_block": "Bloquer",
"conversation_ask_confirm_remove": "Souhaitez-vous vraiment supprimer cette conversation ?",
"conversation_confirm_remove": "Supprimer",
- "conversation_title_one": "{{member0}}",
- "conversation_title_two": "{{member0}} et {{member1}}",
- "conversation_title_three": "{{member0}}, {{member1}} et {{member2}}",
- "conversation_title_four": "{{member0}}, {{member1}}, {{member2}}, +1 autre membre",
- "conversation_title_more": "{{member0}}, {{member1}}, {{member2}}, +{{excess}} autres membres",
"select_placeholder": "Sélectionner une option",
"dialog_close": "Fermer",
"dialog_cancel": "Annuler",
@@ -62,12 +55,6 @@
"username_rule_three": "Le nom d'utilisateur peut contenir des tirets (-).",
"username_rule_four": "Le nom d'utilisateur peut contenir des tirets bas (_).",
"welcome_text": "Bienvenue sur Jami!",
- "message_call_outgoing_missed": "Appel sortant manqué",
- "message_call_incoming_missed": "Appel entrant manqué",
- "message_call_outgoing": "Appel entrant - {{duration}}",
- "message_call_incoming": "Appel sortant - {{duration}}",
- "message_swarm_created": "Le Swarm a été créé",
- "message_user_joined": "{{user}} s'est joint",
"messages_scroll_to_end": "Faire défiler jusqu'à la fin de la conversation",
"conversation_add_contact_form": "Ajouter un contact",
"message_input_placeholder_one": "Écrire à {{member0}}",
@@ -76,6 +63,11 @@
"message_input_placeholder_four": "Écrire à {{member0}}, {{member1}}, {{member2}}, +1 autre membre",
"message_input_placeholder_more": "Écrire à {{member0}}, {{member1}}, {{member2}}, +{{excess}} autres membres",
"missed_incoming_call": "Appel manqué de la conversation {{conversationId}}",
+ "conversation_title_one": "{{member0}}",
+ "conversation_title_two": "{{member0}} et {{member1}}",
+ "conversation_title_three": "{{member0}}, {{member1}} et {{member2}}",
+ "conversation_title_four": "{{member0}}, {{member1}}, {{member2}}, +1 autre membre",
+ "conversation_title_more": "{{member0}}, {{member1}}, {{member2}}, +{{excess}} autres membres",
"conversation_add_contact": "Ajouter le contact",
"loading": "Chargement...",
"calling": "Appel vers {{member0}}",
@@ -115,6 +107,18 @@
"setup_login_password_placeholder_creation": "Nouveau mot de passe",
"setup_login_password_placeholder_repeat": "Répéter le mot de passe",
"admin_creation_submit_button": "Créer un compte admin",
+ "message_call_outgoing_missed": "Appel sortant manqué",
+ "message_call_incoming_missed": "Appel entrant manqué",
+ "message_call_outgoing": "Appel entrant - {{duration}}",
+ "message_call_incoming": "Appel sortant - {{duration}}",
+ "message_member_invited": "{{user}} a été invité(e)",
+ "message_member_joined": "{{user}} s'est joint(e)",
+ "message_member_left": "{{user}} a quitté",
+ "message_member_banned": "{{user}} a été exclu(e)",
+ "message_member_unbanned": "{{user}} a été de nouveau rajouté(e)",
+ "conversation_block": "Bloquer la conversation",
+ "conversation_ask_confirm_block": "Souhaitez-vous vraiment bloquer cette conversation ?",
+ "conversation_confirm_block": "Bloquer",
"share_screen": "Partager votre écran",
"share_window": "Partager la fenêtre",
"share_screen_area": "Partager une partie de l'écran",
diff --git a/client/src/models/account.ts b/client/src/models/account.ts
index ae2299e..e616ae1 100644
--- a/client/src/models/account.ts
+++ b/client/src/models/account.ts
@@ -70,10 +70,10 @@
}
getDisplayName() {
- return this.details['Account.displayName'] ?? this.getDisplayUri();
+ return this.details['Account.displayName'] || this.getDisplayUri();
}
getDisplayNameNoFallback() {
- return this.details['Account.displayName'] ?? this.getRegisteredName();
+ return this.details['Account.displayName'] || this.getRegisteredName();
}
}
diff --git a/client/src/pages/CallInterface.tsx b/client/src/pages/CallInterface.tsx
index b535b25..2425b52 100644
--- a/client/src/pages/CallInterface.tsx
+++ b/client/src/pages/CallInterface.tsx
@@ -16,6 +16,8 @@
* <https://www.gnu.org/licenses/>.
*/
import { Box, Card, Grid, Stack, Typography } from '@mui/material';
+import dayjs from 'dayjs';
+import { Duration } from 'dayjs/plugin/duration';
import {
ComponentType,
Fragment,
@@ -49,6 +51,7 @@
import { CallStatus, useCallContext, VideoStatus } from '../contexts/CallProvider';
import { useConversationContext } from '../contexts/ConversationProvider';
import { useWebRtcContext } from '../contexts/WebRtcProvider';
+import { formatCallDuration } from '../utils/dates×';
import { VideoElementWithSinkId } from '../utils/utils';
import { CallPending } from './CallPending';
import CallPermissionDenied from './CallPermissionDenied';
@@ -151,38 +154,23 @@
);
};
-const formatElapsedSeconds = (elapsedSeconds: number): string => {
- const seconds = Math.floor(elapsedSeconds % 60);
- elapsedSeconds = Math.floor(elapsedSeconds / 60);
- const minutes = elapsedSeconds % 60;
- elapsedSeconds = Math.floor(elapsedSeconds / 60);
- const hours = elapsedSeconds % 24;
-
- const times: string[] = [];
- if (hours > 0) {
- times.push(hours.toString().padStart(2, '0'));
- }
- times.push(minutes.toString().padStart(2, '0'));
- times.push(seconds.toString().padStart(2, '0'));
-
- return times.join(':');
-};
-
const CallInterfaceInformation = () => {
const { callStartTime } = useCallContext();
const { conversationDisplayName } = useConversationContext();
- const [elapsedTime, setElapsedTime] = useState(callStartTime ? (Date.now() - callStartTime) / 1000 : 0);
+ const [elapsedTime, setElapsedTime] = useState<Duration>(
+ dayjs.duration(callStartTime ? Date.now() - callStartTime : 0)
+ );
useEffect(() => {
if (callStartTime) {
const interval = setInterval(() => {
- setElapsedTime((Date.now() - callStartTime) / 1000);
+ setElapsedTime(dayjs.duration(Date.now() - callStartTime));
}, 1000);
return () => clearInterval(interval);
}
}, [callStartTime]);
- const elapsedTimerString = formatElapsedSeconds(elapsedTime);
+ const elapsedTimerString = formatCallDuration(elapsedTime);
return (
<Stack direction="row" justifyContent="space-between" alignItems="center">
diff --git a/client/src/themes/Default.ts b/client/src/themes/Default.ts
index a3f1b86..a29c68d 100644
--- a/client/src/themes/Default.ts
+++ b/client/src/themes/Default.ts
@@ -392,6 +392,24 @@
},
},
},
+ MuiListItemButton: {
+ styleOverrides: {
+ root: {
+ '&.Mui-selected': {
+ backgroundColor: '#cccccc',
+ opacity: 1,
+ '&:hover': {
+ backgroundColor: '#cccccc',
+ opacity: 1,
+ },
+ },
+ '&:hover': {
+ backgroundColor: '#d5d5d5',
+ opacity: 1,
+ },
+ },
+ },
+ },
},
});
};
diff --git a/client/src/utils/chatmessages.ts b/client/src/utils/chatmessages.ts
new file mode 100644
index 0000000..53c6053
--- /dev/null
+++ b/client/src/utils/chatmessages.ts
@@ -0,0 +1,57 @@
+/*
+ * Copyright (C) 2022 Savoir-faire Linux Inc.
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation; either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public
+ * License along with this program. If not, see
+ * <https://www.gnu.org/licenses/>.
+ */
+import dayjs from 'dayjs';
+import { i18n } from 'i18next';
+import { Message } from 'jami-web-common';
+
+import { formatCallDuration } from './dates×';
+
+export const getMessageCallText = (isAccountMessage: boolean, message: Message, i18n: i18n) => {
+ const callDuration = dayjs.duration(parseInt(message?.duration || ''));
+ const formattedCallDuration = formatCallDuration(callDuration);
+ if (callDuration.asSeconds() === 0) {
+ if (isAccountMessage) {
+ return i18n.t('message_call_outgoing_missed');
+ } else {
+ return i18n.t('message_call_incoming_missed');
+ }
+ } else {
+ if (isAccountMessage) {
+ return i18n.t('message_call_outgoing', formattedCallDuration);
+ } else {
+ return i18n.t('message_call_incoming', formattedCallDuration);
+ }
+ }
+};
+
+export const getMessageMemberText = (message: Message, i18n: i18n) => {
+ switch (message.action) {
+ case 'add':
+ return i18n.t('message_member_invited', { user: message.author });
+ case 'join':
+ return i18n.t('message_member_joined', { user: message.author });
+ case 'remove':
+ return i18n.t('message_member_left', { user: message.author });
+ case 'ban':
+ return i18n.t('message_member_banned', { user: message.author });
+ case 'unban':
+ return i18n.t('message_member_unbanned', { user: message.author });
+ default:
+ console.error(`${getMessageMemberText.name} received an unexpected message action: ${message.action}`);
+ }
+};
diff --git a/client/src/utils/dates×.ts b/client/src/utils/dates×.ts
new file mode 100644
index 0000000..d95d1ac
--- /dev/null
+++ b/client/src/utils/dates×.ts
@@ -0,0 +1,40 @@
+/*
+ * Copyright (C) 2022 Savoir-faire Linux Inc.
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation; either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public
+ * License along with this program. If not, see
+ * <https://www.gnu.org/licenses/>.
+ */
+import dayjs, { Dayjs } from 'dayjs';
+import { Duration } from 'dayjs/plugin/duration';
+import { i18n } from 'i18next';
+
+export const formatTime = (time: Dayjs, i18n: i18n) => {
+ return dayjs(time).locale(i18n.language).format('LT');
+};
+
+export const formatRelativeDate = (time: Dayjs, i18n: i18n) => {
+ if (time.isToday()) {
+ return new Intl.RelativeTimeFormat(i18n.language, { numeric: 'auto' }).format(0, 'day');
+ } else if (time.isYesterday()) {
+ return new Intl.RelativeTimeFormat(i18n.language, { numeric: 'auto' }).format(-1, 'day');
+ } else {
+ return dayjs(time).locale(i18n.language).format('L');
+ }
+};
+
+export const formatCallDuration = (duration: Duration) => {
+ const minutes = Math.floor(duration.asMinutes()).toString().padStart(2, '0');
+ const seconds = duration.format('ss');
+ return `${minutes}:${seconds}`;
+};