blob: 2861eb8eee1ee4ee1f3d266751bbc52a72ac257d [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';
simon575c9402022-10-25 16:21:40 -040026import { setRefreshFromSlice } from '../redux/appSlice';
27import { useAppDispatch } from '../redux/hooks';
idillonef9ab812022-11-18 13:46:24 -050028import ContextMenu, { ContextMenuHandler, useContextMenuHandler } from './ContextMenu';
simon575c9402022-10-25 16:21:40 -040029import ConversationAvatar from './ConversationAvatar';
idillonef9ab812022-11-18 13:46:24 -050030import { ConfirmationDialog, DialogContentList, InfosDialog, useDialogHandler } from './Dialog';
31import { PopoverListItemData } from './PopoverList';
simon5da8ca62022-11-09 15:21:25 -050032import {
33 AudioCallIcon,
34 BlockContactIcon,
35 CancelIcon,
36 ContactDetailsIcon,
37 MessageIcon,
38 RemoveContactIcon,
39 VideoCallIcon,
40} from './SvgIcon';
simon575c9402022-10-25 16:21:40 -040041
simon575c9402022-10-25 16:21:40 -040042type ConversationListItemProps = {
43 conversation: Conversation;
44};
45
46export default function ConversationListItem({ conversation }: ConversationListItemProps) {
47 const { conversationId, contactId } = useParams();
idillonef9ab812022-11-18 13:46:24 -050048 const contextMenuHandler = useContextMenuHandler();
simon575c9402022-10-25 16:21:40 -040049
50 const pathId = conversationId || contactId;
51 const isSelected = conversation.getDisplayUri() === pathId;
52 const navigate = useNavigate();
simon416d0792022-11-03 02:46:18 -040053 const [userId] = useState(conversation?.getFirstMember()?.contact.getUri());
idillonef9ab812022-11-18 13:46:24 -050054 const uri = conversation.getId() ? `/conversation/${conversation.getId()}` : `/add-contact/${userId}`;
55 return (
56 <Box onContextMenu={contextMenuHandler.handleAnchorPosition}>
57 <ConversationMenu
58 userId={userId}
59 conversation={conversation}
60 uri={uri}
61 isSelected={isSelected}
62 contextMenuProps={contextMenuHandler.props}
63 />
64 <ListItem button alignItems="flex-start" selected={isSelected} onClick={() => navigate(uri)}>
65 <ListItemAvatar>
66 <ConversationAvatar displayName={conversation.getDisplayNameNoFallback()} />
67 </ListItemAvatar>
68 <ListItemText primary={conversation.getDisplayName()} secondary={conversation.getDisplayUri()} />
69 </ListItem>
70 </Box>
71 );
72}
73
74interface ConversationMenuProps {
75 userId: string;
76 conversation: Conversation;
77 uri: string;
78 isSelected: boolean;
79 contextMenuProps: ContextMenuHandler['props'];
80}
81
82const ConversationMenu = ({ userId, conversation, uri, isSelected, contextMenuProps }: ConversationMenuProps) => {
83 const { t } = useTranslation();
84 const { axiosInstance } = useAuthContext();
simon416d0792022-11-03 02:46:18 -040085 const [isSwarm] = useState(true);
simon575c9402022-10-25 16:21:40 -040086
idillonef9ab812022-11-18 13:46:24 -050087 const detailsDialogHandler = useDialogHandler();
88 const blockContactDialogHandler = useDialogHandler();
89 const removeContactDialogHandler = useDialogHandler();
simon575c9402022-10-25 16:21:40 -040090
idillonef9ab812022-11-18 13:46:24 -050091 const navigate = useNavigate();
92
93 const getContactDetails = useCallback(async () => {
simon575c9402022-10-25 16:21:40 -040094 const controller = new AbortController();
simon94fe53e2022-11-10 12:51:58 -050095 try {
Misha Krieger-Raynauld46e9d242022-11-12 18:02:43 -050096 const { data } = await axiosInstance.get(`/contacts/${userId}`, {
simon94fe53e2022-11-10 12:51:58 -050097 signal: controller.signal,
98 });
99 console.log('CONTACT LIST - DETAILS: ', data);
100 } catch (e) {
101 console.log('ERROR GET CONTACT DETAILS: ', e);
102 }
idillonef9ab812022-11-18 13:46:24 -0500103 }, [axiosInstance, userId]);
simon575c9402022-10-25 16:21:40 -0400104
idillonef9ab812022-11-18 13:46:24 -0500105 const menuOptions: PopoverListItemData[] = useMemo(
106 () => [
107 {
108 label: t('conversation_message'),
109 Icon: MessageIcon,
110 onClick: () => {
111 navigate(uri);
112 },
113 },
114 {
115 label: t('conversation_start_audiocall'),
116 Icon: AudioCallIcon,
117 onClick: () => {
118 navigate(`/account/call/${conversation.getId()}`);
119 },
120 },
121 {
122 label: t('conversation_start_videocall'),
123 Icon: VideoCallIcon,
124 onClick: () => {
125 navigate(`call/${conversation.getId()}?video=true`);
126 },
127 },
128 ...(isSelected
129 ? [
130 {
131 label: t('conversation_close'),
132 Icon: CancelIcon,
133 onClick: () => {
134 navigate(`/`);
135 },
136 },
137 ]
138 : []),
139 {
140 label: t('conversation_details'),
141 Icon: ContactDetailsIcon,
142 onClick: () => {
143 detailsDialogHandler.openDialog();
144 getContactDetails();
145 },
146 },
147 {
148 label: t('conversation_block'),
149 Icon: BlockContactIcon,
150 onClick: () => {
151 blockContactDialogHandler.openDialog();
152 },
153 },
154 {
155 label: t('conversation_delete'),
156 Icon: RemoveContactIcon,
157 onClick: () => {
158 removeContactDialogHandler.openDialog();
159 },
160 },
161 ],
162 [
163 conversation,
164 navigate,
165 uri,
166 isSelected,
167 getContactDetails,
168 detailsDialogHandler,
169 blockContactDialogHandler,
170 removeContactDialogHandler,
171 t,
172 ]
173 );
simon575c9402022-10-25 16:21:40 -0400174
idillonef9ab812022-11-18 13:46:24 -0500175 return (
176 <>
177 <ContextMenu {...contextMenuProps} items={menuOptions} />
178
179 <DetailsDialog {...detailsDialogHandler.props} userId={userId} conversation={conversation} isSwarm={isSwarm} />
180
181 <RemoveContactDialog {...removeContactDialogHandler.props} userId={userId} conversation={conversation} />
182
183 <BlockContactDialog {...blockContactDialogHandler.props} userId={userId} conversation={conversation} />
184 </>
185 );
186};
187
188interface DetailsDialogProps {
189 userId: string;
190 conversation: Conversation;
191 open: boolean;
192 onClose: () => void;
193 isSwarm: boolean;
194}
195
196const DetailsDialog = ({ userId, conversation, open, onClose, isSwarm }: DetailsDialogProps) => {
197 const { t } = useTranslation();
198 const items = useMemo(
199 () => [
200 {
201 label: t('conversation_details_username'),
202 value: conversation.getDisplayNameNoFallback(),
203 },
204 {
205 label: t('conversation_details_identifier'),
206 value: userId,
207 },
208 {
209 label: t('conversation_details_qr_code'),
210 value: <QRCodeCanvas size={80} value={`${userId}`} />,
211 },
212 {
213 label: t('conversation_details_is_swarm'),
214 value: isSwarm ? t('conversation_details_is_swarm_true') : t('conversation_details_is_swarm_false'),
215 },
216 ],
217 [userId, conversation, isSwarm, t]
218 );
219 return (
220 <InfosDialog
221 open={open}
222 onClose={onClose}
223 icon={
224 <ConversationAvatar
225 sx={{ width: 'inherit', height: 'inherit' }}
226 displayName={conversation.getDisplayNameNoFallback()}
227 />
228 }
229 title={conversation.getDisplayNameNoFallback() || ''}
230 content={<DialogContentList title={t('conversation_details_informations')} items={items} />}
231 />
232 );
233};
234
235interface BlockContactDialogProps {
236 userId: string;
237 conversation: Conversation;
238 open: boolean;
239 onClose: () => void;
240}
241
242const BlockContactDialog = ({ userId, open, onClose }: BlockContactDialogProps) => {
243 const { axiosInstance } = useAuthContext();
244 const { t } = useTranslation();
245 const dispatch = useAppDispatch();
246
247 const block = async () => {
simon575c9402022-10-25 16:21:40 -0400248 const controller = new AbortController();
simon94fe53e2022-11-10 12:51:58 -0500249 try {
idillonef9ab812022-11-18 13:46:24 -0500250 await axiosInstance.post(`/contacts/${userId}/block`, {
simon94fe53e2022-11-10 12:51:58 -0500251 signal: controller.signal,
simon575c9402022-10-25 16:21:40 -0400252 });
simon94fe53e2022-11-10 12:51:58 -0500253 dispatch(setRefreshFromSlice());
254 } catch (e) {
idillonef9ab812022-11-18 13:46:24 -0500255 console.error(`Error $block contact : `, e);
simon94fe53e2022-11-10 12:51:58 -0500256 dispatch(setRefreshFromSlice());
257 }
idillonef9ab812022-11-18 13:46:24 -0500258 onClose();
simon575c9402022-10-25 16:21:40 -0400259 };
260
simon575c9402022-10-25 16:21:40 -0400261 return (
idillonef9ab812022-11-18 13:46:24 -0500262 <ConfirmationDialog
263 open={open}
264 onClose={onClose}
265 title={t('dialog_confirm_title_default')}
266 content={t('conversation_ask_confirm_block')}
267 onConfirm={block}
268 confirmButtonText={t('conversation_confirm_block')}
269 />
simon575c9402022-10-25 16:21:40 -0400270 );
idillonef9ab812022-11-18 13:46:24 -0500271};
272
273interface RemoveContactDialogProps {
274 userId: string;
275 conversation: Conversation;
276 open: boolean;
277 onClose: () => void;
simon575c9402022-10-25 16:21:40 -0400278}
idillonef9ab812022-11-18 13:46:24 -0500279
280const RemoveContactDialog = ({ userId, open, onClose }: RemoveContactDialogProps) => {
281 const { axiosInstance } = useAuthContext();
282 const { t } = useTranslation();
283 const dispatch = useAppDispatch();
284
285 const remove = async () => {
286 const controller = new AbortController();
287 try {
288 await axiosInstance.delete(`/contacts/${userId}/remove`, {
289 signal: controller.signal,
290 });
291 dispatch(setRefreshFromSlice());
292 } catch (e) {
293 console.error(`Error removing contact : `, e);
294 dispatch(setRefreshFromSlice());
295 }
296 onClose();
297 };
298
299 return (
300 <ConfirmationDialog
301 open={open}
302 onClose={onClose}
303 title={t('dialog_confirm_title_default')}
304 content={t('conversation_ask_confirm_remove')}
305 onConfirm={remove}
306 confirmButtonText={t('conversation_confirm_remove')}
307 />
308 );
309};