blob: f71705f0455da89d6b3f8f897fbfc1f72593e869 [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 (
83 <Box onContextMenu={contextMenuHandler.handleAnchorPosition}>
84 <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 />
simon21f7d9f2022-11-28 14:21:54 -050091 <ListItem button alignItems="flex-start" selected={isSelected} onClick={onClick}>
idillonef9ab812022-11-18 13:46:24 -050092 <ListItemAvatar>
idillon07d31cc2022-12-06 22:40:14 -050093 <ConversationAvatar displayName={conversationName} />
idillonef9ab812022-11-18 13:46:24 -050094 </ListItemAvatar>
idillon07d31cc2022-12-06 22:40:14 -050095 <ListItemText primary={conversationName} secondary={secondaryText} />
idillonef9ab812022-11-18 13:46:24 -050096 </ListItem>
97 </Box>
98 );
99}
100
101interface ConversationMenuProps {
idillon07d31cc2022-12-06 22:40:14 -0500102 conversationId: string;
103 conversationName: string;
simon21f7d9f2022-11-28 14:21:54 -0500104 onMessageClick: () => void;
idillonef9ab812022-11-18 13:46:24 -0500105 isSelected: boolean;
106 contextMenuProps: ContextMenuHandler['props'];
107}
108
simon21f7d9f2022-11-28 14:21:54 -0500109const ConversationMenu = ({
idillon07d31cc2022-12-06 22:40:14 -0500110 conversationId,
111 conversationName,
simon21f7d9f2022-11-28 14:21:54 -0500112 onMessageClick,
113 isSelected,
114 contextMenuProps,
115}: ConversationMenuProps) => {
idillonef9ab812022-11-18 13:46:24 -0500116 const { t } = useTranslation();
simone35acc22022-12-02 16:51:12 -0500117 const { startCall } = useContext(CallManagerContext);
simon416d0792022-11-03 02:46:18 -0400118 const [isSwarm] = useState(true);
simon575c9402022-10-25 16:21:40 -0400119
idillonef9ab812022-11-18 13:46:24 -0500120 const detailsDialogHandler = useDialogHandler();
idillon07d31cc2022-12-06 22:40:14 -0500121 const RemoveConversationDialogHandler = useDialogHandler();
simon575c9402022-10-25 16:21:40 -0400122
idillonef9ab812022-11-18 13:46:24 -0500123 const navigate = useNavigate();
124
idillonef9ab812022-11-18 13:46:24 -0500125 const menuOptions: PopoverListItemData[] = useMemo(
126 () => [
127 {
128 label: t('conversation_message'),
129 Icon: MessageIcon,
simon21f7d9f2022-11-28 14:21:54 -0500130 onClick: onMessageClick,
idillonef9ab812022-11-18 13:46:24 -0500131 },
132 {
133 label: t('conversation_start_audiocall'),
134 Icon: AudioCallIcon,
135 onClick: () => {
simonff1cb352022-11-24 15:15:26 -0500136 if (conversationId) {
simone35acc22022-12-02 16:51:12 -0500137 startCall({
138 conversationId,
139 role: 'caller',
140 });
simonff1cb352022-11-24 15:15:26 -0500141 }
idillonef9ab812022-11-18 13:46:24 -0500142 },
143 },
144 {
145 label: t('conversation_start_videocall'),
146 Icon: VideoCallIcon,
147 onClick: () => {
simonff1cb352022-11-24 15:15:26 -0500148 if (conversationId) {
simone35acc22022-12-02 16:51:12 -0500149 startCall({
150 conversationId,
151 role: 'caller',
152 withVideoOn: true,
simonff1cb352022-11-24 15:15:26 -0500153 });
154 }
idillonef9ab812022-11-18 13:46:24 -0500155 },
156 },
157 ...(isSelected
158 ? [
159 {
160 label: t('conversation_close'),
161 Icon: CancelIcon,
162 onClick: () => {
163 navigate(`/`);
164 },
165 },
166 ]
167 : []),
168 {
169 label: t('conversation_details'),
idillon07d31cc2022-12-06 22:40:14 -0500170 Icon: PersonIcon,
idillonef9ab812022-11-18 13:46:24 -0500171 onClick: () => {
172 detailsDialogHandler.openDialog();
idillonef9ab812022-11-18 13:46:24 -0500173 },
174 },
175 {
176 label: t('conversation_delete'),
idillon07d31cc2022-12-06 22:40:14 -0500177 Icon: CancelIcon,
idillonef9ab812022-11-18 13:46:24 -0500178 onClick: () => {
idillon07d31cc2022-12-06 22:40:14 -0500179 RemoveConversationDialogHandler.openDialog();
idillonef9ab812022-11-18 13:46:24 -0500180 },
181 },
182 ],
183 [
idillonef9ab812022-11-18 13:46:24 -0500184 navigate,
simon21f7d9f2022-11-28 14:21:54 -0500185 onMessageClick,
idillonef9ab812022-11-18 13:46:24 -0500186 isSelected,
idillonef9ab812022-11-18 13:46:24 -0500187 detailsDialogHandler,
idillon07d31cc2022-12-06 22:40:14 -0500188 RemoveConversationDialogHandler,
idillonef9ab812022-11-18 13:46:24 -0500189 t,
simonff1cb352022-11-24 15:15:26 -0500190 startCall,
191 conversationId,
idillonef9ab812022-11-18 13:46:24 -0500192 ]
193 );
simon575c9402022-10-25 16:21:40 -0400194
idillonef9ab812022-11-18 13:46:24 -0500195 return (
196 <>
197 <ContextMenu {...contextMenuProps} items={menuOptions} />
198
idillon07d31cc2022-12-06 22:40:14 -0500199 <DetailsDialog
200 {...detailsDialogHandler.props}
201 conversationId={conversationId}
202 conversationName={conversationName}
203 isSwarm={isSwarm}
204 />
idillonef9ab812022-11-18 13:46:24 -0500205
idillon07d31cc2022-12-06 22:40:14 -0500206 <RemoveConversationDialog {...RemoveConversationDialogHandler.props} conversationId={conversationId} />
idillonef9ab812022-11-18 13:46:24 -0500207 </>
208 );
209};
210
211interface DetailsDialogProps {
idillon07d31cc2022-12-06 22:40:14 -0500212 conversationId: string;
213 conversationName: string;
idillonef9ab812022-11-18 13:46:24 -0500214 open: boolean;
215 onClose: () => void;
216 isSwarm: boolean;
217}
218
idillon07d31cc2022-12-06 22:40:14 -0500219const DetailsDialog = ({ conversationId, conversationName, open, onClose, isSwarm }: DetailsDialogProps) => {
idillonef9ab812022-11-18 13:46:24 -0500220 const { t } = useTranslation();
221 const items = useMemo(
222 () => [
223 {
idillon07d31cc2022-12-06 22:40:14 -0500224 label: t('conversation_details_name'),
225 value: conversationName,
idillonef9ab812022-11-18 13:46:24 -0500226 },
227 {
228 label: t('conversation_details_identifier'),
idillon07d31cc2022-12-06 22:40:14 -0500229 value: conversationId,
idillonef9ab812022-11-18 13:46:24 -0500230 },
231 {
232 label: t('conversation_details_qr_code'),
idillon07d31cc2022-12-06 22:40:14 -0500233 value: <QRCodeCanvas size={80} value={`${conversationId}`} />,
idillonef9ab812022-11-18 13:46:24 -0500234 },
235 {
236 label: t('conversation_details_is_swarm'),
237 value: isSwarm ? t('conversation_details_is_swarm_true') : t('conversation_details_is_swarm_false'),
238 },
239 ],
idillon07d31cc2022-12-06 22:40:14 -0500240 [conversationId, conversationName, isSwarm, t]
idillonef9ab812022-11-18 13:46:24 -0500241 );
242 return (
243 <InfosDialog
244 open={open}
245 onClose={onClose}
idillon07d31cc2022-12-06 22:40:14 -0500246 icon={<ConversationAvatar sx={{ width: 'inherit', height: 'inherit' }} displayName={conversationName} />}
247 title={conversationName}
idillonef9ab812022-11-18 13:46:24 -0500248 content={<DialogContentList title={t('conversation_details_informations')} items={items} />}
249 />
250 );
251};
252
idillon07d31cc2022-12-06 22:40:14 -0500253interface RemoveConversationDialogProps {
254 conversationId: string;
idillonef9ab812022-11-18 13:46:24 -0500255 open: boolean;
256 onClose: () => void;
257}
258
idillon07d31cc2022-12-06 22:40:14 -0500259const RemoveConversationDialog = ({ conversationId, open, onClose }: RemoveConversationDialogProps) => {
idillonef9ab812022-11-18 13:46:24 -0500260 const { axiosInstance } = useAuthContext();
261 const { t } = useTranslation();
262 const dispatch = useAppDispatch();
263
264 const remove = async () => {
265 const controller = new AbortController();
266 try {
idillon07d31cc2022-12-06 22:40:14 -0500267 await axiosInstance.delete(`/conversations/${conversationId}`, {
idillonef9ab812022-11-18 13:46:24 -0500268 signal: controller.signal,
269 });
270 dispatch(setRefreshFromSlice());
271 } catch (e) {
idillon07d31cc2022-12-06 22:40:14 -0500272 console.error(`Error removing conversation : `, e);
idillonef9ab812022-11-18 13:46:24 -0500273 dispatch(setRefreshFromSlice());
274 }
275 onClose();
276 };
277
278 return (
279 <ConfirmationDialog
280 open={open}
281 onClose={onClose}
282 title={t('dialog_confirm_title_default')}
283 content={t('conversation_ask_confirm_remove')}
284 onConfirm={remove}
285 confirmButtonText={t('conversation_confirm_remove')}
286 />
287 );
288};