blob: f8e3d227b46366601ee62a6b098782f49df3e5c2 [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';
simon21f7d9f2022-11-28 14:21:54 -050022import { useCallback, useContext, 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';
simone35acc22022-12-02 16:51:12 -050027import { CallManagerContext } from '../contexts/CallManagerProvider';
Michelle Sepkap Simec050d9c2022-12-05 09:39:34 -050028import { CallStatus, useCallContext } from '../contexts/CallProvider';
idillon18283ac2023-01-07 12:06:42 -050029import { useConversationDisplayNameShort } from '../hooks/useConversationDisplayName';
idillon07d31cc2022-12-06 22:40:14 -050030import { useUrlParams } from '../hooks/useUrlParams';
idillon07d31cc2022-12-06 22:40:14 -050031import { ConversationRouteParams } from '../router';
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();
111 const callContext = useCallContext(true);
112 const { callData } = useContext(CallManagerContext);
113 const { t, i18n } = useTranslation();
114
idillon9e542ca2022-12-15 17:54:07 -0500115 const timeIndicator = useMemo(() => {
116 const message = conversationSummary.lastMessage;
117 const time = dayjs.unix(Number(message.timestamp));
118 if (time.isToday()) {
119 return formatTime(time, i18n);
120 } else {
121 return formatRelativeDate(time, i18n);
122 }
123 }, [conversationSummary, i18n]);
124
125 const lastMessageText = useMemo(() => {
idillon07d31cc2022-12-06 22:40:14 -0500126 if (!callContext || !callData || callData.conversationId !== conversationSummary.id) {
idillon9e542ca2022-12-15 17:54:07 -0500127 const message = conversationSummary.lastMessage;
128 switch (message.type) {
129 case 'initial': {
130 return t('message_swarm_created');
131 }
132 case 'application/data-transfer+json': {
133 return message.fileId;
134 }
135 case 'application/call-history+json': {
136 const isAccountMessage = message.author === account.getUri();
137 return getMessageCallText(isAccountMessage, message, i18n);
138 }
139 case 'member': {
140 return getMessageMemberText(message, i18n);
141 }
142 case 'text/plain': {
143 return message.body;
144 }
145 default: {
idillon8fef7db2023-01-11 11:03:18 -0500146 console.error(`${ConversationSummaryListItem.name} received an unexpected lastMessage type: ${message.type}`);
idillon9e542ca2022-12-15 17:54:07 -0500147 return '';
148 }
149 }
Michelle Sepkap Simec050d9c2022-12-05 09:39:34 -0500150 }
151
152 if (callContext.callStatus === CallStatus.InCall) {
153 return callContext.isAudioOn ? t('ongoing_call_unmuted') : t('ongoing_call_muted');
154 }
155
156 if (callContext.callStatus === CallStatus.Connecting) {
157 return t('connecting_call');
158 }
159
160 return callContext.callRole === 'caller' ? t('outgoing_call') : t('incoming_call');
idillon9e542ca2022-12-15 17:54:07 -0500161 }, [account, conversationSummary, callContext, callData, t, i18n]);
idillon07d31cc2022-12-06 22:40:14 -0500162
idillonef9ab812022-11-18 13:46:24 -0500163 return (
idillon847b4642022-12-29 14:28:38 -0500164 <Stack direction="row" spacing="5px">
165 <Typography variant="body2" fontWeight={isSelected ? 'bold' : 'normal'}>
166 {timeIndicator}
167 </Typography>
168 <Typography variant="body2">{lastMessageText}</Typography>
169 </Stack>
idillonef9ab812022-11-18 13:46:24 -0500170 );
idillon847b4642022-12-29 14:28:38 -0500171};
idillonef9ab812022-11-18 13:46:24 -0500172
173interface ConversationMenuProps {
idillon07d31cc2022-12-06 22:40:14 -0500174 conversationId: string;
175 conversationName: string;
simon21f7d9f2022-11-28 14:21:54 -0500176 onMessageClick: () => void;
idillonef9ab812022-11-18 13:46:24 -0500177 isSelected: boolean;
178 contextMenuProps: ContextMenuHandler['props'];
179}
180
simon21f7d9f2022-11-28 14:21:54 -0500181const ConversationMenu = ({
idillon07d31cc2022-12-06 22:40:14 -0500182 conversationId,
183 conversationName,
simon21f7d9f2022-11-28 14:21:54 -0500184 onMessageClick,
185 isSelected,
186 contextMenuProps,
187}: ConversationMenuProps) => {
idillonef9ab812022-11-18 13:46:24 -0500188 const { t } = useTranslation();
simone35acc22022-12-02 16:51:12 -0500189 const { startCall } = useContext(CallManagerContext);
simon416d0792022-11-03 02:46:18 -0400190 const [isSwarm] = useState(true);
simon575c9402022-10-25 16:21:40 -0400191
idillonef9ab812022-11-18 13:46:24 -0500192 const detailsDialogHandler = useDialogHandler();
idillon07d31cc2022-12-06 22:40:14 -0500193 const RemoveConversationDialogHandler = useDialogHandler();
simon575c9402022-10-25 16:21:40 -0400194
idillonef9ab812022-11-18 13:46:24 -0500195 const navigate = useNavigate();
196
idillonef9ab812022-11-18 13:46:24 -0500197 const menuOptions: PopoverListItemData[] = useMemo(
198 () => [
199 {
200 label: t('conversation_message'),
201 Icon: MessageIcon,
simon21f7d9f2022-11-28 14:21:54 -0500202 onClick: onMessageClick,
idillonef9ab812022-11-18 13:46:24 -0500203 },
204 {
205 label: t('conversation_start_audiocall'),
206 Icon: AudioCallIcon,
207 onClick: () => {
simonff1cb352022-11-24 15:15:26 -0500208 if (conversationId) {
simone35acc22022-12-02 16:51:12 -0500209 startCall({
210 conversationId,
211 role: 'caller',
212 });
simonff1cb352022-11-24 15:15:26 -0500213 }
idillonef9ab812022-11-18 13:46:24 -0500214 },
215 },
216 {
217 label: t('conversation_start_videocall'),
218 Icon: VideoCallIcon,
219 onClick: () => {
simonff1cb352022-11-24 15:15:26 -0500220 if (conversationId) {
simone35acc22022-12-02 16:51:12 -0500221 startCall({
222 conversationId,
223 role: 'caller',
224 withVideoOn: true,
simonff1cb352022-11-24 15:15:26 -0500225 });
226 }
idillonef9ab812022-11-18 13:46:24 -0500227 },
228 },
229 ...(isSelected
230 ? [
231 {
232 label: t('conversation_close'),
233 Icon: CancelIcon,
234 onClick: () => {
235 navigate(`/`);
236 },
237 },
238 ]
239 : []),
240 {
241 label: t('conversation_details'),
idillon07d31cc2022-12-06 22:40:14 -0500242 Icon: PersonIcon,
idillonef9ab812022-11-18 13:46:24 -0500243 onClick: () => {
244 detailsDialogHandler.openDialog();
idillonef9ab812022-11-18 13:46:24 -0500245 },
246 },
247 {
248 label: t('conversation_delete'),
idillon07d31cc2022-12-06 22:40:14 -0500249 Icon: CancelIcon,
idillonef9ab812022-11-18 13:46:24 -0500250 onClick: () => {
idillon07d31cc2022-12-06 22:40:14 -0500251 RemoveConversationDialogHandler.openDialog();
idillonef9ab812022-11-18 13:46:24 -0500252 },
253 },
254 ],
255 [
idillonef9ab812022-11-18 13:46:24 -0500256 navigate,
simon21f7d9f2022-11-28 14:21:54 -0500257 onMessageClick,
idillonef9ab812022-11-18 13:46:24 -0500258 isSelected,
idillonef9ab812022-11-18 13:46:24 -0500259 detailsDialogHandler,
idillon07d31cc2022-12-06 22:40:14 -0500260 RemoveConversationDialogHandler,
idillonef9ab812022-11-18 13:46:24 -0500261 t,
simonff1cb352022-11-24 15:15:26 -0500262 startCall,
263 conversationId,
idillonef9ab812022-11-18 13:46:24 -0500264 ]
265 );
simon575c9402022-10-25 16:21:40 -0400266
idillonef9ab812022-11-18 13:46:24 -0500267 return (
268 <>
269 <ContextMenu {...contextMenuProps} items={menuOptions} />
270
idillon07d31cc2022-12-06 22:40:14 -0500271 <DetailsDialog
272 {...detailsDialogHandler.props}
273 conversationId={conversationId}
274 conversationName={conversationName}
275 isSwarm={isSwarm}
276 />
idillonef9ab812022-11-18 13:46:24 -0500277
idillon18283ac2023-01-07 12:06:42 -0500278 <RemoveConversationDialog
279 {...RemoveConversationDialogHandler.props}
280 conversationId={conversationId}
281 isSelected={isSelected}
282 />
idillonef9ab812022-11-18 13:46:24 -0500283 </>
284 );
285};
286
287interface DetailsDialogProps {
idillon07d31cc2022-12-06 22:40:14 -0500288 conversationId: string;
289 conversationName: string;
idillonef9ab812022-11-18 13:46:24 -0500290 open: boolean;
291 onClose: () => void;
292 isSwarm: boolean;
293}
294
idillon07d31cc2022-12-06 22:40:14 -0500295const DetailsDialog = ({ conversationId, conversationName, open, onClose, isSwarm }: DetailsDialogProps) => {
idillonef9ab812022-11-18 13:46:24 -0500296 const { t } = useTranslation();
297 const items = useMemo(
298 () => [
299 {
idillon07d31cc2022-12-06 22:40:14 -0500300 label: t('conversation_details_name'),
301 value: conversationName,
idillonef9ab812022-11-18 13:46:24 -0500302 },
303 {
304 label: t('conversation_details_identifier'),
idillon07d31cc2022-12-06 22:40:14 -0500305 value: conversationId,
idillonef9ab812022-11-18 13:46:24 -0500306 },
307 {
308 label: t('conversation_details_qr_code'),
idillon07d31cc2022-12-06 22:40:14 -0500309 value: <QRCodeCanvas size={80} value={`${conversationId}`} />,
idillonef9ab812022-11-18 13:46:24 -0500310 },
311 {
312 label: t('conversation_details_is_swarm'),
313 value: isSwarm ? t('conversation_details_is_swarm_true') : t('conversation_details_is_swarm_false'),
314 },
315 ],
idillon07d31cc2022-12-06 22:40:14 -0500316 [conversationId, conversationName, isSwarm, t]
idillonef9ab812022-11-18 13:46:24 -0500317 );
318 return (
319 <InfosDialog
320 open={open}
321 onClose={onClose}
idillon07d31cc2022-12-06 22:40:14 -0500322 icon={<ConversationAvatar sx={{ width: 'inherit', height: 'inherit' }} displayName={conversationName} />}
323 title={conversationName}
idillonef9ab812022-11-18 13:46:24 -0500324 content={<DialogContentList title={t('conversation_details_informations')} items={items} />}
325 />
326 );
327};
328
idillon07d31cc2022-12-06 22:40:14 -0500329interface RemoveConversationDialogProps {
330 conversationId: string;
idillon18283ac2023-01-07 12:06:42 -0500331 isSelected: boolean;
idillonef9ab812022-11-18 13:46:24 -0500332 open: boolean;
333 onClose: () => void;
334}
335
idillon18283ac2023-01-07 12:06:42 -0500336const RemoveConversationDialog = ({ conversationId, isSelected, open, onClose }: RemoveConversationDialogProps) => {
idillonef9ab812022-11-18 13:46:24 -0500337 const { t } = useTranslation();
idillon18283ac2023-01-07 12:06:42 -0500338 const navigate = useNavigate();
339 const removeConversationMutation = useRemoveConversationMutation();
idillonef9ab812022-11-18 13:46:24 -0500340
idillon18283ac2023-01-07 12:06:42 -0500341 const remove = useCallback(async () => {
342 removeConversationMutation.mutate(
343 { conversationId },
344 {
345 onSuccess: () => {
346 if (isSelected) {
347 navigate('/conversation/');
348 }
349 },
350 onError: (e) => {
351 console.error(`Error removing conversation : `, e);
352 },
353 onSettled: () => {
354 onClose();
355 },
356 }
357 );
358 }, [conversationId, isSelected, navigate, onClose, removeConversationMutation]);
idillonef9ab812022-11-18 13:46:24 -0500359
360 return (
361 <ConfirmationDialog
362 open={open}
363 onClose={onClose}
364 title={t('dialog_confirm_title_default')}
365 content={t('conversation_ask_confirm_remove')}
366 onConfirm={remove}
367 confirmButtonText={t('conversation_confirm_remove')}
368 />
369 );
370};