blob: 5db337ba4cb906f6b7ef958cff7ecf783e65f960 [file] [log] [blame]
simon575c9402022-10-25 16:21:40 -04001/*
2 * Copyright (C) 2022 Savoir-faire Linux Inc.
3 *
4 * This program is free software; you can redistribute it and/or modify
5 * it under the terms of the GNU Affero General Public License as
6 * published by the Free Software Foundation; either version 3 of the
7 * License, or (at your option) any later version.
8 *
9 * This program is distributed in the hope that it will be useful,
10 * but WITHOUT ANY WARRANTY; without even the implied warranty of
11 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 * GNU Affero General Public License for more details.
13 *
14 * You should have received a copy of the GNU Affero General Public
15 * License along with this program. If not, see
16 * <https://www.gnu.org/licenses/>.
17 */
idillon8fef7db2023-01-11 11:03:18 -050018import { Box, List, Stack, Typography } from '@mui/material';
idillon9e542ca2022-12-15 17:54:07 -050019import dayjs from 'dayjs';
idillon07d31cc2022-12-06 22:40:14 -050020import { IConversationSummary } from 'jami-web-common';
simon575c9402022-10-25 16:21:40 -040021import { QRCodeCanvas } from 'qrcode.react';
idillon255682e2023-02-06 13:25:26 -050022import { useCallback, useMemo, useState } from 'react';
simon4e7445c2022-11-16 21:18:46 -050023import { useTranslation } from 'react-i18next';
simon21f7d9f2022-11-28 14:21:54 -050024import { useNavigate } from 'react-router-dom';
simon575c9402022-10-25 16:21:40 -040025
simon5da8ca62022-11-09 15:21:25 -050026import { useAuthContext } from '../contexts/AuthProvider';
idillon255682e2023-02-06 13:25:26 -050027import { useCallManagerContext } from '../contexts/CallManagerProvider';
idillon18283ac2023-01-07 12:06:42 -050028import { useConversationDisplayNameShort } from '../hooks/useConversationDisplayName';
idillon07d31cc2022-12-06 22:40:14 -050029import { useUrlParams } from '../hooks/useUrlParams';
idillon07d31cc2022-12-06 22:40:14 -050030import { ConversationRouteParams } from '../router';
idillonc45a43d2023-02-10 18:12:10 -050031import { CallStatus } from '../services/CallManager';
idillon18283ac2023-01-07 12:06:42 -050032import { useRemoveConversationMutation } from '../services/conversationQueries';
idillon9e542ca2022-12-15 17:54:07 -050033import { getMessageCallText, getMessageMemberText } from '../utils/chatmessages';
34import { formatRelativeDate, formatTime } from '../utils/dates&times';
idillonef9ab812022-11-18 13:46:24 -050035import ContextMenu, { ContextMenuHandler, useContextMenuHandler } from './ContextMenu';
simon575c9402022-10-25 16:21:40 -040036import ConversationAvatar from './ConversationAvatar';
idillon847b4642022-12-29 14:28:38 -050037import { CustomListItemButton } from './CustomListItemButton';
idillonef9ab812022-11-18 13:46:24 -050038import { ConfirmationDialog, DialogContentList, InfosDialog, useDialogHandler } from './Dialog';
39import { PopoverListItemData } from './PopoverList';
idillon07d31cc2022-12-06 22:40:14 -050040import { AudioCallIcon, CancelIcon, MessageIcon, PersonIcon, VideoCallIcon } from './SvgIcon';
simon575c9402022-10-25 16:21:40 -040041
idillon8fef7db2023-01-11 11:03:18 -050042type ConversationSummaryListProps = {
43 conversationsSummaries: IConversationSummary[];
44};
45
46export const ConversationSummaryList = ({ conversationsSummaries }: ConversationSummaryListProps) => {
47 return (
48 <List>
49 {conversationsSummaries?.map((conversationSummary) => (
50 <ConversationSummaryListItem key={conversationSummary.id} conversationSummary={conversationSummary} />
51 ))}
52 </List>
53 );
54};
55
56type ConversationSummaryListItemProps = {
idillon07d31cc2022-12-06 22:40:14 -050057 conversationSummary: IConversationSummary;
simon575c9402022-10-25 16:21:40 -040058};
59
idillon8fef7db2023-01-11 11:03:18 -050060const ConversationSummaryListItem = ({ conversationSummary }: ConversationSummaryListItemProps) => {
idillon9e542ca2022-12-15 17:54:07 -050061 const { account } = useAuthContext();
idillon07d31cc2022-12-06 22:40:14 -050062 const {
63 urlParams: { conversationId: selectedConversationId },
64 } = useUrlParams<ConversationRouteParams>();
idillonef9ab812022-11-18 13:46:24 -050065 const contextMenuHandler = useContextMenuHandler();
simon575c9402022-10-25 16:21:40 -040066 const navigate = useNavigate();
idillon07d31cc2022-12-06 22:40:14 -050067
68 const conversationId = conversationSummary.id;
69 const isSelected = conversationId === selectedConversationId;
simonff1cb352022-11-24 15:15:26 -050070
simon21f7d9f2022-11-28 14:21:54 -050071 const onClick = useCallback(() => {
idillon07d31cc2022-12-06 22:40:14 -050072 if (conversationId) {
73 navigate(`/conversation/${conversationId}`);
simon21f7d9f2022-11-28 14:21:54 -050074 }
idillon07d31cc2022-12-06 22:40:14 -050075 }, [navigate, conversationId]);
simon21f7d9f2022-11-28 14:21:54 -050076
idillon18283ac2023-01-07 12:06:42 -050077 const conversationName = useConversationDisplayNameShort(
78 account,
79 conversationSummary.title,
80 conversationSummary.membersNames
idillon847b4642022-12-29 14:28:38 -050081 );
82
83 return (
84 <Box>
85 <ConversationMenu
86 conversationId={conversationId}
87 conversationName={conversationName}
88 onMessageClick={onClick}
89 isSelected={isSelected}
90 contextMenuProps={contextMenuHandler.props}
91 />
92 <CustomListItemButton
93 selected={isSelected}
94 onClick={onClick}
95 onContextMenu={contextMenuHandler.handleAnchorPosition}
idillon18283ac2023-01-07 12:06:42 -050096 icon={<ConversationAvatar displayName={conversationName} src={conversationSummary.avatar} />}
idillon847b4642022-12-29 14:28:38 -050097 primaryText={<Typography variant="body1">{conversationName}</Typography>}
98 secondaryText={<SecondaryText conversationSummary={conversationSummary} isSelected={isSelected} />}
99 />
100 </Box>
101 );
idillon8fef7db2023-01-11 11:03:18 -0500102};
idillon847b4642022-12-29 14:28:38 -0500103
104type SecondaryTextProps = {
105 conversationSummary: IConversationSummary;
106 isSelected: boolean;
107};
108
109const SecondaryText = ({ conversationSummary, isSelected }: SecondaryTextProps) => {
110 const { account } = useAuthContext();
idillon255682e2023-02-06 13:25:26 -0500111 const { callData, callStatus, isAudioOn } = useCallManagerContext();
idillon847b4642022-12-29 14:28:38 -0500112 const { t, i18n } = useTranslation();
113
idillon9e542ca2022-12-15 17:54:07 -0500114 const timeIndicator = useMemo(() => {
115 const message = conversationSummary.lastMessage;
116 const time = dayjs.unix(Number(message.timestamp));
117 if (time.isToday()) {
118 return formatTime(time, i18n);
119 } else {
120 return formatRelativeDate(time, i18n);
121 }
122 }, [conversationSummary, i18n]);
123
124 const lastMessageText = useMemo(() => {
idillon255682e2023-02-06 13:25:26 -0500125 if (!callData || callData.conversationId !== conversationSummary.id) {
idillon9e542ca2022-12-15 17:54:07 -0500126 const message = conversationSummary.lastMessage;
127 switch (message.type) {
128 case 'initial': {
129 return t('message_swarm_created');
130 }
131 case 'application/data-transfer+json': {
132 return message.fileId;
133 }
134 case 'application/call-history+json': {
135 const isAccountMessage = message.author === account.getUri();
136 return getMessageCallText(isAccountMessage, message, i18n);
137 }
138 case 'member': {
139 return getMessageMemberText(message, i18n);
140 }
141 case 'text/plain': {
142 return message.body;
143 }
144 default: {
idillon8fef7db2023-01-11 11:03:18 -0500145 console.error(`${ConversationSummaryListItem.name} received an unexpected lastMessage type: ${message.type}`);
idillon9e542ca2022-12-15 17:54:07 -0500146 return '';
147 }
148 }
Michelle Sepkap Simec050d9c2022-12-05 09:39:34 -0500149 }
150
idillon255682e2023-02-06 13:25:26 -0500151 if (callStatus === CallStatus.InCall) {
152 return isAudioOn ? t('ongoing_call_unmuted') : t('ongoing_call_muted');
Michelle Sepkap Simec050d9c2022-12-05 09:39:34 -0500153 }
154
idillon255682e2023-02-06 13:25:26 -0500155 if (callStatus === CallStatus.Connecting) {
Michelle Sepkap Simec050d9c2022-12-05 09:39:34 -0500156 return t('connecting_call');
157 }
158
idillon255682e2023-02-06 13:25:26 -0500159 return callData.role === 'caller' ? t('outgoing_call') : t('incoming_call');
160 }, [account, conversationSummary, callData, callStatus, isAudioOn, t, i18n]);
idillon07d31cc2022-12-06 22:40:14 -0500161
idillonef9ab812022-11-18 13:46:24 -0500162 return (
idillon847b4642022-12-29 14:28:38 -0500163 <Stack direction="row" spacing="5px">
164 <Typography variant="body2" fontWeight={isSelected ? 'bold' : 'normal'}>
165 {timeIndicator}
166 </Typography>
167 <Typography variant="body2">{lastMessageText}</Typography>
168 </Stack>
idillonef9ab812022-11-18 13:46:24 -0500169 );
idillon847b4642022-12-29 14:28:38 -0500170};
idillonef9ab812022-11-18 13:46:24 -0500171
172interface ConversationMenuProps {
idillon07d31cc2022-12-06 22:40:14 -0500173 conversationId: string;
174 conversationName: string;
simon21f7d9f2022-11-28 14:21:54 -0500175 onMessageClick: () => void;
idillonef9ab812022-11-18 13:46:24 -0500176 isSelected: boolean;
177 contextMenuProps: ContextMenuHandler['props'];
178}
179
simon21f7d9f2022-11-28 14:21:54 -0500180const ConversationMenu = ({
idillon07d31cc2022-12-06 22:40:14 -0500181 conversationId,
182 conversationName,
simon21f7d9f2022-11-28 14:21:54 -0500183 onMessageClick,
184 isSelected,
185 contextMenuProps,
186}: ConversationMenuProps) => {
idillonef9ab812022-11-18 13:46:24 -0500187 const { t } = useTranslation();
idillon255682e2023-02-06 13:25:26 -0500188 const { startCall } = useCallManagerContext();
simon416d0792022-11-03 02:46:18 -0400189 const [isSwarm] = useState(true);
simon575c9402022-10-25 16:21:40 -0400190
idillonef9ab812022-11-18 13:46:24 -0500191 const detailsDialogHandler = useDialogHandler();
idillon07d31cc2022-12-06 22:40:14 -0500192 const RemoveConversationDialogHandler = useDialogHandler();
simon575c9402022-10-25 16:21:40 -0400193
idillonef9ab812022-11-18 13:46:24 -0500194 const navigate = useNavigate();
195
idillonef9ab812022-11-18 13:46:24 -0500196 const menuOptions: PopoverListItemData[] = useMemo(
197 () => [
198 {
199 label: t('conversation_message'),
200 Icon: MessageIcon,
simon21f7d9f2022-11-28 14:21:54 -0500201 onClick: onMessageClick,
idillonef9ab812022-11-18 13:46:24 -0500202 },
203 {
204 label: t('conversation_start_audiocall'),
205 Icon: AudioCallIcon,
206 onClick: () => {
simonff1cb352022-11-24 15:15:26 -0500207 if (conversationId) {
idillon255682e2023-02-06 13:25:26 -0500208 startCall(conversationId);
simonff1cb352022-11-24 15:15:26 -0500209 }
idillonef9ab812022-11-18 13:46:24 -0500210 },
211 },
212 {
213 label: t('conversation_start_videocall'),
214 Icon: VideoCallIcon,
215 onClick: () => {
simonff1cb352022-11-24 15:15:26 -0500216 if (conversationId) {
idillon255682e2023-02-06 13:25:26 -0500217 startCall(conversationId, true);
simonff1cb352022-11-24 15:15:26 -0500218 }
idillonef9ab812022-11-18 13:46:24 -0500219 },
220 },
221 ...(isSelected
222 ? [
223 {
224 label: t('conversation_close'),
225 Icon: CancelIcon,
226 onClick: () => {
227 navigate(`/`);
228 },
229 },
230 ]
231 : []),
232 {
233 label: t('conversation_details'),
idillon07d31cc2022-12-06 22:40:14 -0500234 Icon: PersonIcon,
idillonef9ab812022-11-18 13:46:24 -0500235 onClick: () => {
236 detailsDialogHandler.openDialog();
idillonef9ab812022-11-18 13:46:24 -0500237 },
238 },
239 {
240 label: t('conversation_delete'),
idillon07d31cc2022-12-06 22:40:14 -0500241 Icon: CancelIcon,
idillonef9ab812022-11-18 13:46:24 -0500242 onClick: () => {
idillon07d31cc2022-12-06 22:40:14 -0500243 RemoveConversationDialogHandler.openDialog();
idillonef9ab812022-11-18 13:46:24 -0500244 },
245 },
246 ],
247 [
idillonef9ab812022-11-18 13:46:24 -0500248 navigate,
simon21f7d9f2022-11-28 14:21:54 -0500249 onMessageClick,
idillonef9ab812022-11-18 13:46:24 -0500250 isSelected,
idillonef9ab812022-11-18 13:46:24 -0500251 detailsDialogHandler,
idillon07d31cc2022-12-06 22:40:14 -0500252 RemoveConversationDialogHandler,
idillonef9ab812022-11-18 13:46:24 -0500253 t,
simonff1cb352022-11-24 15:15:26 -0500254 startCall,
255 conversationId,
idillonef9ab812022-11-18 13:46:24 -0500256 ]
257 );
simon575c9402022-10-25 16:21:40 -0400258
idillonef9ab812022-11-18 13:46:24 -0500259 return (
260 <>
261 <ContextMenu {...contextMenuProps} items={menuOptions} />
262
idillon07d31cc2022-12-06 22:40:14 -0500263 <DetailsDialog
264 {...detailsDialogHandler.props}
265 conversationId={conversationId}
266 conversationName={conversationName}
267 isSwarm={isSwarm}
268 />
idillonef9ab812022-11-18 13:46:24 -0500269
idillon18283ac2023-01-07 12:06:42 -0500270 <RemoveConversationDialog
271 {...RemoveConversationDialogHandler.props}
272 conversationId={conversationId}
273 isSelected={isSelected}
274 />
idillonef9ab812022-11-18 13:46:24 -0500275 </>
276 );
277};
278
279interface DetailsDialogProps {
idillon07d31cc2022-12-06 22:40:14 -0500280 conversationId: string;
281 conversationName: string;
idillonef9ab812022-11-18 13:46:24 -0500282 open: boolean;
283 onClose: () => void;
284 isSwarm: boolean;
285}
286
idillon07d31cc2022-12-06 22:40:14 -0500287const DetailsDialog = ({ conversationId, conversationName, open, onClose, isSwarm }: DetailsDialogProps) => {
idillonef9ab812022-11-18 13:46:24 -0500288 const { t } = useTranslation();
289 const items = useMemo(
290 () => [
291 {
idillon07d31cc2022-12-06 22:40:14 -0500292 label: t('conversation_details_name'),
293 value: conversationName,
idillonef9ab812022-11-18 13:46:24 -0500294 },
295 {
296 label: t('conversation_details_identifier'),
idillon07d31cc2022-12-06 22:40:14 -0500297 value: conversationId,
idillonef9ab812022-11-18 13:46:24 -0500298 },
299 {
300 label: t('conversation_details_qr_code'),
idillon07d31cc2022-12-06 22:40:14 -0500301 value: <QRCodeCanvas size={80} value={`${conversationId}`} />,
idillonef9ab812022-11-18 13:46:24 -0500302 },
303 {
304 label: t('conversation_details_is_swarm'),
305 value: isSwarm ? t('conversation_details_is_swarm_true') : t('conversation_details_is_swarm_false'),
306 },
307 ],
idillon07d31cc2022-12-06 22:40:14 -0500308 [conversationId, conversationName, isSwarm, t]
idillonef9ab812022-11-18 13:46:24 -0500309 );
310 return (
311 <InfosDialog
312 open={open}
313 onClose={onClose}
idillon07d31cc2022-12-06 22:40:14 -0500314 icon={<ConversationAvatar sx={{ width: 'inherit', height: 'inherit' }} displayName={conversationName} />}
315 title={conversationName}
idillonef9ab812022-11-18 13:46:24 -0500316 content={<DialogContentList title={t('conversation_details_informations')} items={items} />}
317 />
318 );
319};
320
idillon07d31cc2022-12-06 22:40:14 -0500321interface RemoveConversationDialogProps {
322 conversationId: string;
idillon18283ac2023-01-07 12:06:42 -0500323 isSelected: boolean;
idillonef9ab812022-11-18 13:46:24 -0500324 open: boolean;
325 onClose: () => void;
326}
327
idillon18283ac2023-01-07 12:06:42 -0500328const RemoveConversationDialog = ({ conversationId, isSelected, open, onClose }: RemoveConversationDialogProps) => {
idillonef9ab812022-11-18 13:46:24 -0500329 const { t } = useTranslation();
idillon18283ac2023-01-07 12:06:42 -0500330 const navigate = useNavigate();
331 const removeConversationMutation = useRemoveConversationMutation();
idillonef9ab812022-11-18 13:46:24 -0500332
idillon18283ac2023-01-07 12:06:42 -0500333 const remove = useCallback(async () => {
334 removeConversationMutation.mutate(
335 { conversationId },
336 {
337 onSuccess: () => {
338 if (isSelected) {
339 navigate('/conversation/');
340 }
341 },
342 onError: (e) => {
343 console.error(`Error removing conversation : `, e);
344 },
345 onSettled: () => {
346 onClose();
347 },
348 }
349 );
350 }, [conversationId, isSelected, navigate, onClose, removeConversationMutation]);
idillonef9ab812022-11-18 13:46:24 -0500351
352 return (
353 <ConfirmationDialog
354 open={open}
355 onClose={onClose}
356 title={t('dialog_confirm_title_default')}
357 content={t('conversation_ask_confirm_remove')}
358 onConfirm={remove}
359 confirmButtonText={t('conversation_confirm_remove')}
360 />
361 );
362};