blob: d94733d24e97e4b2e101d4b9d4c3dd784fc0d05a [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';
simon09fe4822022-11-30 23:36:25 -050026import { useConversationContext } from '../contexts/ConversationProvider';
simon21f7d9f2022-11-28 14:21:54 -050027import { MessengerContext } from '../contexts/MessengerProvider';
simonff1cb352022-11-24 15:15:26 -050028import { useStartCall } from '../hooks/useStartCall';
Misha Krieger-Raynauldcfa44302022-11-30 18:36:36 -050029import { Conversation } from '../models/conversation';
simon575c9402022-10-25 16:21:40 -040030import { setRefreshFromSlice } from '../redux/appSlice';
31import { useAppDispatch } from '../redux/hooks';
idillonef9ab812022-11-18 13:46:24 -050032import ContextMenu, { ContextMenuHandler, useContextMenuHandler } from './ContextMenu';
simon575c9402022-10-25 16:21:40 -040033import ConversationAvatar from './ConversationAvatar';
idillonef9ab812022-11-18 13:46:24 -050034import { ConfirmationDialog, DialogContentList, InfosDialog, useDialogHandler } from './Dialog';
35import { PopoverListItemData } from './PopoverList';
simon5da8ca62022-11-09 15:21:25 -050036import {
37 AudioCallIcon,
38 BlockContactIcon,
39 CancelIcon,
40 ContactDetailsIcon,
41 MessageIcon,
42 RemoveContactIcon,
43 VideoCallIcon,
44} from './SvgIcon';
simon575c9402022-10-25 16:21:40 -040045
simon575c9402022-10-25 16:21:40 -040046type ConversationListItemProps = {
47 conversation: Conversation;
48};
49
50export default function ConversationListItem({ conversation }: ConversationListItemProps) {
simon09fe4822022-11-30 23:36:25 -050051 const conversationContext = useConversationContext(true);
52 const conversationId = conversationContext?.conversationId;
idillonef9ab812022-11-18 13:46:24 -050053 const contextMenuHandler = useContextMenuHandler();
simon21f7d9f2022-11-28 14:21:54 -050054 const { newContactId, setNewContactId } = useContext(MessengerContext);
simon575c9402022-10-25 16:21:40 -040055
simon09fe4822022-11-30 23:36:25 -050056 const pathId = conversationId ?? newContactId;
simon575c9402022-10-25 16:21:40 -040057 const isSelected = conversation.getDisplayUri() === pathId;
simon21f7d9f2022-11-28 14:21:54 -050058
simon575c9402022-10-25 16:21:40 -040059 const navigate = useNavigate();
Misha Krieger-Raynauldcfa44302022-11-30 18:36:36 -050060 const userId = conversation?.getFirstMember()?.contact.uri;
simonff1cb352022-11-24 15:15:26 -050061
simon21f7d9f2022-11-28 14:21:54 -050062 const onClick = useCallback(() => {
Misha Krieger-Raynauldcfa44302022-11-30 18:36:36 -050063 const newConversationId = conversation.id;
simon21f7d9f2022-11-28 14:21:54 -050064 if (newConversationId) {
65 navigate(`/conversation/${newConversationId}`);
66 } else {
67 setNewContactId(userId);
68 }
69 }, [navigate, conversation, userId, setNewContactId]);
70
idillonef9ab812022-11-18 13:46:24 -050071 return (
72 <Box onContextMenu={contextMenuHandler.handleAnchorPosition}>
73 <ConversationMenu
74 userId={userId}
75 conversation={conversation}
simon21f7d9f2022-11-28 14:21:54 -050076 onMessageClick={onClick}
idillonef9ab812022-11-18 13:46:24 -050077 isSelected={isSelected}
78 contextMenuProps={contextMenuHandler.props}
79 />
simon21f7d9f2022-11-28 14:21:54 -050080 <ListItem button alignItems="flex-start" selected={isSelected} onClick={onClick}>
idillonef9ab812022-11-18 13:46:24 -050081 <ListItemAvatar>
82 <ConversationAvatar displayName={conversation.getDisplayNameNoFallback()} />
83 </ListItemAvatar>
84 <ListItemText primary={conversation.getDisplayName()} secondary={conversation.getDisplayUri()} />
85 </ListItem>
86 </Box>
87 );
88}
89
90interface ConversationMenuProps {
91 userId: string;
92 conversation: Conversation;
simon21f7d9f2022-11-28 14:21:54 -050093 onMessageClick: () => void;
idillonef9ab812022-11-18 13:46:24 -050094 isSelected: boolean;
95 contextMenuProps: ContextMenuHandler['props'];
96}
97
simon21f7d9f2022-11-28 14:21:54 -050098const ConversationMenu = ({
99 userId,
100 conversation,
101 onMessageClick,
102 isSelected,
103 contextMenuProps,
104}: ConversationMenuProps) => {
idillonef9ab812022-11-18 13:46:24 -0500105 const { t } = useTranslation();
106 const { axiosInstance } = useAuthContext();
simon416d0792022-11-03 02:46:18 -0400107 const [isSwarm] = useState(true);
simon575c9402022-10-25 16:21:40 -0400108
idillonef9ab812022-11-18 13:46:24 -0500109 const detailsDialogHandler = useDialogHandler();
110 const blockContactDialogHandler = useDialogHandler();
111 const removeContactDialogHandler = useDialogHandler();
simon575c9402022-10-25 16:21:40 -0400112
idillonef9ab812022-11-18 13:46:24 -0500113 const navigate = useNavigate();
114
simonff1cb352022-11-24 15:15:26 -0500115 const startCall = useStartCall();
116
idillonef9ab812022-11-18 13:46:24 -0500117 const getContactDetails = useCallback(async () => {
simon575c9402022-10-25 16:21:40 -0400118 const controller = new AbortController();
simon94fe53e2022-11-10 12:51:58 -0500119 try {
Misha Krieger-Raynauldcfa44302022-11-30 18:36:36 -0500120 const { data } = await axiosInstance.get<ContactDetails>(`/contacts/${userId}`, {
simon94fe53e2022-11-10 12:51:58 -0500121 signal: controller.signal,
122 });
123 console.log('CONTACT LIST - DETAILS: ', data);
124 } catch (e) {
125 console.log('ERROR GET CONTACT DETAILS: ', e);
126 }
idillonef9ab812022-11-18 13:46:24 -0500127 }, [axiosInstance, userId]);
simon575c9402022-10-25 16:21:40 -0400128
Misha Krieger-Raynauldcfa44302022-11-30 18:36:36 -0500129 const conversationId = conversation.id;
simonff1cb352022-11-24 15:15:26 -0500130
idillonef9ab812022-11-18 13:46:24 -0500131 const menuOptions: PopoverListItemData[] = useMemo(
132 () => [
133 {
134 label: t('conversation_message'),
135 Icon: MessageIcon,
simon21f7d9f2022-11-28 14:21:54 -0500136 onClick: onMessageClick,
idillonef9ab812022-11-18 13:46:24 -0500137 },
138 {
139 label: t('conversation_start_audiocall'),
140 Icon: AudioCallIcon,
141 onClick: () => {
simonff1cb352022-11-24 15:15:26 -0500142 if (conversationId) {
Charlie380dc5e2022-11-29 16:51:42 -0500143 startCall(conversationId);
simonff1cb352022-11-24 15:15:26 -0500144 }
idillonef9ab812022-11-18 13:46:24 -0500145 },
146 },
147 {
148 label: t('conversation_start_videocall'),
149 Icon: VideoCallIcon,
150 onClick: () => {
simonff1cb352022-11-24 15:15:26 -0500151 if (conversationId) {
Charlie380dc5e2022-11-29 16:51:42 -0500152 startCall(conversationId, {
simonff1cb352022-11-24 15:15:26 -0500153 isVideoOn: true,
154 });
155 }
idillonef9ab812022-11-18 13:46:24 -0500156 },
157 },
158 ...(isSelected
159 ? [
160 {
161 label: t('conversation_close'),
162 Icon: CancelIcon,
163 onClick: () => {
164 navigate(`/`);
165 },
166 },
167 ]
168 : []),
169 {
170 label: t('conversation_details'),
171 Icon: ContactDetailsIcon,
172 onClick: () => {
173 detailsDialogHandler.openDialog();
174 getContactDetails();
175 },
176 },
177 {
178 label: t('conversation_block'),
179 Icon: BlockContactIcon,
180 onClick: () => {
181 blockContactDialogHandler.openDialog();
182 },
183 },
184 {
185 label: t('conversation_delete'),
186 Icon: RemoveContactIcon,
187 onClick: () => {
188 removeContactDialogHandler.openDialog();
189 },
190 },
191 ],
192 [
idillonef9ab812022-11-18 13:46:24 -0500193 navigate,
simon21f7d9f2022-11-28 14:21:54 -0500194 onMessageClick,
idillonef9ab812022-11-18 13:46:24 -0500195 isSelected,
196 getContactDetails,
197 detailsDialogHandler,
198 blockContactDialogHandler,
199 removeContactDialogHandler,
200 t,
simonff1cb352022-11-24 15:15:26 -0500201 startCall,
202 conversationId,
idillonef9ab812022-11-18 13:46:24 -0500203 ]
204 );
simon575c9402022-10-25 16:21:40 -0400205
idillonef9ab812022-11-18 13:46:24 -0500206 return (
207 <>
208 <ContextMenu {...contextMenuProps} items={menuOptions} />
209
210 <DetailsDialog {...detailsDialogHandler.props} userId={userId} conversation={conversation} isSwarm={isSwarm} />
211
212 <RemoveContactDialog {...removeContactDialogHandler.props} userId={userId} conversation={conversation} />
213
214 <BlockContactDialog {...blockContactDialogHandler.props} userId={userId} conversation={conversation} />
215 </>
216 );
217};
218
219interface DetailsDialogProps {
220 userId: string;
221 conversation: Conversation;
222 open: boolean;
223 onClose: () => void;
224 isSwarm: boolean;
225}
226
227const DetailsDialog = ({ userId, conversation, open, onClose, isSwarm }: DetailsDialogProps) => {
228 const { t } = useTranslation();
229 const items = useMemo(
230 () => [
231 {
232 label: t('conversation_details_username'),
233 value: conversation.getDisplayNameNoFallback(),
234 },
235 {
236 label: t('conversation_details_identifier'),
237 value: userId,
238 },
239 {
240 label: t('conversation_details_qr_code'),
241 value: <QRCodeCanvas size={80} value={`${userId}`} />,
242 },
243 {
244 label: t('conversation_details_is_swarm'),
245 value: isSwarm ? t('conversation_details_is_swarm_true') : t('conversation_details_is_swarm_false'),
246 },
247 ],
248 [userId, conversation, isSwarm, t]
249 );
250 return (
251 <InfosDialog
252 open={open}
253 onClose={onClose}
254 icon={
255 <ConversationAvatar
256 sx={{ width: 'inherit', height: 'inherit' }}
257 displayName={conversation.getDisplayNameNoFallback()}
258 />
259 }
260 title={conversation.getDisplayNameNoFallback() || ''}
261 content={<DialogContentList title={t('conversation_details_informations')} items={items} />}
262 />
263 );
264};
265
266interface BlockContactDialogProps {
267 userId: string;
268 conversation: Conversation;
269 open: boolean;
270 onClose: () => void;
271}
272
273const BlockContactDialog = ({ userId, open, onClose }: BlockContactDialogProps) => {
274 const { axiosInstance } = useAuthContext();
275 const { t } = useTranslation();
276 const dispatch = useAppDispatch();
277
278 const block = async () => {
simon575c9402022-10-25 16:21:40 -0400279 const controller = new AbortController();
simon94fe53e2022-11-10 12:51:58 -0500280 try {
idillonef9ab812022-11-18 13:46:24 -0500281 await axiosInstance.post(`/contacts/${userId}/block`, {
simon94fe53e2022-11-10 12:51:58 -0500282 signal: controller.signal,
simon575c9402022-10-25 16:21:40 -0400283 });
simon94fe53e2022-11-10 12:51:58 -0500284 dispatch(setRefreshFromSlice());
285 } catch (e) {
idillonef9ab812022-11-18 13:46:24 -0500286 console.error(`Error $block contact : `, e);
simon94fe53e2022-11-10 12:51:58 -0500287 dispatch(setRefreshFromSlice());
288 }
idillonef9ab812022-11-18 13:46:24 -0500289 onClose();
simon575c9402022-10-25 16:21:40 -0400290 };
291
simon575c9402022-10-25 16:21:40 -0400292 return (
idillonef9ab812022-11-18 13:46:24 -0500293 <ConfirmationDialog
294 open={open}
295 onClose={onClose}
296 title={t('dialog_confirm_title_default')}
297 content={t('conversation_ask_confirm_block')}
298 onConfirm={block}
299 confirmButtonText={t('conversation_confirm_block')}
300 />
simon575c9402022-10-25 16:21:40 -0400301 );
idillonef9ab812022-11-18 13:46:24 -0500302};
303
304interface RemoveContactDialogProps {
305 userId: string;
306 conversation: Conversation;
307 open: boolean;
308 onClose: () => void;
simon575c9402022-10-25 16:21:40 -0400309}
idillonef9ab812022-11-18 13:46:24 -0500310
311const RemoveContactDialog = ({ userId, open, onClose }: RemoveContactDialogProps) => {
312 const { axiosInstance } = useAuthContext();
313 const { t } = useTranslation();
314 const dispatch = useAppDispatch();
315
316 const remove = async () => {
317 const controller = new AbortController();
318 try {
simon21f7d9f2022-11-28 14:21:54 -0500319 await axiosInstance.delete(`/contacts/${userId}`, {
idillonef9ab812022-11-18 13:46:24 -0500320 signal: controller.signal,
321 });
322 dispatch(setRefreshFromSlice());
323 } catch (e) {
324 console.error(`Error removing contact : `, e);
325 dispatch(setRefreshFromSlice());
326 }
327 onClose();
328 };
329
330 return (
331 <ConfirmationDialog
332 open={open}
333 onClose={onClose}
334 title={t('dialog_confirm_title_default')}
335 content={t('conversation_ask_confirm_remove')}
336 onConfirm={remove}
337 confirmButtonText={t('conversation_confirm_remove')}
338 />
339 );
340};