blob: e3803a2c8fa2099a392e0e66d0231adbe6969e20 [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 */
idillon847b4642022-12-29 14:28:38 -050018import { Box, 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
simon575c9402022-10-25 16:21:40 -040042type ConversationListItemProps = {
idillon07d31cc2022-12-06 22:40:14 -050043 conversationSummary: IConversationSummary;
simon575c9402022-10-25 16:21:40 -040044};
45
idillon07d31cc2022-12-06 22:40:14 -050046export default function ConversationListItem({ conversationSummary }: ConversationListItemProps) {
idillon9e542ca2022-12-15 17:54:07 -050047 const { account } = useAuthContext();
idillon07d31cc2022-12-06 22:40:14 -050048 const {
49 urlParams: { conversationId: selectedConversationId },
50 } = useUrlParams<ConversationRouteParams>();
idillonef9ab812022-11-18 13:46:24 -050051 const contextMenuHandler = useContextMenuHandler();
simon575c9402022-10-25 16:21:40 -040052 const navigate = useNavigate();
idillon07d31cc2022-12-06 22:40:14 -050053
54 const conversationId = conversationSummary.id;
55 const isSelected = conversationId === selectedConversationId;
simonff1cb352022-11-24 15:15:26 -050056
simon21f7d9f2022-11-28 14:21:54 -050057 const onClick = useCallback(() => {
idillon07d31cc2022-12-06 22:40:14 -050058 if (conversationId) {
59 navigate(`/conversation/${conversationId}`);
simon21f7d9f2022-11-28 14:21:54 -050060 }
idillon07d31cc2022-12-06 22:40:14 -050061 }, [navigate, conversationId]);
simon21f7d9f2022-11-28 14:21:54 -050062
idillon18283ac2023-01-07 12:06:42 -050063 const conversationName = useConversationDisplayNameShort(
64 account,
65 conversationSummary.title,
66 conversationSummary.membersNames
idillon847b4642022-12-29 14:28:38 -050067 );
68
69 return (
70 <Box>
71 <ConversationMenu
72 conversationId={conversationId}
73 conversationName={conversationName}
74 onMessageClick={onClick}
75 isSelected={isSelected}
76 contextMenuProps={contextMenuHandler.props}
77 />
78 <CustomListItemButton
79 selected={isSelected}
80 onClick={onClick}
81 onContextMenu={contextMenuHandler.handleAnchorPosition}
idillon18283ac2023-01-07 12:06:42 -050082 icon={<ConversationAvatar displayName={conversationName} src={conversationSummary.avatar} />}
idillon847b4642022-12-29 14:28:38 -050083 primaryText={<Typography variant="body1">{conversationName}</Typography>}
84 secondaryText={<SecondaryText conversationSummary={conversationSummary} isSelected={isSelected} />}
85 />
86 </Box>
87 );
88}
89
90type SecondaryTextProps = {
91 conversationSummary: IConversationSummary;
92 isSelected: boolean;
93};
94
95const SecondaryText = ({ conversationSummary, isSelected }: SecondaryTextProps) => {
96 const { account } = useAuthContext();
97 const callContext = useCallContext(true);
98 const { callData } = useContext(CallManagerContext);
99 const { t, i18n } = useTranslation();
100
idillon9e542ca2022-12-15 17:54:07 -0500101 const timeIndicator = useMemo(() => {
102 const message = conversationSummary.lastMessage;
103 const time = dayjs.unix(Number(message.timestamp));
104 if (time.isToday()) {
105 return formatTime(time, i18n);
106 } else {
107 return formatRelativeDate(time, i18n);
108 }
109 }, [conversationSummary, i18n]);
110
111 const lastMessageText = useMemo(() => {
idillon07d31cc2022-12-06 22:40:14 -0500112 if (!callContext || !callData || callData.conversationId !== conversationSummary.id) {
idillon9e542ca2022-12-15 17:54:07 -0500113 const message = conversationSummary.lastMessage;
114 switch (message.type) {
115 case 'initial': {
116 return t('message_swarm_created');
117 }
118 case 'application/data-transfer+json': {
119 return message.fileId;
120 }
121 case 'application/call-history+json': {
122 const isAccountMessage = message.author === account.getUri();
123 return getMessageCallText(isAccountMessage, message, i18n);
124 }
125 case 'member': {
126 return getMessageMemberText(message, i18n);
127 }
128 case 'text/plain': {
129 return message.body;
130 }
131 default: {
132 console.error(`${ConversationListItem.name} received an unexpected lastMessage type: ${message.type}`);
133 return '';
134 }
135 }
Michelle Sepkap Simec050d9c2022-12-05 09:39:34 -0500136 }
137
138 if (callContext.callStatus === CallStatus.InCall) {
139 return callContext.isAudioOn ? t('ongoing_call_unmuted') : t('ongoing_call_muted');
140 }
141
142 if (callContext.callStatus === CallStatus.Connecting) {
143 return t('connecting_call');
144 }
145
146 return callContext.callRole === 'caller' ? t('outgoing_call') : t('incoming_call');
idillon9e542ca2022-12-15 17:54:07 -0500147 }, [account, conversationSummary, callContext, callData, t, i18n]);
idillon07d31cc2022-12-06 22:40:14 -0500148
idillonef9ab812022-11-18 13:46:24 -0500149 return (
idillon847b4642022-12-29 14:28:38 -0500150 <Stack direction="row" spacing="5px">
151 <Typography variant="body2" fontWeight={isSelected ? 'bold' : 'normal'}>
152 {timeIndicator}
153 </Typography>
154 <Typography variant="body2">{lastMessageText}</Typography>
155 </Stack>
idillonef9ab812022-11-18 13:46:24 -0500156 );
idillon847b4642022-12-29 14:28:38 -0500157};
idillonef9ab812022-11-18 13:46:24 -0500158
159interface ConversationMenuProps {
idillon07d31cc2022-12-06 22:40:14 -0500160 conversationId: string;
161 conversationName: string;
simon21f7d9f2022-11-28 14:21:54 -0500162 onMessageClick: () => void;
idillonef9ab812022-11-18 13:46:24 -0500163 isSelected: boolean;
164 contextMenuProps: ContextMenuHandler['props'];
165}
166
simon21f7d9f2022-11-28 14:21:54 -0500167const ConversationMenu = ({
idillon07d31cc2022-12-06 22:40:14 -0500168 conversationId,
169 conversationName,
simon21f7d9f2022-11-28 14:21:54 -0500170 onMessageClick,
171 isSelected,
172 contextMenuProps,
173}: ConversationMenuProps) => {
idillonef9ab812022-11-18 13:46:24 -0500174 const { t } = useTranslation();
simone35acc22022-12-02 16:51:12 -0500175 const { startCall } = useContext(CallManagerContext);
simon416d0792022-11-03 02:46:18 -0400176 const [isSwarm] = useState(true);
simon575c9402022-10-25 16:21:40 -0400177
idillonef9ab812022-11-18 13:46:24 -0500178 const detailsDialogHandler = useDialogHandler();
idillon07d31cc2022-12-06 22:40:14 -0500179 const RemoveConversationDialogHandler = useDialogHandler();
simon575c9402022-10-25 16:21:40 -0400180
idillonef9ab812022-11-18 13:46:24 -0500181 const navigate = useNavigate();
182
idillonef9ab812022-11-18 13:46:24 -0500183 const menuOptions: PopoverListItemData[] = useMemo(
184 () => [
185 {
186 label: t('conversation_message'),
187 Icon: MessageIcon,
simon21f7d9f2022-11-28 14:21:54 -0500188 onClick: onMessageClick,
idillonef9ab812022-11-18 13:46:24 -0500189 },
190 {
191 label: t('conversation_start_audiocall'),
192 Icon: AudioCallIcon,
193 onClick: () => {
simonff1cb352022-11-24 15:15:26 -0500194 if (conversationId) {
simone35acc22022-12-02 16:51:12 -0500195 startCall({
196 conversationId,
197 role: 'caller',
198 });
simonff1cb352022-11-24 15:15:26 -0500199 }
idillonef9ab812022-11-18 13:46:24 -0500200 },
201 },
202 {
203 label: t('conversation_start_videocall'),
204 Icon: VideoCallIcon,
205 onClick: () => {
simonff1cb352022-11-24 15:15:26 -0500206 if (conversationId) {
simone35acc22022-12-02 16:51:12 -0500207 startCall({
208 conversationId,
209 role: 'caller',
210 withVideoOn: true,
simonff1cb352022-11-24 15:15:26 -0500211 });
212 }
idillonef9ab812022-11-18 13:46:24 -0500213 },
214 },
215 ...(isSelected
216 ? [
217 {
218 label: t('conversation_close'),
219 Icon: CancelIcon,
220 onClick: () => {
221 navigate(`/`);
222 },
223 },
224 ]
225 : []),
226 {
227 label: t('conversation_details'),
idillon07d31cc2022-12-06 22:40:14 -0500228 Icon: PersonIcon,
idillonef9ab812022-11-18 13:46:24 -0500229 onClick: () => {
230 detailsDialogHandler.openDialog();
idillonef9ab812022-11-18 13:46:24 -0500231 },
232 },
233 {
234 label: t('conversation_delete'),
idillon07d31cc2022-12-06 22:40:14 -0500235 Icon: CancelIcon,
idillonef9ab812022-11-18 13:46:24 -0500236 onClick: () => {
idillon07d31cc2022-12-06 22:40:14 -0500237 RemoveConversationDialogHandler.openDialog();
idillonef9ab812022-11-18 13:46:24 -0500238 },
239 },
240 ],
241 [
idillonef9ab812022-11-18 13:46:24 -0500242 navigate,
simon21f7d9f2022-11-28 14:21:54 -0500243 onMessageClick,
idillonef9ab812022-11-18 13:46:24 -0500244 isSelected,
idillonef9ab812022-11-18 13:46:24 -0500245 detailsDialogHandler,
idillon07d31cc2022-12-06 22:40:14 -0500246 RemoveConversationDialogHandler,
idillonef9ab812022-11-18 13:46:24 -0500247 t,
simonff1cb352022-11-24 15:15:26 -0500248 startCall,
249 conversationId,
idillonef9ab812022-11-18 13:46:24 -0500250 ]
251 );
simon575c9402022-10-25 16:21:40 -0400252
idillonef9ab812022-11-18 13:46:24 -0500253 return (
254 <>
255 <ContextMenu {...contextMenuProps} items={menuOptions} />
256
idillon07d31cc2022-12-06 22:40:14 -0500257 <DetailsDialog
258 {...detailsDialogHandler.props}
259 conversationId={conversationId}
260 conversationName={conversationName}
261 isSwarm={isSwarm}
262 />
idillonef9ab812022-11-18 13:46:24 -0500263
idillon18283ac2023-01-07 12:06:42 -0500264 <RemoveConversationDialog
265 {...RemoveConversationDialogHandler.props}
266 conversationId={conversationId}
267 isSelected={isSelected}
268 />
idillonef9ab812022-11-18 13:46:24 -0500269 </>
270 );
271};
272
273interface DetailsDialogProps {
idillon07d31cc2022-12-06 22:40:14 -0500274 conversationId: string;
275 conversationName: string;
idillonef9ab812022-11-18 13:46:24 -0500276 open: boolean;
277 onClose: () => void;
278 isSwarm: boolean;
279}
280
idillon07d31cc2022-12-06 22:40:14 -0500281const DetailsDialog = ({ conversationId, conversationName, open, onClose, isSwarm }: DetailsDialogProps) => {
idillonef9ab812022-11-18 13:46:24 -0500282 const { t } = useTranslation();
283 const items = useMemo(
284 () => [
285 {
idillon07d31cc2022-12-06 22:40:14 -0500286 label: t('conversation_details_name'),
287 value: conversationName,
idillonef9ab812022-11-18 13:46:24 -0500288 },
289 {
290 label: t('conversation_details_identifier'),
idillon07d31cc2022-12-06 22:40:14 -0500291 value: conversationId,
idillonef9ab812022-11-18 13:46:24 -0500292 },
293 {
294 label: t('conversation_details_qr_code'),
idillon07d31cc2022-12-06 22:40:14 -0500295 value: <QRCodeCanvas size={80} value={`${conversationId}`} />,
idillonef9ab812022-11-18 13:46:24 -0500296 },
297 {
298 label: t('conversation_details_is_swarm'),
299 value: isSwarm ? t('conversation_details_is_swarm_true') : t('conversation_details_is_swarm_false'),
300 },
301 ],
idillon07d31cc2022-12-06 22:40:14 -0500302 [conversationId, conversationName, isSwarm, t]
idillonef9ab812022-11-18 13:46:24 -0500303 );
304 return (
305 <InfosDialog
306 open={open}
307 onClose={onClose}
idillon07d31cc2022-12-06 22:40:14 -0500308 icon={<ConversationAvatar sx={{ width: 'inherit', height: 'inherit' }} displayName={conversationName} />}
309 title={conversationName}
idillonef9ab812022-11-18 13:46:24 -0500310 content={<DialogContentList title={t('conversation_details_informations')} items={items} />}
311 />
312 );
313};
314
idillon07d31cc2022-12-06 22:40:14 -0500315interface RemoveConversationDialogProps {
316 conversationId: string;
idillon18283ac2023-01-07 12:06:42 -0500317 isSelected: boolean;
idillonef9ab812022-11-18 13:46:24 -0500318 open: boolean;
319 onClose: () => void;
320}
321
idillon18283ac2023-01-07 12:06:42 -0500322const RemoveConversationDialog = ({ conversationId, isSelected, open, onClose }: RemoveConversationDialogProps) => {
idillonef9ab812022-11-18 13:46:24 -0500323 const { t } = useTranslation();
idillon18283ac2023-01-07 12:06:42 -0500324 const navigate = useNavigate();
325 const removeConversationMutation = useRemoveConversationMutation();
idillonef9ab812022-11-18 13:46:24 -0500326
idillon18283ac2023-01-07 12:06:42 -0500327 const remove = useCallback(async () => {
328 removeConversationMutation.mutate(
329 { conversationId },
330 {
331 onSuccess: () => {
332 if (isSelected) {
333 navigate('/conversation/');
334 }
335 },
336 onError: (e) => {
337 console.error(`Error removing conversation : `, e);
338 },
339 onSettled: () => {
340 onClose();
341 },
342 }
343 );
344 }, [conversationId, isSelected, navigate, onClose, removeConversationMutation]);
idillonef9ab812022-11-18 13:46:24 -0500345
346 return (
347 <ConfirmationDialog
348 open={open}
349 onClose={onClose}
350 title={t('dialog_confirm_title_default')}
351 content={t('conversation_ask_confirm_remove')}
352 onConfirm={remove}
353 confirmButtonText={t('conversation_confirm_remove')}
354 />
355 );
356};