blob: 985ee2c1ae86195a0561274424cd47411ab9c5d6 [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 */
idillon9e542ca2022-12-15 17:54:07 -050018import { Box, ListItemButton, Stack, Typography } from '@mui/material';
19import 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';
idillon07d31cc2022-12-06 22:40:14 -050029import { useUrlParams } from '../hooks/useUrlParams';
simon575c9402022-10-25 16:21:40 -040030import { setRefreshFromSlice } from '../redux/appSlice';
31import { useAppDispatch } from '../redux/hooks';
idillon07d31cc2022-12-06 22:40:14 -050032import { ConversationRouteParams } from '../router';
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';
idillonef9ab812022-11-18 13:46:24 -050037import { ConfirmationDialog, DialogContentList, InfosDialog, useDialogHandler } from './Dialog';
38import { PopoverListItemData } from './PopoverList';
idillon07d31cc2022-12-06 22:40:14 -050039import { AudioCallIcon, CancelIcon, MessageIcon, PersonIcon, VideoCallIcon } from './SvgIcon';
simon575c9402022-10-25 16:21:40 -040040
simon575c9402022-10-25 16:21:40 -040041type ConversationListItemProps = {
idillon07d31cc2022-12-06 22:40:14 -050042 conversationSummary: IConversationSummary;
simon575c9402022-10-25 16:21:40 -040043};
44
idillon07d31cc2022-12-06 22:40:14 -050045export default function ConversationListItem({ conversationSummary }: ConversationListItemProps) {
idillon9e542ca2022-12-15 17:54:07 -050046 const { account } = useAuthContext();
idillon07d31cc2022-12-06 22:40:14 -050047 const {
48 urlParams: { conversationId: selectedConversationId },
49 } = useUrlParams<ConversationRouteParams>();
idillonef9ab812022-11-18 13:46:24 -050050 const contextMenuHandler = useContextMenuHandler();
Michelle Sepkap Simec050d9c2022-12-05 09:39:34 -050051 const callContext = useCallContext(true);
52 const { callData } = useContext(CallManagerContext);
idillon9e542ca2022-12-15 17:54:07 -050053 const { t, i18n } = useTranslation();
simon575c9402022-10-25 16:21:40 -040054 const navigate = useNavigate();
idillon07d31cc2022-12-06 22:40:14 -050055
56 const conversationId = conversationSummary.id;
57 const isSelected = conversationId === selectedConversationId;
simonff1cb352022-11-24 15:15:26 -050058
simon21f7d9f2022-11-28 14:21:54 -050059 const onClick = useCallback(() => {
idillon07d31cc2022-12-06 22:40:14 -050060 if (conversationId) {
61 navigate(`/conversation/${conversationId}`);
simon21f7d9f2022-11-28 14:21:54 -050062 }
idillon07d31cc2022-12-06 22:40:14 -050063 }, [navigate, conversationId]);
simon21f7d9f2022-11-28 14:21:54 -050064
idillon9e542ca2022-12-15 17:54:07 -050065 const timeIndicator = useMemo(() => {
66 const message = conversationSummary.lastMessage;
67 const time = dayjs.unix(Number(message.timestamp));
68 if (time.isToday()) {
69 return formatTime(time, i18n);
70 } else {
71 return formatRelativeDate(time, i18n);
72 }
73 }, [conversationSummary, i18n]);
74
75 const lastMessageText = useMemo(() => {
idillon07d31cc2022-12-06 22:40:14 -050076 if (!callContext || !callData || callData.conversationId !== conversationSummary.id) {
idillon9e542ca2022-12-15 17:54:07 -050077 const message = conversationSummary.lastMessage;
78 switch (message.type) {
79 case 'initial': {
80 return t('message_swarm_created');
81 }
82 case 'application/data-transfer+json': {
83 return message.fileId;
84 }
85 case 'application/call-history+json': {
86 const isAccountMessage = message.author === account.getUri();
87 return getMessageCallText(isAccountMessage, message, i18n);
88 }
89 case 'member': {
90 return getMessageMemberText(message, i18n);
91 }
92 case 'text/plain': {
93 return message.body;
94 }
95 default: {
96 console.error(`${ConversationListItem.name} received an unexpected lastMessage type: ${message.type}`);
97 return '';
98 }
99 }
Michelle Sepkap Simec050d9c2022-12-05 09:39:34 -0500100 }
101
102 if (callContext.callStatus === CallStatus.InCall) {
103 return callContext.isAudioOn ? t('ongoing_call_unmuted') : t('ongoing_call_muted');
104 }
105
106 if (callContext.callStatus === CallStatus.Connecting) {
107 return t('connecting_call');
108 }
109
110 return callContext.callRole === 'caller' ? t('outgoing_call') : t('incoming_call');
idillon9e542ca2022-12-15 17:54:07 -0500111 }, [account, conversationSummary, callContext, callData, t, i18n]);
idillon07d31cc2022-12-06 22:40:14 -0500112
113 const conversationName = useMemo(
idillon9e542ca2022-12-15 17:54:07 -0500114 () => conversationSummary.title || conversationSummary.membersNames.join(', ') || account.getDisplayName(),
115 [account, conversationSummary]
idillon07d31cc2022-12-06 22:40:14 -0500116 );
Michelle Sepkap Simec050d9c2022-12-05 09:39:34 -0500117
idillonef9ab812022-11-18 13:46:24 -0500118 return (
idillon22ed8192022-11-29 12:17:04 -0500119 <Box>
idillonef9ab812022-11-18 13:46:24 -0500120 <ConversationMenu
idillon07d31cc2022-12-06 22:40:14 -0500121 conversationId={conversationId}
122 conversationName={conversationName}
simon21f7d9f2022-11-28 14:21:54 -0500123 onMessageClick={onClick}
idillonef9ab812022-11-18 13:46:24 -0500124 isSelected={isSelected}
125 contextMenuProps={contextMenuHandler.props}
126 />
idillon9e542ca2022-12-15 17:54:07 -0500127 <ListItemButton alignItems="flex-start" selected={isSelected} onClick={onClick}>
128 <Stack direction="row" spacing="10px">
idillon07d31cc2022-12-06 22:40:14 -0500129 <ConversationAvatar displayName={conversationName} />
idillon9e542ca2022-12-15 17:54:07 -0500130 <Stack>
131 <Typography variant="body1">{conversationName}</Typography>
132 <Stack direction="row" spacing="5px">
133 <Typography variant="body2" fontWeight={isSelected ? 'bold' : 'normal'}>
134 {timeIndicator}
135 </Typography>
136 <Typography variant="body2">{lastMessageText}</Typography>
137 </Stack>
138 </Stack>
139 </Stack>
140 </ListItemButton>
idillonef9ab812022-11-18 13:46:24 -0500141 </Box>
142 );
143}
144
145interface ConversationMenuProps {
idillon07d31cc2022-12-06 22:40:14 -0500146 conversationId: string;
147 conversationName: string;
simon21f7d9f2022-11-28 14:21:54 -0500148 onMessageClick: () => void;
idillonef9ab812022-11-18 13:46:24 -0500149 isSelected: boolean;
150 contextMenuProps: ContextMenuHandler['props'];
151}
152
simon21f7d9f2022-11-28 14:21:54 -0500153const ConversationMenu = ({
idillon07d31cc2022-12-06 22:40:14 -0500154 conversationId,
155 conversationName,
simon21f7d9f2022-11-28 14:21:54 -0500156 onMessageClick,
157 isSelected,
158 contextMenuProps,
159}: ConversationMenuProps) => {
idillonef9ab812022-11-18 13:46:24 -0500160 const { t } = useTranslation();
simone35acc22022-12-02 16:51:12 -0500161 const { startCall } = useContext(CallManagerContext);
simon416d0792022-11-03 02:46:18 -0400162 const [isSwarm] = useState(true);
simon575c9402022-10-25 16:21:40 -0400163
idillonef9ab812022-11-18 13:46:24 -0500164 const detailsDialogHandler = useDialogHandler();
idillon07d31cc2022-12-06 22:40:14 -0500165 const RemoveConversationDialogHandler = useDialogHandler();
simon575c9402022-10-25 16:21:40 -0400166
idillonef9ab812022-11-18 13:46:24 -0500167 const navigate = useNavigate();
168
idillonef9ab812022-11-18 13:46:24 -0500169 const menuOptions: PopoverListItemData[] = useMemo(
170 () => [
171 {
172 label: t('conversation_message'),
173 Icon: MessageIcon,
simon21f7d9f2022-11-28 14:21:54 -0500174 onClick: onMessageClick,
idillonef9ab812022-11-18 13:46:24 -0500175 },
176 {
177 label: t('conversation_start_audiocall'),
178 Icon: AudioCallIcon,
179 onClick: () => {
simonff1cb352022-11-24 15:15:26 -0500180 if (conversationId) {
simone35acc22022-12-02 16:51:12 -0500181 startCall({
182 conversationId,
183 role: 'caller',
184 });
simonff1cb352022-11-24 15:15:26 -0500185 }
idillonef9ab812022-11-18 13:46:24 -0500186 },
187 },
188 {
189 label: t('conversation_start_videocall'),
190 Icon: VideoCallIcon,
191 onClick: () => {
simonff1cb352022-11-24 15:15:26 -0500192 if (conversationId) {
simone35acc22022-12-02 16:51:12 -0500193 startCall({
194 conversationId,
195 role: 'caller',
196 withVideoOn: true,
simonff1cb352022-11-24 15:15:26 -0500197 });
198 }
idillonef9ab812022-11-18 13:46:24 -0500199 },
200 },
201 ...(isSelected
202 ? [
203 {
204 label: t('conversation_close'),
205 Icon: CancelIcon,
206 onClick: () => {
207 navigate(`/`);
208 },
209 },
210 ]
211 : []),
212 {
213 label: t('conversation_details'),
idillon07d31cc2022-12-06 22:40:14 -0500214 Icon: PersonIcon,
idillonef9ab812022-11-18 13:46:24 -0500215 onClick: () => {
216 detailsDialogHandler.openDialog();
idillonef9ab812022-11-18 13:46:24 -0500217 },
218 },
219 {
220 label: t('conversation_delete'),
idillon07d31cc2022-12-06 22:40:14 -0500221 Icon: CancelIcon,
idillonef9ab812022-11-18 13:46:24 -0500222 onClick: () => {
idillon07d31cc2022-12-06 22:40:14 -0500223 RemoveConversationDialogHandler.openDialog();
idillonef9ab812022-11-18 13:46:24 -0500224 },
225 },
226 ],
227 [
idillonef9ab812022-11-18 13:46:24 -0500228 navigate,
simon21f7d9f2022-11-28 14:21:54 -0500229 onMessageClick,
idillonef9ab812022-11-18 13:46:24 -0500230 isSelected,
idillonef9ab812022-11-18 13:46:24 -0500231 detailsDialogHandler,
idillon07d31cc2022-12-06 22:40:14 -0500232 RemoveConversationDialogHandler,
idillonef9ab812022-11-18 13:46:24 -0500233 t,
simonff1cb352022-11-24 15:15:26 -0500234 startCall,
235 conversationId,
idillonef9ab812022-11-18 13:46:24 -0500236 ]
237 );
simon575c9402022-10-25 16:21:40 -0400238
idillonef9ab812022-11-18 13:46:24 -0500239 return (
240 <>
241 <ContextMenu {...contextMenuProps} items={menuOptions} />
242
idillon07d31cc2022-12-06 22:40:14 -0500243 <DetailsDialog
244 {...detailsDialogHandler.props}
245 conversationId={conversationId}
246 conversationName={conversationName}
247 isSwarm={isSwarm}
248 />
idillonef9ab812022-11-18 13:46:24 -0500249
idillon07d31cc2022-12-06 22:40:14 -0500250 <RemoveConversationDialog {...RemoveConversationDialogHandler.props} conversationId={conversationId} />
idillonef9ab812022-11-18 13:46:24 -0500251 </>
252 );
253};
254
255interface DetailsDialogProps {
idillon07d31cc2022-12-06 22:40:14 -0500256 conversationId: string;
257 conversationName: string;
idillonef9ab812022-11-18 13:46:24 -0500258 open: boolean;
259 onClose: () => void;
260 isSwarm: boolean;
261}
262
idillon07d31cc2022-12-06 22:40:14 -0500263const DetailsDialog = ({ conversationId, conversationName, open, onClose, isSwarm }: DetailsDialogProps) => {
idillonef9ab812022-11-18 13:46:24 -0500264 const { t } = useTranslation();
265 const items = useMemo(
266 () => [
267 {
idillon07d31cc2022-12-06 22:40:14 -0500268 label: t('conversation_details_name'),
269 value: conversationName,
idillonef9ab812022-11-18 13:46:24 -0500270 },
271 {
272 label: t('conversation_details_identifier'),
idillon07d31cc2022-12-06 22:40:14 -0500273 value: conversationId,
idillonef9ab812022-11-18 13:46:24 -0500274 },
275 {
276 label: t('conversation_details_qr_code'),
idillon07d31cc2022-12-06 22:40:14 -0500277 value: <QRCodeCanvas size={80} value={`${conversationId}`} />,
idillonef9ab812022-11-18 13:46:24 -0500278 },
279 {
280 label: t('conversation_details_is_swarm'),
281 value: isSwarm ? t('conversation_details_is_swarm_true') : t('conversation_details_is_swarm_false'),
282 },
283 ],
idillon07d31cc2022-12-06 22:40:14 -0500284 [conversationId, conversationName, isSwarm, t]
idillonef9ab812022-11-18 13:46:24 -0500285 );
286 return (
287 <InfosDialog
288 open={open}
289 onClose={onClose}
idillon07d31cc2022-12-06 22:40:14 -0500290 icon={<ConversationAvatar sx={{ width: 'inherit', height: 'inherit' }} displayName={conversationName} />}
291 title={conversationName}
idillonef9ab812022-11-18 13:46:24 -0500292 content={<DialogContentList title={t('conversation_details_informations')} items={items} />}
293 />
294 );
295};
296
idillon07d31cc2022-12-06 22:40:14 -0500297interface RemoveConversationDialogProps {
298 conversationId: string;
idillonef9ab812022-11-18 13:46:24 -0500299 open: boolean;
300 onClose: () => void;
301}
302
idillon07d31cc2022-12-06 22:40:14 -0500303const RemoveConversationDialog = ({ conversationId, open, onClose }: RemoveConversationDialogProps) => {
idillonef9ab812022-11-18 13:46:24 -0500304 const { axiosInstance } = useAuthContext();
305 const { t } = useTranslation();
306 const dispatch = useAppDispatch();
307
308 const remove = async () => {
309 const controller = new AbortController();
310 try {
idillon07d31cc2022-12-06 22:40:14 -0500311 await axiosInstance.delete(`/conversations/${conversationId}`, {
idillonef9ab812022-11-18 13:46:24 -0500312 signal: controller.signal,
313 });
314 dispatch(setRefreshFromSlice());
315 } catch (e) {
idillon07d31cc2022-12-06 22:40:14 -0500316 console.error(`Error removing conversation : `, e);
idillonef9ab812022-11-18 13:46:24 -0500317 dispatch(setRefreshFromSlice());
318 }
319 onClose();
320 };
321
322 return (
323 <ConfirmationDialog
324 open={open}
325 onClose={onClose}
326 title={t('dialog_confirm_title_default')}
327 content={t('conversation_ask_confirm_remove')}
328 onConfirm={remove}
329 confirmButtonText={t('conversation_confirm_remove')}
330 />
331 );
332};