blob: 710fe0b0e520e979bac343b22075acf476be749e [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';
idillon07d31cc2022-12-06 22:40:14 -050019import { IConversationSummary } 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';
Michelle Sepkap Simec050d9c2022-12-05 09:39:34 -050027import { CallStatus, useCallContext } from '../contexts/CallProvider';
idillon07d31cc2022-12-06 22:40:14 -050028import { useUrlParams } from '../hooks/useUrlParams';
simon575c9402022-10-25 16:21:40 -040029import { setRefreshFromSlice } from '../redux/appSlice';
30import { useAppDispatch } from '../redux/hooks';
idillon07d31cc2022-12-06 22:40:14 -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';
idillon07d31cc2022-12-06 22:40:14 -050036import { AudioCallIcon, CancelIcon, MessageIcon, PersonIcon, VideoCallIcon } from './SvgIcon';
simon575c9402022-10-25 16:21:40 -040037
simon575c9402022-10-25 16:21:40 -040038type ConversationListItemProps = {
idillon07d31cc2022-12-06 22:40:14 -050039 conversationSummary: IConversationSummary;
simon575c9402022-10-25 16:21:40 -040040};
41
idillon07d31cc2022-12-06 22:40:14 -050042export default function ConversationListItem({ conversationSummary }: ConversationListItemProps) {
43 const {
44 urlParams: { conversationId: selectedConversationId },
45 } = useUrlParams<ConversationRouteParams>();
idillonef9ab812022-11-18 13:46:24 -050046 const contextMenuHandler = useContextMenuHandler();
Michelle Sepkap Simec050d9c2022-12-05 09:39:34 -050047 const callContext = useCallContext(true);
48 const { callData } = useContext(CallManagerContext);
49 const { t } = useTranslation();
simon575c9402022-10-25 16:21:40 -040050 const navigate = useNavigate();
idillon07d31cc2022-12-06 22:40:14 -050051
52 const conversationId = conversationSummary.id;
53 const isSelected = conversationId === selectedConversationId;
simonff1cb352022-11-24 15:15:26 -050054
simon21f7d9f2022-11-28 14:21:54 -050055 const onClick = useCallback(() => {
idillon07d31cc2022-12-06 22:40:14 -050056 if (conversationId) {
57 navigate(`/conversation/${conversationId}`);
simon21f7d9f2022-11-28 14:21:54 -050058 }
idillon07d31cc2022-12-06 22:40:14 -050059 }, [navigate, conversationId]);
simon21f7d9f2022-11-28 14:21:54 -050060
idillon07d31cc2022-12-06 22:40:14 -050061 const secondaryText = useMemo(() => {
62 if (!callContext || !callData || callData.conversationId !== conversationSummary.id) {
63 return conversationSummary.lastMessage.body;
Michelle Sepkap Simec050d9c2022-12-05 09:39:34 -050064 }
65
66 if (callContext.callStatus === CallStatus.InCall) {
67 return callContext.isAudioOn ? t('ongoing_call_unmuted') : t('ongoing_call_muted');
68 }
69
70 if (callContext.callStatus === CallStatus.Connecting) {
71 return t('connecting_call');
72 }
73
74 return callContext.callRole === 'caller' ? t('outgoing_call') : t('incoming_call');
idillon07d31cc2022-12-06 22:40:14 -050075 }, [conversationSummary, callContext, callData, t]);
76
77 const conversationName = useMemo(
78 () => conversationSummary.title ?? conversationSummary.membersNames.join(', '),
79 [conversationSummary]
80 );
Michelle Sepkap Simec050d9c2022-12-05 09:39:34 -050081
idillonef9ab812022-11-18 13:46:24 -050082 return (
idillon22ed8192022-11-29 12:17:04 -050083 <Box>
idillonef9ab812022-11-18 13:46:24 -050084 <ConversationMenu
idillon07d31cc2022-12-06 22:40:14 -050085 conversationId={conversationId}
86 conversationName={conversationName}
simon21f7d9f2022-11-28 14:21:54 -050087 onMessageClick={onClick}
idillonef9ab812022-11-18 13:46:24 -050088 isSelected={isSelected}
89 contextMenuProps={contextMenuHandler.props}
90 />
idillon22ed8192022-11-29 12:17:04 -050091 <ListItem
92 button
93 alignItems="flex-start"
94 selected={isSelected}
95 onClick={onClick}
96 onContextMenu={contextMenuHandler.handleAnchorPosition}
97 >
idillonef9ab812022-11-18 13:46:24 -050098 <ListItemAvatar>
idillon07d31cc2022-12-06 22:40:14 -050099 <ConversationAvatar displayName={conversationName} />
idillonef9ab812022-11-18 13:46:24 -0500100 </ListItemAvatar>
idillon07d31cc2022-12-06 22:40:14 -0500101 <ListItemText primary={conversationName} secondary={secondaryText} />
idillonef9ab812022-11-18 13:46:24 -0500102 </ListItem>
103 </Box>
104 );
105}
106
107interface ConversationMenuProps {
idillon07d31cc2022-12-06 22:40:14 -0500108 conversationId: string;
109 conversationName: string;
simon21f7d9f2022-11-28 14:21:54 -0500110 onMessageClick: () => void;
idillonef9ab812022-11-18 13:46:24 -0500111 isSelected: boolean;
112 contextMenuProps: ContextMenuHandler['props'];
113}
114
simon21f7d9f2022-11-28 14:21:54 -0500115const ConversationMenu = ({
idillon07d31cc2022-12-06 22:40:14 -0500116 conversationId,
117 conversationName,
simon21f7d9f2022-11-28 14:21:54 -0500118 onMessageClick,
119 isSelected,
120 contextMenuProps,
121}: ConversationMenuProps) => {
idillonef9ab812022-11-18 13:46:24 -0500122 const { t } = useTranslation();
simone35acc22022-12-02 16:51:12 -0500123 const { startCall } = useContext(CallManagerContext);
simon416d0792022-11-03 02:46:18 -0400124 const [isSwarm] = useState(true);
simon575c9402022-10-25 16:21:40 -0400125
idillonef9ab812022-11-18 13:46:24 -0500126 const detailsDialogHandler = useDialogHandler();
idillon07d31cc2022-12-06 22:40:14 -0500127 const RemoveConversationDialogHandler = useDialogHandler();
simon575c9402022-10-25 16:21:40 -0400128
idillonef9ab812022-11-18 13:46:24 -0500129 const navigate = useNavigate();
130
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) {
simone35acc22022-12-02 16:51:12 -0500143 startCall({
144 conversationId,
145 role: 'caller',
146 });
simonff1cb352022-11-24 15:15:26 -0500147 }
idillonef9ab812022-11-18 13:46:24 -0500148 },
149 },
150 {
151 label: t('conversation_start_videocall'),
152 Icon: VideoCallIcon,
153 onClick: () => {
simonff1cb352022-11-24 15:15:26 -0500154 if (conversationId) {
simone35acc22022-12-02 16:51:12 -0500155 startCall({
156 conversationId,
157 role: 'caller',
158 withVideoOn: true,
simonff1cb352022-11-24 15:15:26 -0500159 });
160 }
idillonef9ab812022-11-18 13:46:24 -0500161 },
162 },
163 ...(isSelected
164 ? [
165 {
166 label: t('conversation_close'),
167 Icon: CancelIcon,
168 onClick: () => {
169 navigate(`/`);
170 },
171 },
172 ]
173 : []),
174 {
175 label: t('conversation_details'),
idillon07d31cc2022-12-06 22:40:14 -0500176 Icon: PersonIcon,
idillonef9ab812022-11-18 13:46:24 -0500177 onClick: () => {
178 detailsDialogHandler.openDialog();
idillonef9ab812022-11-18 13:46:24 -0500179 },
180 },
181 {
182 label: t('conversation_delete'),
idillon07d31cc2022-12-06 22:40:14 -0500183 Icon: CancelIcon,
idillonef9ab812022-11-18 13:46:24 -0500184 onClick: () => {
idillon07d31cc2022-12-06 22:40:14 -0500185 RemoveConversationDialogHandler.openDialog();
idillonef9ab812022-11-18 13:46:24 -0500186 },
187 },
188 ],
189 [
idillonef9ab812022-11-18 13:46:24 -0500190 navigate,
simon21f7d9f2022-11-28 14:21:54 -0500191 onMessageClick,
idillonef9ab812022-11-18 13:46:24 -0500192 isSelected,
idillonef9ab812022-11-18 13:46:24 -0500193 detailsDialogHandler,
idillon07d31cc2022-12-06 22:40:14 -0500194 RemoveConversationDialogHandler,
idillonef9ab812022-11-18 13:46:24 -0500195 t,
simonff1cb352022-11-24 15:15:26 -0500196 startCall,
197 conversationId,
idillonef9ab812022-11-18 13:46:24 -0500198 ]
199 );
simon575c9402022-10-25 16:21:40 -0400200
idillonef9ab812022-11-18 13:46:24 -0500201 return (
202 <>
203 <ContextMenu {...contextMenuProps} items={menuOptions} />
204
idillon07d31cc2022-12-06 22:40:14 -0500205 <DetailsDialog
206 {...detailsDialogHandler.props}
207 conversationId={conversationId}
208 conversationName={conversationName}
209 isSwarm={isSwarm}
210 />
idillonef9ab812022-11-18 13:46:24 -0500211
idillon07d31cc2022-12-06 22:40:14 -0500212 <RemoveConversationDialog {...RemoveConversationDialogHandler.props} conversationId={conversationId} />
idillonef9ab812022-11-18 13:46:24 -0500213 </>
214 );
215};
216
217interface DetailsDialogProps {
idillon07d31cc2022-12-06 22:40:14 -0500218 conversationId: string;
219 conversationName: string;
idillonef9ab812022-11-18 13:46:24 -0500220 open: boolean;
221 onClose: () => void;
222 isSwarm: boolean;
223}
224
idillon07d31cc2022-12-06 22:40:14 -0500225const DetailsDialog = ({ conversationId, conversationName, open, onClose, isSwarm }: DetailsDialogProps) => {
idillonef9ab812022-11-18 13:46:24 -0500226 const { t } = useTranslation();
227 const items = useMemo(
228 () => [
229 {
idillon07d31cc2022-12-06 22:40:14 -0500230 label: t('conversation_details_name'),
231 value: conversationName,
idillonef9ab812022-11-18 13:46:24 -0500232 },
233 {
234 label: t('conversation_details_identifier'),
idillon07d31cc2022-12-06 22:40:14 -0500235 value: conversationId,
idillonef9ab812022-11-18 13:46:24 -0500236 },
237 {
238 label: t('conversation_details_qr_code'),
idillon07d31cc2022-12-06 22:40:14 -0500239 value: <QRCodeCanvas size={80} value={`${conversationId}`} />,
idillonef9ab812022-11-18 13:46:24 -0500240 },
241 {
242 label: t('conversation_details_is_swarm'),
243 value: isSwarm ? t('conversation_details_is_swarm_true') : t('conversation_details_is_swarm_false'),
244 },
245 ],
idillon07d31cc2022-12-06 22:40:14 -0500246 [conversationId, conversationName, isSwarm, t]
idillonef9ab812022-11-18 13:46:24 -0500247 );
248 return (
249 <InfosDialog
250 open={open}
251 onClose={onClose}
idillon07d31cc2022-12-06 22:40:14 -0500252 icon={<ConversationAvatar sx={{ width: 'inherit', height: 'inherit' }} displayName={conversationName} />}
253 title={conversationName}
idillonef9ab812022-11-18 13:46:24 -0500254 content={<DialogContentList title={t('conversation_details_informations')} items={items} />}
255 />
256 );
257};
258
idillon07d31cc2022-12-06 22:40:14 -0500259interface RemoveConversationDialogProps {
260 conversationId: string;
idillonef9ab812022-11-18 13:46:24 -0500261 open: boolean;
262 onClose: () => void;
263}
264
idillon07d31cc2022-12-06 22:40:14 -0500265const RemoveConversationDialog = ({ conversationId, open, onClose }: RemoveConversationDialogProps) => {
idillonef9ab812022-11-18 13:46:24 -0500266 const { axiosInstance } = useAuthContext();
267 const { t } = useTranslation();
268 const dispatch = useAppDispatch();
269
270 const remove = async () => {
271 const controller = new AbortController();
272 try {
idillon07d31cc2022-12-06 22:40:14 -0500273 await axiosInstance.delete(`/conversations/${conversationId}`, {
idillonef9ab812022-11-18 13:46:24 -0500274 signal: controller.signal,
275 });
276 dispatch(setRefreshFromSlice());
277 } catch (e) {
idillon07d31cc2022-12-06 22:40:14 -0500278 console.error(`Error removing conversation : `, e);
idillonef9ab812022-11-18 13:46:24 -0500279 dispatch(setRefreshFromSlice());
280 }
281 onClose();
282 };
283
284 return (
285 <ConfirmationDialog
286 open={open}
287 onClose={onClose}
288 title={t('dialog_confirm_title_default')}
289 content={t('conversation_ask_confirm_remove')}
290 onConfirm={remove}
291 confirmButtonText={t('conversation_confirm_remove')}
292 />
293 );
294};