blob: 48e9004ed3f6bcfb6d9565be884696fb5bad843d [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';
idillonef9ab812022-11-18 13:46:24 -050021import { useCallback, useMemo, useState } from 'react';
simon4e7445c2022-11-16 21:18:46 -050022import { useTranslation } from 'react-i18next';
simon575c9402022-10-25 16:21:40 -040023import { useNavigate, useParams } from 'react-router-dom';
24
simon5da8ca62022-11-09 15:21:25 -050025import { useAuthContext } from '../contexts/AuthProvider';
simonff1cb352022-11-24 15:15:26 -050026import { useStartCall } from '../hooks/useStartCall';
simon575c9402022-10-25 16:21:40 -040027import { setRefreshFromSlice } from '../redux/appSlice';
28import { useAppDispatch } from '../redux/hooks';
idillonef9ab812022-11-18 13:46:24 -050029import ContextMenu, { ContextMenuHandler, useContextMenuHandler } from './ContextMenu';
simon575c9402022-10-25 16:21:40 -040030import ConversationAvatar from './ConversationAvatar';
idillonef9ab812022-11-18 13:46:24 -050031import { ConfirmationDialog, DialogContentList, InfosDialog, useDialogHandler } from './Dialog';
32import { PopoverListItemData } from './PopoverList';
simon5da8ca62022-11-09 15:21:25 -050033import {
34 AudioCallIcon,
35 BlockContactIcon,
36 CancelIcon,
37 ContactDetailsIcon,
38 MessageIcon,
39 RemoveContactIcon,
40 VideoCallIcon,
41} from './SvgIcon';
simon575c9402022-10-25 16:21:40 -040042
simon575c9402022-10-25 16:21:40 -040043type ConversationListItemProps = {
44 conversation: Conversation;
45};
46
47export default function ConversationListItem({ conversation }: ConversationListItemProps) {
48 const { conversationId, contactId } = useParams();
idillonef9ab812022-11-18 13:46:24 -050049 const contextMenuHandler = useContextMenuHandler();
simon575c9402022-10-25 16:21:40 -040050
51 const pathId = conversationId || contactId;
52 const isSelected = conversation.getDisplayUri() === pathId;
53 const navigate = useNavigate();
Michelle Sepkap Simedd82cbf2022-11-17 23:31:49 -050054 const userId = conversation?.getFirstMember()?.contact.getUri();
simonff1cb352022-11-24 15:15:26 -050055
56 // TODO: Improve this component. conversationId should never be undefined.
57 // (https://git.jami.net/savoirfairelinux/jami-web/-/issues/171)
58 const uri = conversation.getId()
59 ? `/conversation/${conversation.getId()}`
60 : `/conversation/add-contact?newContactId=${userId}`;
idillonef9ab812022-11-18 13:46:24 -050061 return (
62 <Box onContextMenu={contextMenuHandler.handleAnchorPosition}>
63 <ConversationMenu
64 userId={userId}
65 conversation={conversation}
66 uri={uri}
67 isSelected={isSelected}
68 contextMenuProps={contextMenuHandler.props}
69 />
70 <ListItem button alignItems="flex-start" selected={isSelected} onClick={() => navigate(uri)}>
71 <ListItemAvatar>
72 <ConversationAvatar displayName={conversation.getDisplayNameNoFallback()} />
73 </ListItemAvatar>
74 <ListItemText primary={conversation.getDisplayName()} secondary={conversation.getDisplayUri()} />
75 </ListItem>
76 </Box>
77 );
78}
79
80interface ConversationMenuProps {
81 userId: string;
82 conversation: Conversation;
83 uri: string;
84 isSelected: boolean;
85 contextMenuProps: ContextMenuHandler['props'];
86}
87
88const ConversationMenu = ({ userId, conversation, uri, isSelected, contextMenuProps }: ConversationMenuProps) => {
89 const { t } = useTranslation();
90 const { axiosInstance } = useAuthContext();
simon416d0792022-11-03 02:46:18 -040091 const [isSwarm] = useState(true);
simon575c9402022-10-25 16:21:40 -040092
idillonef9ab812022-11-18 13:46:24 -050093 const detailsDialogHandler = useDialogHandler();
94 const blockContactDialogHandler = useDialogHandler();
95 const removeContactDialogHandler = useDialogHandler();
simon575c9402022-10-25 16:21:40 -040096
idillonef9ab812022-11-18 13:46:24 -050097 const navigate = useNavigate();
98
simonff1cb352022-11-24 15:15:26 -050099 const startCall = useStartCall();
100
idillonef9ab812022-11-18 13:46:24 -0500101 const getContactDetails = useCallback(async () => {
simon575c9402022-10-25 16:21:40 -0400102 const controller = new AbortController();
simon94fe53e2022-11-10 12:51:58 -0500103 try {
Misha Krieger-Raynauld46e9d242022-11-12 18:02:43 -0500104 const { data } = await axiosInstance.get(`/contacts/${userId}`, {
simon94fe53e2022-11-10 12:51:58 -0500105 signal: controller.signal,
106 });
107 console.log('CONTACT LIST - DETAILS: ', data);
108 } catch (e) {
109 console.log('ERROR GET CONTACT DETAILS: ', e);
110 }
idillonef9ab812022-11-18 13:46:24 -0500111 }, [axiosInstance, userId]);
simon575c9402022-10-25 16:21:40 -0400112
simonff1cb352022-11-24 15:15:26 -0500113 const conversationId = conversation.getId();
114
idillonef9ab812022-11-18 13:46:24 -0500115 const menuOptions: PopoverListItemData[] = useMemo(
116 () => [
117 {
118 label: t('conversation_message'),
119 Icon: MessageIcon,
120 onClick: () => {
121 navigate(uri);
122 },
123 },
124 {
125 label: t('conversation_start_audiocall'),
126 Icon: AudioCallIcon,
127 onClick: () => {
simonff1cb352022-11-24 15:15:26 -0500128 if (conversationId) {
129 startCall(conversationId);
130 }
idillonef9ab812022-11-18 13:46:24 -0500131 },
132 },
133 {
134 label: t('conversation_start_videocall'),
135 Icon: VideoCallIcon,
136 onClick: () => {
simonff1cb352022-11-24 15:15:26 -0500137 if (conversationId) {
138 startCall(conversationId, {
139 isVideoOn: true,
140 });
141 }
idillonef9ab812022-11-18 13:46:24 -0500142 },
143 },
144 ...(isSelected
145 ? [
146 {
147 label: t('conversation_close'),
148 Icon: CancelIcon,
149 onClick: () => {
150 navigate(`/`);
151 },
152 },
153 ]
154 : []),
155 {
156 label: t('conversation_details'),
157 Icon: ContactDetailsIcon,
158 onClick: () => {
159 detailsDialogHandler.openDialog();
160 getContactDetails();
161 },
162 },
163 {
164 label: t('conversation_block'),
165 Icon: BlockContactIcon,
166 onClick: () => {
167 blockContactDialogHandler.openDialog();
168 },
169 },
170 {
171 label: t('conversation_delete'),
172 Icon: RemoveContactIcon,
173 onClick: () => {
174 removeContactDialogHandler.openDialog();
175 },
176 },
177 ],
178 [
idillonef9ab812022-11-18 13:46:24 -0500179 navigate,
180 uri,
181 isSelected,
182 getContactDetails,
183 detailsDialogHandler,
184 blockContactDialogHandler,
185 removeContactDialogHandler,
186 t,
simonff1cb352022-11-24 15:15:26 -0500187 startCall,
188 conversationId,
idillonef9ab812022-11-18 13:46:24 -0500189 ]
190 );
simon575c9402022-10-25 16:21:40 -0400191
idillonef9ab812022-11-18 13:46:24 -0500192 return (
193 <>
194 <ContextMenu {...contextMenuProps} items={menuOptions} />
195
196 <DetailsDialog {...detailsDialogHandler.props} userId={userId} conversation={conversation} isSwarm={isSwarm} />
197
198 <RemoveContactDialog {...removeContactDialogHandler.props} userId={userId} conversation={conversation} />
199
200 <BlockContactDialog {...blockContactDialogHandler.props} userId={userId} conversation={conversation} />
201 </>
202 );
203};
204
205interface DetailsDialogProps {
206 userId: string;
207 conversation: Conversation;
208 open: boolean;
209 onClose: () => void;
210 isSwarm: boolean;
211}
212
213const DetailsDialog = ({ userId, conversation, open, onClose, isSwarm }: DetailsDialogProps) => {
214 const { t } = useTranslation();
215 const items = useMemo(
216 () => [
217 {
218 label: t('conversation_details_username'),
219 value: conversation.getDisplayNameNoFallback(),
220 },
221 {
222 label: t('conversation_details_identifier'),
223 value: userId,
224 },
225 {
226 label: t('conversation_details_qr_code'),
227 value: <QRCodeCanvas size={80} value={`${userId}`} />,
228 },
229 {
230 label: t('conversation_details_is_swarm'),
231 value: isSwarm ? t('conversation_details_is_swarm_true') : t('conversation_details_is_swarm_false'),
232 },
233 ],
234 [userId, conversation, isSwarm, t]
235 );
236 return (
237 <InfosDialog
238 open={open}
239 onClose={onClose}
240 icon={
241 <ConversationAvatar
242 sx={{ width: 'inherit', height: 'inherit' }}
243 displayName={conversation.getDisplayNameNoFallback()}
244 />
245 }
246 title={conversation.getDisplayNameNoFallback() || ''}
247 content={<DialogContentList title={t('conversation_details_informations')} items={items} />}
248 />
249 );
250};
251
252interface BlockContactDialogProps {
253 userId: string;
254 conversation: Conversation;
255 open: boolean;
256 onClose: () => void;
257}
258
259const BlockContactDialog = ({ userId, open, onClose }: BlockContactDialogProps) => {
260 const { axiosInstance } = useAuthContext();
261 const { t } = useTranslation();
262 const dispatch = useAppDispatch();
263
264 const block = async () => {
simon575c9402022-10-25 16:21:40 -0400265 const controller = new AbortController();
simon94fe53e2022-11-10 12:51:58 -0500266 try {
idillonef9ab812022-11-18 13:46:24 -0500267 await axiosInstance.post(`/contacts/${userId}/block`, {
simon94fe53e2022-11-10 12:51:58 -0500268 signal: controller.signal,
simon575c9402022-10-25 16:21:40 -0400269 });
simon94fe53e2022-11-10 12:51:58 -0500270 dispatch(setRefreshFromSlice());
271 } catch (e) {
idillonef9ab812022-11-18 13:46:24 -0500272 console.error(`Error $block contact : `, e);
simon94fe53e2022-11-10 12:51:58 -0500273 dispatch(setRefreshFromSlice());
274 }
idillonef9ab812022-11-18 13:46:24 -0500275 onClose();
simon575c9402022-10-25 16:21:40 -0400276 };
277
simon575c9402022-10-25 16:21:40 -0400278 return (
idillonef9ab812022-11-18 13:46:24 -0500279 <ConfirmationDialog
280 open={open}
281 onClose={onClose}
282 title={t('dialog_confirm_title_default')}
283 content={t('conversation_ask_confirm_block')}
284 onConfirm={block}
285 confirmButtonText={t('conversation_confirm_block')}
286 />
simon575c9402022-10-25 16:21:40 -0400287 );
idillonef9ab812022-11-18 13:46:24 -0500288};
289
290interface RemoveContactDialogProps {
291 userId: string;
292 conversation: Conversation;
293 open: boolean;
294 onClose: () => void;
simon575c9402022-10-25 16:21:40 -0400295}
idillonef9ab812022-11-18 13:46:24 -0500296
297const RemoveContactDialog = ({ userId, open, onClose }: RemoveContactDialogProps) => {
298 const { axiosInstance } = useAuthContext();
299 const { t } = useTranslation();
300 const dispatch = useAppDispatch();
301
302 const remove = async () => {
303 const controller = new AbortController();
304 try {
305 await axiosInstance.delete(`/contacts/${userId}/remove`, {
306 signal: controller.signal,
307 });
308 dispatch(setRefreshFromSlice());
309 } catch (e) {
310 console.error(`Error removing contact : `, e);
311 dispatch(setRefreshFromSlice());
312 }
313 onClose();
314 };
315
316 return (
317 <ConfirmationDialog
318 open={open}
319 onClose={onClose}
320 title={t('dialog_confirm_title_default')}
321 content={t('conversation_ask_confirm_remove')}
322 onConfirm={remove}
323 confirmButtonText={t('conversation_confirm_remove')}
324 />
325 );
326};