blob: e4be263301187bfa40c9366b25f135a58eab2bd1 [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 */
idillonef9ab812022-11-18 13:46:24 -050018import { Box, ListItem, ListItemAvatar, ListItemText } from '@mui/material';
Misha Krieger-Raynauldcfa44302022-11-30 18:36:36 -050019import { ContactDetails } from 'jami-web-common';
simon575c9402022-10-25 16:21:40 -040020import { QRCodeCanvas } from 'qrcode.react';
simon21f7d9f2022-11-28 14:21:54 -050021import { useCallback, useContext, useMemo, useState } from 'react';
simon4e7445c2022-11-16 21:18:46 -050022import { useTranslation } from 'react-i18next';
simon21f7d9f2022-11-28 14:21:54 -050023import { useNavigate } from 'react-router-dom';
simon575c9402022-10-25 16:21:40 -040024
simon5da8ca62022-11-09 15:21:25 -050025import { useAuthContext } from '../contexts/AuthProvider';
simone35acc22022-12-02 16:51:12 -050026import { CallManagerContext } from '../contexts/CallManagerProvider';
Michelle Sepkap Simec050d9c2022-12-05 09:39:34 -050027import { CallStatus, useCallContext } from '../contexts/CallProvider';
simon09fe4822022-11-30 23:36:25 -050028import { useConversationContext } from '../contexts/ConversationProvider';
simon21f7d9f2022-11-28 14:21:54 -050029import { MessengerContext } from '../contexts/MessengerProvider';
Misha Krieger-Raynauldcfa44302022-11-30 18:36:36 -050030import { Conversation } from '../models/conversation';
simon575c9402022-10-25 16:21:40 -040031import { setRefreshFromSlice } from '../redux/appSlice';
32import { useAppDispatch } from '../redux/hooks';
idillonef9ab812022-11-18 13:46:24 -050033import ContextMenu, { ContextMenuHandler, useContextMenuHandler } from './ContextMenu';
simon575c9402022-10-25 16:21:40 -040034import ConversationAvatar from './ConversationAvatar';
idillonef9ab812022-11-18 13:46:24 -050035import { ConfirmationDialog, DialogContentList, InfosDialog, useDialogHandler } from './Dialog';
36import { PopoverListItemData } from './PopoverList';
simon5da8ca62022-11-09 15:21:25 -050037import {
38 AudioCallIcon,
39 BlockContactIcon,
40 CancelIcon,
41 ContactDetailsIcon,
42 MessageIcon,
43 RemoveContactIcon,
44 VideoCallIcon,
45} from './SvgIcon';
simon575c9402022-10-25 16:21:40 -040046
simon575c9402022-10-25 16:21:40 -040047type ConversationListItemProps = {
48 conversation: Conversation;
49};
50
51export default function ConversationListItem({ conversation }: ConversationListItemProps) {
simon09fe4822022-11-30 23:36:25 -050052 const conversationContext = useConversationContext(true);
53 const conversationId = conversationContext?.conversationId;
idillonef9ab812022-11-18 13:46:24 -050054 const contextMenuHandler = useContextMenuHandler();
simon21f7d9f2022-11-28 14:21:54 -050055 const { newContactId, setNewContactId } = useContext(MessengerContext);
Michelle Sepkap Simec050d9c2022-12-05 09:39:34 -050056 const callContext = useCallContext(true);
57 const { callData } = useContext(CallManagerContext);
58 const { t } = useTranslation();
simon575c9402022-10-25 16:21:40 -040059
simon09fe4822022-11-30 23:36:25 -050060 const pathId = conversationId ?? newContactId;
simon575c9402022-10-25 16:21:40 -040061 const isSelected = conversation.getDisplayUri() === pathId;
simon21f7d9f2022-11-28 14:21:54 -050062
simon575c9402022-10-25 16:21:40 -040063 const navigate = useNavigate();
Misha Krieger-Raynauldcfa44302022-11-30 18:36:36 -050064 const userId = conversation?.getFirstMember()?.contact.uri;
simonff1cb352022-11-24 15:15:26 -050065
simon21f7d9f2022-11-28 14:21:54 -050066 const onClick = useCallback(() => {
Misha Krieger-Raynauldcfa44302022-11-30 18:36:36 -050067 const newConversationId = conversation.id;
simon21f7d9f2022-11-28 14:21:54 -050068 if (newConversationId) {
69 navigate(`/conversation/${newConversationId}`);
70 } else {
71 setNewContactId(userId);
72 }
73 }, [navigate, conversation, userId, setNewContactId]);
74
Michelle Sepkap Simec050d9c2022-12-05 09:39:34 -050075 const getSecondaryText = () => {
76 const propsConversationId = conversation.id;
77
78 if (!propsConversationId) {
79 return '';
80 }
81
82 if (!callContext || !callData || callData.conversationId !== propsConversationId) {
83 return conversation.getDisplayUri();
84 }
85
86 if (callContext.callStatus === CallStatus.InCall) {
87 return callContext.isAudioOn ? t('ongoing_call_unmuted') : t('ongoing_call_muted');
88 }
89
90 if (callContext.callStatus === CallStatus.Connecting) {
91 return t('connecting_call');
92 }
93
94 return callContext.callRole === 'caller' ? t('outgoing_call') : t('incoming_call');
95 };
96
idillonef9ab812022-11-18 13:46:24 -050097 return (
98 <Box onContextMenu={contextMenuHandler.handleAnchorPosition}>
99 <ConversationMenu
100 userId={userId}
101 conversation={conversation}
simon21f7d9f2022-11-28 14:21:54 -0500102 onMessageClick={onClick}
idillonef9ab812022-11-18 13:46:24 -0500103 isSelected={isSelected}
104 contextMenuProps={contextMenuHandler.props}
105 />
simon21f7d9f2022-11-28 14:21:54 -0500106 <ListItem button alignItems="flex-start" selected={isSelected} onClick={onClick}>
idillonef9ab812022-11-18 13:46:24 -0500107 <ListItemAvatar>
108 <ConversationAvatar displayName={conversation.getDisplayNameNoFallback()} />
109 </ListItemAvatar>
Michelle Sepkap Simec050d9c2022-12-05 09:39:34 -0500110 <ListItemText primary={conversation.getDisplayName()} secondary={getSecondaryText()} />
idillonef9ab812022-11-18 13:46:24 -0500111 </ListItem>
112 </Box>
113 );
114}
115
116interface ConversationMenuProps {
117 userId: string;
118 conversation: Conversation;
simon21f7d9f2022-11-28 14:21:54 -0500119 onMessageClick: () => void;
idillonef9ab812022-11-18 13:46:24 -0500120 isSelected: boolean;
121 contextMenuProps: ContextMenuHandler['props'];
122}
123
simon21f7d9f2022-11-28 14:21:54 -0500124const ConversationMenu = ({
125 userId,
126 conversation,
127 onMessageClick,
128 isSelected,
129 contextMenuProps,
130}: ConversationMenuProps) => {
idillonef9ab812022-11-18 13:46:24 -0500131 const { t } = useTranslation();
132 const { axiosInstance } = useAuthContext();
simone35acc22022-12-02 16:51:12 -0500133 const { startCall } = useContext(CallManagerContext);
simon416d0792022-11-03 02:46:18 -0400134 const [isSwarm] = useState(true);
simon575c9402022-10-25 16:21:40 -0400135
idillonef9ab812022-11-18 13:46:24 -0500136 const detailsDialogHandler = useDialogHandler();
137 const blockContactDialogHandler = useDialogHandler();
138 const removeContactDialogHandler = useDialogHandler();
simon575c9402022-10-25 16:21:40 -0400139
idillonef9ab812022-11-18 13:46:24 -0500140 const navigate = useNavigate();
141
142 const getContactDetails = useCallback(async () => {
simon575c9402022-10-25 16:21:40 -0400143 const controller = new AbortController();
simon94fe53e2022-11-10 12:51:58 -0500144 try {
Misha Krieger-Raynauldcfa44302022-11-30 18:36:36 -0500145 const { data } = await axiosInstance.get<ContactDetails>(`/contacts/${userId}`, {
simon94fe53e2022-11-10 12:51:58 -0500146 signal: controller.signal,
147 });
148 console.log('CONTACT LIST - DETAILS: ', data);
149 } catch (e) {
150 console.log('ERROR GET CONTACT DETAILS: ', e);
151 }
idillonef9ab812022-11-18 13:46:24 -0500152 }, [axiosInstance, userId]);
simon575c9402022-10-25 16:21:40 -0400153
Misha Krieger-Raynauldcfa44302022-11-30 18:36:36 -0500154 const conversationId = conversation.id;
simonff1cb352022-11-24 15:15:26 -0500155
idillonef9ab812022-11-18 13:46:24 -0500156 const menuOptions: PopoverListItemData[] = useMemo(
157 () => [
158 {
159 label: t('conversation_message'),
160 Icon: MessageIcon,
simon21f7d9f2022-11-28 14:21:54 -0500161 onClick: onMessageClick,
idillonef9ab812022-11-18 13:46:24 -0500162 },
163 {
164 label: t('conversation_start_audiocall'),
165 Icon: AudioCallIcon,
166 onClick: () => {
simonff1cb352022-11-24 15:15:26 -0500167 if (conversationId) {
simone35acc22022-12-02 16:51:12 -0500168 startCall({
169 conversationId,
170 role: 'caller',
171 });
simonff1cb352022-11-24 15:15:26 -0500172 }
idillonef9ab812022-11-18 13:46:24 -0500173 },
174 },
175 {
176 label: t('conversation_start_videocall'),
177 Icon: VideoCallIcon,
178 onClick: () => {
simonff1cb352022-11-24 15:15:26 -0500179 if (conversationId) {
simone35acc22022-12-02 16:51:12 -0500180 startCall({
181 conversationId,
182 role: 'caller',
183 withVideoOn: true,
simonff1cb352022-11-24 15:15:26 -0500184 });
185 }
idillonef9ab812022-11-18 13:46:24 -0500186 },
187 },
188 ...(isSelected
189 ? [
190 {
191 label: t('conversation_close'),
192 Icon: CancelIcon,
193 onClick: () => {
194 navigate(`/`);
195 },
196 },
197 ]
198 : []),
199 {
200 label: t('conversation_details'),
201 Icon: ContactDetailsIcon,
202 onClick: () => {
203 detailsDialogHandler.openDialog();
204 getContactDetails();
205 },
206 },
207 {
208 label: t('conversation_block'),
209 Icon: BlockContactIcon,
210 onClick: () => {
211 blockContactDialogHandler.openDialog();
212 },
213 },
214 {
215 label: t('conversation_delete'),
216 Icon: RemoveContactIcon,
217 onClick: () => {
218 removeContactDialogHandler.openDialog();
219 },
220 },
221 ],
222 [
idillonef9ab812022-11-18 13:46:24 -0500223 navigate,
simon21f7d9f2022-11-28 14:21:54 -0500224 onMessageClick,
idillonef9ab812022-11-18 13:46:24 -0500225 isSelected,
226 getContactDetails,
227 detailsDialogHandler,
228 blockContactDialogHandler,
229 removeContactDialogHandler,
230 t,
simonff1cb352022-11-24 15:15:26 -0500231 startCall,
232 conversationId,
idillonef9ab812022-11-18 13:46:24 -0500233 ]
234 );
simon575c9402022-10-25 16:21:40 -0400235
idillonef9ab812022-11-18 13:46:24 -0500236 return (
237 <>
238 <ContextMenu {...contextMenuProps} items={menuOptions} />
239
240 <DetailsDialog {...detailsDialogHandler.props} userId={userId} conversation={conversation} isSwarm={isSwarm} />
241
242 <RemoveContactDialog {...removeContactDialogHandler.props} userId={userId} conversation={conversation} />
243
244 <BlockContactDialog {...blockContactDialogHandler.props} userId={userId} conversation={conversation} />
245 </>
246 );
247};
248
249interface DetailsDialogProps {
250 userId: string;
251 conversation: Conversation;
252 open: boolean;
253 onClose: () => void;
254 isSwarm: boolean;
255}
256
257const DetailsDialog = ({ userId, conversation, open, onClose, isSwarm }: DetailsDialogProps) => {
258 const { t } = useTranslation();
259 const items = useMemo(
260 () => [
261 {
262 label: t('conversation_details_username'),
263 value: conversation.getDisplayNameNoFallback(),
264 },
265 {
266 label: t('conversation_details_identifier'),
267 value: userId,
268 },
269 {
270 label: t('conversation_details_qr_code'),
271 value: <QRCodeCanvas size={80} value={`${userId}`} />,
272 },
273 {
274 label: t('conversation_details_is_swarm'),
275 value: isSwarm ? t('conversation_details_is_swarm_true') : t('conversation_details_is_swarm_false'),
276 },
277 ],
278 [userId, conversation, isSwarm, t]
279 );
280 return (
281 <InfosDialog
282 open={open}
283 onClose={onClose}
284 icon={
285 <ConversationAvatar
286 sx={{ width: 'inherit', height: 'inherit' }}
287 displayName={conversation.getDisplayNameNoFallback()}
288 />
289 }
290 title={conversation.getDisplayNameNoFallback() || ''}
291 content={<DialogContentList title={t('conversation_details_informations')} items={items} />}
292 />
293 );
294};
295
296interface BlockContactDialogProps {
297 userId: string;
298 conversation: Conversation;
299 open: boolean;
300 onClose: () => void;
301}
302
303const BlockContactDialog = ({ userId, open, onClose }: BlockContactDialogProps) => {
304 const { axiosInstance } = useAuthContext();
305 const { t } = useTranslation();
306 const dispatch = useAppDispatch();
307
308 const block = async () => {
simon575c9402022-10-25 16:21:40 -0400309 const controller = new AbortController();
simon94fe53e2022-11-10 12:51:58 -0500310 try {
idillonef9ab812022-11-18 13:46:24 -0500311 await axiosInstance.post(`/contacts/${userId}/block`, {
simon94fe53e2022-11-10 12:51:58 -0500312 signal: controller.signal,
simon575c9402022-10-25 16:21:40 -0400313 });
simon94fe53e2022-11-10 12:51:58 -0500314 dispatch(setRefreshFromSlice());
315 } catch (e) {
idillonef9ab812022-11-18 13:46:24 -0500316 console.error(`Error $block contact : `, e);
simon94fe53e2022-11-10 12:51:58 -0500317 dispatch(setRefreshFromSlice());
318 }
idillonef9ab812022-11-18 13:46:24 -0500319 onClose();
simon575c9402022-10-25 16:21:40 -0400320 };
321
simon575c9402022-10-25 16:21:40 -0400322 return (
idillonef9ab812022-11-18 13:46:24 -0500323 <ConfirmationDialog
324 open={open}
325 onClose={onClose}
326 title={t('dialog_confirm_title_default')}
327 content={t('conversation_ask_confirm_block')}
328 onConfirm={block}
329 confirmButtonText={t('conversation_confirm_block')}
330 />
simon575c9402022-10-25 16:21:40 -0400331 );
idillonef9ab812022-11-18 13:46:24 -0500332};
333
334interface RemoveContactDialogProps {
335 userId: string;
336 conversation: Conversation;
337 open: boolean;
338 onClose: () => void;
simon575c9402022-10-25 16:21:40 -0400339}
idillonef9ab812022-11-18 13:46:24 -0500340
341const RemoveContactDialog = ({ userId, open, onClose }: RemoveContactDialogProps) => {
342 const { axiosInstance } = useAuthContext();
343 const { t } = useTranslation();
344 const dispatch = useAppDispatch();
345
346 const remove = async () => {
347 const controller = new AbortController();
348 try {
simon21f7d9f2022-11-28 14:21:54 -0500349 await axiosInstance.delete(`/contacts/${userId}`, {
idillonef9ab812022-11-18 13:46:24 -0500350 signal: controller.signal,
351 });
352 dispatch(setRefreshFromSlice());
353 } catch (e) {
354 console.error(`Error removing contact : `, e);
355 dispatch(setRefreshFromSlice());
356 }
357 onClose();
358 };
359
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};