blob: 03685c31774d6fb90a2b98a26f222b25d7b9be04 [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';
simon575c9402022-10-25 16:21:40 -040019import { Conversation } from 'jami-web-common';
20import { 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';
simon21f7d9f2022-11-28 14:21:54 -050026import { MessengerContext } from '../contexts/MessengerProvider';
simonff1cb352022-11-24 15:15:26 -050027import { useStartCall } from '../hooks/useStartCall';
simon21f7d9f2022-11-28 14:21:54 -050028import { useUrlParams } from '../hooks/useUrlParams';
simon575c9402022-10-25 16:21:40 -040029import { setRefreshFromSlice } from '../redux/appSlice';
30import { useAppDispatch } from '../redux/hooks';
simon21f7d9f2022-11-28 14:21:54 -050031import { ConversationRouteParams } from '../router';
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) {
simon21f7d9f2022-11-28 14:21:54 -050051 const {
52 urlParams: { conversationId },
53 } = useUrlParams<ConversationRouteParams>();
idillonef9ab812022-11-18 13:46:24 -050054 const contextMenuHandler = useContextMenuHandler();
simon21f7d9f2022-11-28 14:21:54 -050055 const { newContactId, setNewContactId } = useContext(MessengerContext);
simon575c9402022-10-25 16:21:40 -040056
simon21f7d9f2022-11-28 14:21:54 -050057 const pathId = conversationId || newContactId;
simon575c9402022-10-25 16:21:40 -040058 const isSelected = conversation.getDisplayUri() === pathId;
simon21f7d9f2022-11-28 14:21:54 -050059
simon575c9402022-10-25 16:21:40 -040060 const navigate = useNavigate();
Michelle Sepkap Simedd82cbf2022-11-17 23:31:49 -050061 const userId = conversation?.getFirstMember()?.contact.getUri();
simonff1cb352022-11-24 15:15:26 -050062
simon21f7d9f2022-11-28 14:21:54 -050063 const onClick = useCallback(() => {
64 const newConversationId = conversation.getId();
65 if (newConversationId) {
66 navigate(`/conversation/${newConversationId}`);
67 } else {
68 setNewContactId(userId);
69 }
70 }, [navigate, conversation, userId, setNewContactId]);
71
idillonef9ab812022-11-18 13:46:24 -050072 return (
73 <Box onContextMenu={contextMenuHandler.handleAnchorPosition}>
74 <ConversationMenu
75 userId={userId}
76 conversation={conversation}
simon21f7d9f2022-11-28 14:21:54 -050077 onMessageClick={onClick}
idillonef9ab812022-11-18 13:46:24 -050078 isSelected={isSelected}
79 contextMenuProps={contextMenuHandler.props}
80 />
simon21f7d9f2022-11-28 14:21:54 -050081 <ListItem button alignItems="flex-start" selected={isSelected} onClick={onClick}>
idillonef9ab812022-11-18 13:46:24 -050082 <ListItemAvatar>
83 <ConversationAvatar displayName={conversation.getDisplayNameNoFallback()} />
84 </ListItemAvatar>
85 <ListItemText primary={conversation.getDisplayName()} secondary={conversation.getDisplayUri()} />
86 </ListItem>
87 </Box>
88 );
89}
90
91interface ConversationMenuProps {
92 userId: string;
93 conversation: Conversation;
simon21f7d9f2022-11-28 14:21:54 -050094 onMessageClick: () => void;
idillonef9ab812022-11-18 13:46:24 -050095 isSelected: boolean;
96 contextMenuProps: ContextMenuHandler['props'];
97}
98
simon21f7d9f2022-11-28 14:21:54 -050099const ConversationMenu = ({
100 userId,
101 conversation,
102 onMessageClick,
103 isSelected,
104 contextMenuProps,
105}: ConversationMenuProps) => {
idillonef9ab812022-11-18 13:46:24 -0500106 const { t } = useTranslation();
107 const { axiosInstance } = useAuthContext();
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
simonff1cb352022-11-24 15:15:26 -0500116 const startCall = useStartCall();
117
idillonef9ab812022-11-18 13:46:24 -0500118 const getContactDetails = useCallback(async () => {
simon575c9402022-10-25 16:21:40 -0400119 const controller = new AbortController();
simon94fe53e2022-11-10 12:51:58 -0500120 try {
Misha Krieger-Raynauld46e9d242022-11-12 18:02:43 -0500121 const { data } = await axiosInstance.get(`/contacts/${userId}`, {
simon94fe53e2022-11-10 12:51:58 -0500122 signal: controller.signal,
123 });
124 console.log('CONTACT LIST - DETAILS: ', data);
125 } catch (e) {
126 console.log('ERROR GET CONTACT DETAILS: ', e);
127 }
idillonef9ab812022-11-18 13:46:24 -0500128 }, [axiosInstance, userId]);
simon575c9402022-10-25 16:21:40 -0400129
simonff1cb352022-11-24 15:15:26 -0500130 const conversationId = conversation.getId();
131
idillonef9ab812022-11-18 13:46:24 -0500132 const menuOptions: PopoverListItemData[] = useMemo(
133 () => [
134 {
135 label: t('conversation_message'),
136 Icon: MessageIcon,
simon21f7d9f2022-11-28 14:21:54 -0500137 onClick: onMessageClick,
idillonef9ab812022-11-18 13:46:24 -0500138 },
139 {
140 label: t('conversation_start_audiocall'),
141 Icon: AudioCallIcon,
142 onClick: () => {
simonff1cb352022-11-24 15:15:26 -0500143 if (conversationId) {
144 startCall(conversationId);
145 }
idillonef9ab812022-11-18 13:46:24 -0500146 },
147 },
148 {
149 label: t('conversation_start_videocall'),
150 Icon: VideoCallIcon,
151 onClick: () => {
simonff1cb352022-11-24 15:15:26 -0500152 if (conversationId) {
153 startCall(conversationId, {
154 isVideoOn: true,
155 });
156 }
idillonef9ab812022-11-18 13:46:24 -0500157 },
158 },
159 ...(isSelected
160 ? [
161 {
162 label: t('conversation_close'),
163 Icon: CancelIcon,
164 onClick: () => {
165 navigate(`/`);
166 },
167 },
168 ]
169 : []),
170 {
171 label: t('conversation_details'),
172 Icon: ContactDetailsIcon,
173 onClick: () => {
174 detailsDialogHandler.openDialog();
175 getContactDetails();
176 },
177 },
178 {
179 label: t('conversation_block'),
180 Icon: BlockContactIcon,
181 onClick: () => {
182 blockContactDialogHandler.openDialog();
183 },
184 },
185 {
186 label: t('conversation_delete'),
187 Icon: RemoveContactIcon,
188 onClick: () => {
189 removeContactDialogHandler.openDialog();
190 },
191 },
192 ],
193 [
idillonef9ab812022-11-18 13:46:24 -0500194 navigate,
simon21f7d9f2022-11-28 14:21:54 -0500195 onMessageClick,
idillonef9ab812022-11-18 13:46:24 -0500196 isSelected,
197 getContactDetails,
198 detailsDialogHandler,
199 blockContactDialogHandler,
200 removeContactDialogHandler,
201 t,
simonff1cb352022-11-24 15:15:26 -0500202 startCall,
203 conversationId,
idillonef9ab812022-11-18 13:46:24 -0500204 ]
205 );
simon575c9402022-10-25 16:21:40 -0400206
idillonef9ab812022-11-18 13:46:24 -0500207 return (
208 <>
209 <ContextMenu {...contextMenuProps} items={menuOptions} />
210
211 <DetailsDialog {...detailsDialogHandler.props} userId={userId} conversation={conversation} isSwarm={isSwarm} />
212
213 <RemoveContactDialog {...removeContactDialogHandler.props} userId={userId} conversation={conversation} />
214
215 <BlockContactDialog {...blockContactDialogHandler.props} userId={userId} conversation={conversation} />
216 </>
217 );
218};
219
220interface DetailsDialogProps {
221 userId: string;
222 conversation: Conversation;
223 open: boolean;
224 onClose: () => void;
225 isSwarm: boolean;
226}
227
228const DetailsDialog = ({ userId, conversation, open, onClose, isSwarm }: DetailsDialogProps) => {
229 const { t } = useTranslation();
230 const items = useMemo(
231 () => [
232 {
233 label: t('conversation_details_username'),
234 value: conversation.getDisplayNameNoFallback(),
235 },
236 {
237 label: t('conversation_details_identifier'),
238 value: userId,
239 },
240 {
241 label: t('conversation_details_qr_code'),
242 value: <QRCodeCanvas size={80} value={`${userId}`} />,
243 },
244 {
245 label: t('conversation_details_is_swarm'),
246 value: isSwarm ? t('conversation_details_is_swarm_true') : t('conversation_details_is_swarm_false'),
247 },
248 ],
249 [userId, conversation, isSwarm, t]
250 );
251 return (
252 <InfosDialog
253 open={open}
254 onClose={onClose}
255 icon={
256 <ConversationAvatar
257 sx={{ width: 'inherit', height: 'inherit' }}
258 displayName={conversation.getDisplayNameNoFallback()}
259 />
260 }
261 title={conversation.getDisplayNameNoFallback() || ''}
262 content={<DialogContentList title={t('conversation_details_informations')} items={items} />}
263 />
264 );
265};
266
267interface BlockContactDialogProps {
268 userId: string;
269 conversation: Conversation;
270 open: boolean;
271 onClose: () => void;
272}
273
274const BlockContactDialog = ({ userId, open, onClose }: BlockContactDialogProps) => {
275 const { axiosInstance } = useAuthContext();
276 const { t } = useTranslation();
277 const dispatch = useAppDispatch();
278
279 const block = async () => {
simon575c9402022-10-25 16:21:40 -0400280 const controller = new AbortController();
simon94fe53e2022-11-10 12:51:58 -0500281 try {
idillonef9ab812022-11-18 13:46:24 -0500282 await axiosInstance.post(`/contacts/${userId}/block`, {
simon94fe53e2022-11-10 12:51:58 -0500283 signal: controller.signal,
simon575c9402022-10-25 16:21:40 -0400284 });
simon94fe53e2022-11-10 12:51:58 -0500285 dispatch(setRefreshFromSlice());
286 } catch (e) {
idillonef9ab812022-11-18 13:46:24 -0500287 console.error(`Error $block contact : `, e);
simon94fe53e2022-11-10 12:51:58 -0500288 dispatch(setRefreshFromSlice());
289 }
idillonef9ab812022-11-18 13:46:24 -0500290 onClose();
simon575c9402022-10-25 16:21:40 -0400291 };
292
simon575c9402022-10-25 16:21:40 -0400293 return (
idillonef9ab812022-11-18 13:46:24 -0500294 <ConfirmationDialog
295 open={open}
296 onClose={onClose}
297 title={t('dialog_confirm_title_default')}
298 content={t('conversation_ask_confirm_block')}
299 onConfirm={block}
300 confirmButtonText={t('conversation_confirm_block')}
301 />
simon575c9402022-10-25 16:21:40 -0400302 );
idillonef9ab812022-11-18 13:46:24 -0500303};
304
305interface RemoveContactDialogProps {
306 userId: string;
307 conversation: Conversation;
308 open: boolean;
309 onClose: () => void;
simon575c9402022-10-25 16:21:40 -0400310}
idillonef9ab812022-11-18 13:46:24 -0500311
312const RemoveContactDialog = ({ userId, open, onClose }: RemoveContactDialogProps) => {
313 const { axiosInstance } = useAuthContext();
314 const { t } = useTranslation();
315 const dispatch = useAppDispatch();
316
317 const remove = async () => {
318 const controller = new AbortController();
319 try {
simon21f7d9f2022-11-28 14:21:54 -0500320 await axiosInstance.delete(`/contacts/${userId}`, {
idillonef9ab812022-11-18 13:46:24 -0500321 signal: controller.signal,
322 });
323 dispatch(setRefreshFromSlice());
324 } catch (e) {
325 console.error(`Error removing contact : `, e);
326 dispatch(setRefreshFromSlice());
327 }
328 onClose();
329 };
330
331 return (
332 <ConfirmationDialog
333 open={open}
334 onClose={onClose}
335 title={t('dialog_confirm_title_default')}
336 content={t('conversation_ask_confirm_remove')}
337 onConfirm={remove}
338 confirmButtonText={t('conversation_confirm_remove')}
339 />
340 );
341};