blob: 3f5dad3c0184f0f559ed760225fa6f955890f13e [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';
simon09fe4822022-11-30 23:36:25 -050027import { useConversationContext } from '../contexts/ConversationProvider';
simon21f7d9f2022-11-28 14:21:54 -050028import { MessengerContext } from '../contexts/MessengerProvider';
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();
simone35acc22022-12-02 16:51:12 -0500107 const { startCall } = useContext(CallManagerContext);
simon416d0792022-11-03 02:46:18 -0400108 const [isSwarm] = useState(true);
simon575c9402022-10-25 16:21:40 -0400109
idillonef9ab812022-11-18 13:46:24 -0500110 const detailsDialogHandler = useDialogHandler();
111 const blockContactDialogHandler = useDialogHandler();
112 const removeContactDialogHandler = useDialogHandler();
simon575c9402022-10-25 16:21:40 -0400113
idillonef9ab812022-11-18 13:46:24 -0500114 const navigate = useNavigate();
115
116 const getContactDetails = useCallback(async () => {
simon575c9402022-10-25 16:21:40 -0400117 const controller = new AbortController();
simon94fe53e2022-11-10 12:51:58 -0500118 try {
Misha Krieger-Raynauldcfa44302022-11-30 18:36:36 -0500119 const { data } = await axiosInstance.get<ContactDetails>(`/contacts/${userId}`, {
simon94fe53e2022-11-10 12:51:58 -0500120 signal: controller.signal,
121 });
122 console.log('CONTACT LIST - DETAILS: ', data);
123 } catch (e) {
124 console.log('ERROR GET CONTACT DETAILS: ', e);
125 }
idillonef9ab812022-11-18 13:46:24 -0500126 }, [axiosInstance, userId]);
simon575c9402022-10-25 16:21:40 -0400127
Misha Krieger-Raynauldcfa44302022-11-30 18:36:36 -0500128 const conversationId = conversation.id;
simonff1cb352022-11-24 15:15:26 -0500129
idillonef9ab812022-11-18 13:46:24 -0500130 const menuOptions: PopoverListItemData[] = useMemo(
131 () => [
132 {
133 label: t('conversation_message'),
134 Icon: MessageIcon,
simon21f7d9f2022-11-28 14:21:54 -0500135 onClick: onMessageClick,
idillonef9ab812022-11-18 13:46:24 -0500136 },
137 {
138 label: t('conversation_start_audiocall'),
139 Icon: AudioCallIcon,
140 onClick: () => {
simonff1cb352022-11-24 15:15:26 -0500141 if (conversationId) {
simone35acc22022-12-02 16:51:12 -0500142 startCall({
143 conversationId,
144 role: 'caller',
145 });
simonff1cb352022-11-24 15:15:26 -0500146 }
idillonef9ab812022-11-18 13:46:24 -0500147 },
148 },
149 {
150 label: t('conversation_start_videocall'),
151 Icon: VideoCallIcon,
152 onClick: () => {
simonff1cb352022-11-24 15:15:26 -0500153 if (conversationId) {
simone35acc22022-12-02 16:51:12 -0500154 startCall({
155 conversationId,
156 role: 'caller',
157 withVideoOn: true,
simonff1cb352022-11-24 15:15:26 -0500158 });
159 }
idillonef9ab812022-11-18 13:46:24 -0500160 },
161 },
162 ...(isSelected
163 ? [
164 {
165 label: t('conversation_close'),
166 Icon: CancelIcon,
167 onClick: () => {
168 navigate(`/`);
169 },
170 },
171 ]
172 : []),
173 {
174 label: t('conversation_details'),
175 Icon: ContactDetailsIcon,
176 onClick: () => {
177 detailsDialogHandler.openDialog();
178 getContactDetails();
179 },
180 },
181 {
182 label: t('conversation_block'),
183 Icon: BlockContactIcon,
184 onClick: () => {
185 blockContactDialogHandler.openDialog();
186 },
187 },
188 {
189 label: t('conversation_delete'),
190 Icon: RemoveContactIcon,
191 onClick: () => {
192 removeContactDialogHandler.openDialog();
193 },
194 },
195 ],
196 [
idillonef9ab812022-11-18 13:46:24 -0500197 navigate,
simon21f7d9f2022-11-28 14:21:54 -0500198 onMessageClick,
idillonef9ab812022-11-18 13:46:24 -0500199 isSelected,
200 getContactDetails,
201 detailsDialogHandler,
202 blockContactDialogHandler,
203 removeContactDialogHandler,
204 t,
simonff1cb352022-11-24 15:15:26 -0500205 startCall,
206 conversationId,
idillonef9ab812022-11-18 13:46:24 -0500207 ]
208 );
simon575c9402022-10-25 16:21:40 -0400209
idillonef9ab812022-11-18 13:46:24 -0500210 return (
211 <>
212 <ContextMenu {...contextMenuProps} items={menuOptions} />
213
214 <DetailsDialog {...detailsDialogHandler.props} userId={userId} conversation={conversation} isSwarm={isSwarm} />
215
216 <RemoveContactDialog {...removeContactDialogHandler.props} userId={userId} conversation={conversation} />
217
218 <BlockContactDialog {...blockContactDialogHandler.props} userId={userId} conversation={conversation} />
219 </>
220 );
221};
222
223interface DetailsDialogProps {
224 userId: string;
225 conversation: Conversation;
226 open: boolean;
227 onClose: () => void;
228 isSwarm: boolean;
229}
230
231const DetailsDialog = ({ userId, conversation, open, onClose, isSwarm }: DetailsDialogProps) => {
232 const { t } = useTranslation();
233 const items = useMemo(
234 () => [
235 {
236 label: t('conversation_details_username'),
237 value: conversation.getDisplayNameNoFallback(),
238 },
239 {
240 label: t('conversation_details_identifier'),
241 value: userId,
242 },
243 {
244 label: t('conversation_details_qr_code'),
245 value: <QRCodeCanvas size={80} value={`${userId}`} />,
246 },
247 {
248 label: t('conversation_details_is_swarm'),
249 value: isSwarm ? t('conversation_details_is_swarm_true') : t('conversation_details_is_swarm_false'),
250 },
251 ],
252 [userId, conversation, isSwarm, t]
253 );
254 return (
255 <InfosDialog
256 open={open}
257 onClose={onClose}
258 icon={
259 <ConversationAvatar
260 sx={{ width: 'inherit', height: 'inherit' }}
261 displayName={conversation.getDisplayNameNoFallback()}
262 />
263 }
264 title={conversation.getDisplayNameNoFallback() || ''}
265 content={<DialogContentList title={t('conversation_details_informations')} items={items} />}
266 />
267 );
268};
269
270interface BlockContactDialogProps {
271 userId: string;
272 conversation: Conversation;
273 open: boolean;
274 onClose: () => void;
275}
276
277const BlockContactDialog = ({ userId, open, onClose }: BlockContactDialogProps) => {
278 const { axiosInstance } = useAuthContext();
279 const { t } = useTranslation();
280 const dispatch = useAppDispatch();
281
282 const block = async () => {
simon575c9402022-10-25 16:21:40 -0400283 const controller = new AbortController();
simon94fe53e2022-11-10 12:51:58 -0500284 try {
idillonef9ab812022-11-18 13:46:24 -0500285 await axiosInstance.post(`/contacts/${userId}/block`, {
simon94fe53e2022-11-10 12:51:58 -0500286 signal: controller.signal,
simon575c9402022-10-25 16:21:40 -0400287 });
simon94fe53e2022-11-10 12:51:58 -0500288 dispatch(setRefreshFromSlice());
289 } catch (e) {
idillonef9ab812022-11-18 13:46:24 -0500290 console.error(`Error $block contact : `, e);
simon94fe53e2022-11-10 12:51:58 -0500291 dispatch(setRefreshFromSlice());
292 }
idillonef9ab812022-11-18 13:46:24 -0500293 onClose();
simon575c9402022-10-25 16:21:40 -0400294 };
295
simon575c9402022-10-25 16:21:40 -0400296 return (
idillonef9ab812022-11-18 13:46:24 -0500297 <ConfirmationDialog
298 open={open}
299 onClose={onClose}
300 title={t('dialog_confirm_title_default')}
301 content={t('conversation_ask_confirm_block')}
302 onConfirm={block}
303 confirmButtonText={t('conversation_confirm_block')}
304 />
simon575c9402022-10-25 16:21:40 -0400305 );
idillonef9ab812022-11-18 13:46:24 -0500306};
307
308interface RemoveContactDialogProps {
309 userId: string;
310 conversation: Conversation;
311 open: boolean;
312 onClose: () => void;
simon575c9402022-10-25 16:21:40 -0400313}
idillonef9ab812022-11-18 13:46:24 -0500314
315const RemoveContactDialog = ({ userId, open, onClose }: RemoveContactDialogProps) => {
316 const { axiosInstance } = useAuthContext();
317 const { t } = useTranslation();
318 const dispatch = useAppDispatch();
319
320 const remove = async () => {
321 const controller = new AbortController();
322 try {
simon21f7d9f2022-11-28 14:21:54 -0500323 await axiosInstance.delete(`/contacts/${userId}`, {
idillonef9ab812022-11-18 13:46:24 -0500324 signal: controller.signal,
325 });
326 dispatch(setRefreshFromSlice());
327 } catch (e) {
328 console.error(`Error removing contact : `, e);
329 dispatch(setRefreshFromSlice());
330 }
331 onClose();
332 };
333
334 return (
335 <ConfirmationDialog
336 open={open}
337 onClose={onClose}
338 title={t('dialog_confirm_title_default')}
339 content={t('conversation_ask_confirm_remove')}
340 onConfirm={remove}
341 confirmButtonText={t('conversation_confirm_remove')}
342 />
343 );
344};