blob: 02e856dbc49243dc4fed890edef212f234dbef15 [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 */
idillon847b4642022-12-29 14:28:38 -050018import { Box, Stack, Typography } from '@mui/material';
idillon9e542ca2022-12-15 17:54:07 -050019import dayjs from 'dayjs';
idillon07d31cc2022-12-06 22:40:14 -050020import { IConversationSummary } from 'jami-web-common';
simon575c9402022-10-25 16:21:40 -040021import { QRCodeCanvas } from 'qrcode.react';
simon21f7d9f2022-11-28 14:21:54 -050022import { useCallback, useContext, useMemo, useState } from 'react';
simon4e7445c2022-11-16 21:18:46 -050023import { useTranslation } from 'react-i18next';
simon21f7d9f2022-11-28 14:21:54 -050024import { useNavigate } from 'react-router-dom';
simon575c9402022-10-25 16:21:40 -040025
simon5da8ca62022-11-09 15:21:25 -050026import { useAuthContext } from '../contexts/AuthProvider';
simone35acc22022-12-02 16:51:12 -050027import { CallManagerContext } from '../contexts/CallManagerProvider';
Michelle Sepkap Simec050d9c2022-12-05 09:39:34 -050028import { CallStatus, useCallContext } from '../contexts/CallProvider';
idillon07d31cc2022-12-06 22:40:14 -050029import { useUrlParams } from '../hooks/useUrlParams';
simon575c9402022-10-25 16:21:40 -040030import { setRefreshFromSlice } from '../redux/appSlice';
31import { useAppDispatch } from '../redux/hooks';
idillon07d31cc2022-12-06 22:40:14 -050032import { ConversationRouteParams } from '../router';
idillon9e542ca2022-12-15 17:54:07 -050033import { getMessageCallText, getMessageMemberText } from '../utils/chatmessages';
34import { formatRelativeDate, formatTime } from '../utils/dates&times';
idillonef9ab812022-11-18 13:46:24 -050035import ContextMenu, { ContextMenuHandler, useContextMenuHandler } from './ContextMenu';
simon575c9402022-10-25 16:21:40 -040036import ConversationAvatar from './ConversationAvatar';
idillon847b4642022-12-29 14:28:38 -050037import { CustomListItemButton } from './CustomListItemButton';
idillonef9ab812022-11-18 13:46:24 -050038import { ConfirmationDialog, DialogContentList, InfosDialog, useDialogHandler } from './Dialog';
39import { PopoverListItemData } from './PopoverList';
idillon07d31cc2022-12-06 22:40:14 -050040import { AudioCallIcon, CancelIcon, MessageIcon, PersonIcon, VideoCallIcon } from './SvgIcon';
simon575c9402022-10-25 16:21:40 -040041
simon575c9402022-10-25 16:21:40 -040042type ConversationListItemProps = {
idillon07d31cc2022-12-06 22:40:14 -050043 conversationSummary: IConversationSummary;
simon575c9402022-10-25 16:21:40 -040044};
45
idillon07d31cc2022-12-06 22:40:14 -050046export default function ConversationListItem({ conversationSummary }: ConversationListItemProps) {
idillon9e542ca2022-12-15 17:54:07 -050047 const { account } = useAuthContext();
idillon07d31cc2022-12-06 22:40:14 -050048 const {
49 urlParams: { conversationId: selectedConversationId },
50 } = useUrlParams<ConversationRouteParams>();
idillonef9ab812022-11-18 13:46:24 -050051 const contextMenuHandler = useContextMenuHandler();
simon575c9402022-10-25 16:21:40 -040052 const navigate = useNavigate();
idillon07d31cc2022-12-06 22:40:14 -050053
54 const conversationId = conversationSummary.id;
55 const isSelected = conversationId === selectedConversationId;
simonff1cb352022-11-24 15:15:26 -050056
simon21f7d9f2022-11-28 14:21:54 -050057 const onClick = useCallback(() => {
idillon07d31cc2022-12-06 22:40:14 -050058 if (conversationId) {
59 navigate(`/conversation/${conversationId}`);
simon21f7d9f2022-11-28 14:21:54 -050060 }
idillon07d31cc2022-12-06 22:40:14 -050061 }, [navigate, conversationId]);
simon21f7d9f2022-11-28 14:21:54 -050062
idillon847b4642022-12-29 14:28:38 -050063 const conversationName = useMemo(
64 () => conversationSummary.title || conversationSummary.membersNames.join(', ') || account.getDisplayName(),
65 [account, conversationSummary]
66 );
67
68 return (
69 <Box>
70 <ConversationMenu
71 conversationId={conversationId}
72 conversationName={conversationName}
73 onMessageClick={onClick}
74 isSelected={isSelected}
75 contextMenuProps={contextMenuHandler.props}
76 />
77 <CustomListItemButton
78 selected={isSelected}
79 onClick={onClick}
80 onContextMenu={contextMenuHandler.handleAnchorPosition}
81 icon={<ConversationAvatar displayName={conversationName} />}
82 primaryText={<Typography variant="body1">{conversationName}</Typography>}
83 secondaryText={<SecondaryText conversationSummary={conversationSummary} isSelected={isSelected} />}
84 />
85 </Box>
86 );
87}
88
89type SecondaryTextProps = {
90 conversationSummary: IConversationSummary;
91 isSelected: boolean;
92};
93
94const SecondaryText = ({ conversationSummary, isSelected }: SecondaryTextProps) => {
95 const { account } = useAuthContext();
96 const callContext = useCallContext(true);
97 const { callData } = useContext(CallManagerContext);
98 const { t, i18n } = useTranslation();
99
idillon9e542ca2022-12-15 17:54:07 -0500100 const timeIndicator = useMemo(() => {
101 const message = conversationSummary.lastMessage;
102 const time = dayjs.unix(Number(message.timestamp));
103 if (time.isToday()) {
104 return formatTime(time, i18n);
105 } else {
106 return formatRelativeDate(time, i18n);
107 }
108 }, [conversationSummary, i18n]);
109
110 const lastMessageText = useMemo(() => {
idillon07d31cc2022-12-06 22:40:14 -0500111 if (!callContext || !callData || callData.conversationId !== conversationSummary.id) {
idillon9e542ca2022-12-15 17:54:07 -0500112 const message = conversationSummary.lastMessage;
113 switch (message.type) {
114 case 'initial': {
115 return t('message_swarm_created');
116 }
117 case 'application/data-transfer+json': {
118 return message.fileId;
119 }
120 case 'application/call-history+json': {
121 const isAccountMessage = message.author === account.getUri();
122 return getMessageCallText(isAccountMessage, message, i18n);
123 }
124 case 'member': {
125 return getMessageMemberText(message, i18n);
126 }
127 case 'text/plain': {
128 return message.body;
129 }
130 default: {
131 console.error(`${ConversationListItem.name} received an unexpected lastMessage type: ${message.type}`);
132 return '';
133 }
134 }
Michelle Sepkap Simec050d9c2022-12-05 09:39:34 -0500135 }
136
137 if (callContext.callStatus === CallStatus.InCall) {
138 return callContext.isAudioOn ? t('ongoing_call_unmuted') : t('ongoing_call_muted');
139 }
140
141 if (callContext.callStatus === CallStatus.Connecting) {
142 return t('connecting_call');
143 }
144
145 return callContext.callRole === 'caller' ? t('outgoing_call') : t('incoming_call');
idillon9e542ca2022-12-15 17:54:07 -0500146 }, [account, conversationSummary, callContext, callData, t, i18n]);
idillon07d31cc2022-12-06 22:40:14 -0500147
idillonef9ab812022-11-18 13:46:24 -0500148 return (
idillon847b4642022-12-29 14:28:38 -0500149 <Stack direction="row" spacing="5px">
150 <Typography variant="body2" fontWeight={isSelected ? 'bold' : 'normal'}>
151 {timeIndicator}
152 </Typography>
153 <Typography variant="body2">{lastMessageText}</Typography>
154 </Stack>
idillonef9ab812022-11-18 13:46:24 -0500155 );
idillon847b4642022-12-29 14:28:38 -0500156};
idillonef9ab812022-11-18 13:46:24 -0500157
158interface ConversationMenuProps {
idillon07d31cc2022-12-06 22:40:14 -0500159 conversationId: string;
160 conversationName: string;
simon21f7d9f2022-11-28 14:21:54 -0500161 onMessageClick: () => void;
idillonef9ab812022-11-18 13:46:24 -0500162 isSelected: boolean;
163 contextMenuProps: ContextMenuHandler['props'];
164}
165
simon21f7d9f2022-11-28 14:21:54 -0500166const ConversationMenu = ({
idillon07d31cc2022-12-06 22:40:14 -0500167 conversationId,
168 conversationName,
simon21f7d9f2022-11-28 14:21:54 -0500169 onMessageClick,
170 isSelected,
171 contextMenuProps,
172}: ConversationMenuProps) => {
idillonef9ab812022-11-18 13:46:24 -0500173 const { t } = useTranslation();
simone35acc22022-12-02 16:51:12 -0500174 const { startCall } = useContext(CallManagerContext);
simon416d0792022-11-03 02:46:18 -0400175 const [isSwarm] = useState(true);
simon575c9402022-10-25 16:21:40 -0400176
idillonef9ab812022-11-18 13:46:24 -0500177 const detailsDialogHandler = useDialogHandler();
idillon07d31cc2022-12-06 22:40:14 -0500178 const RemoveConversationDialogHandler = useDialogHandler();
simon575c9402022-10-25 16:21:40 -0400179
idillonef9ab812022-11-18 13:46:24 -0500180 const navigate = useNavigate();
181
idillonef9ab812022-11-18 13:46:24 -0500182 const menuOptions: PopoverListItemData[] = useMemo(
183 () => [
184 {
185 label: t('conversation_message'),
186 Icon: MessageIcon,
simon21f7d9f2022-11-28 14:21:54 -0500187 onClick: onMessageClick,
idillonef9ab812022-11-18 13:46:24 -0500188 },
189 {
190 label: t('conversation_start_audiocall'),
191 Icon: AudioCallIcon,
192 onClick: () => {
simonff1cb352022-11-24 15:15:26 -0500193 if (conversationId) {
simone35acc22022-12-02 16:51:12 -0500194 startCall({
195 conversationId,
196 role: 'caller',
197 });
simonff1cb352022-11-24 15:15:26 -0500198 }
idillonef9ab812022-11-18 13:46:24 -0500199 },
200 },
201 {
202 label: t('conversation_start_videocall'),
203 Icon: VideoCallIcon,
204 onClick: () => {
simonff1cb352022-11-24 15:15:26 -0500205 if (conversationId) {
simone35acc22022-12-02 16:51:12 -0500206 startCall({
207 conversationId,
208 role: 'caller',
209 withVideoOn: true,
simonff1cb352022-11-24 15:15:26 -0500210 });
211 }
idillonef9ab812022-11-18 13:46:24 -0500212 },
213 },
214 ...(isSelected
215 ? [
216 {
217 label: t('conversation_close'),
218 Icon: CancelIcon,
219 onClick: () => {
220 navigate(`/`);
221 },
222 },
223 ]
224 : []),
225 {
226 label: t('conversation_details'),
idillon07d31cc2022-12-06 22:40:14 -0500227 Icon: PersonIcon,
idillonef9ab812022-11-18 13:46:24 -0500228 onClick: () => {
229 detailsDialogHandler.openDialog();
idillonef9ab812022-11-18 13:46:24 -0500230 },
231 },
232 {
233 label: t('conversation_delete'),
idillon07d31cc2022-12-06 22:40:14 -0500234 Icon: CancelIcon,
idillonef9ab812022-11-18 13:46:24 -0500235 onClick: () => {
idillon07d31cc2022-12-06 22:40:14 -0500236 RemoveConversationDialogHandler.openDialog();
idillonef9ab812022-11-18 13:46:24 -0500237 },
238 },
239 ],
240 [
idillonef9ab812022-11-18 13:46:24 -0500241 navigate,
simon21f7d9f2022-11-28 14:21:54 -0500242 onMessageClick,
idillonef9ab812022-11-18 13:46:24 -0500243 isSelected,
idillonef9ab812022-11-18 13:46:24 -0500244 detailsDialogHandler,
idillon07d31cc2022-12-06 22:40:14 -0500245 RemoveConversationDialogHandler,
idillonef9ab812022-11-18 13:46:24 -0500246 t,
simonff1cb352022-11-24 15:15:26 -0500247 startCall,
248 conversationId,
idillonef9ab812022-11-18 13:46:24 -0500249 ]
250 );
simon575c9402022-10-25 16:21:40 -0400251
idillonef9ab812022-11-18 13:46:24 -0500252 return (
253 <>
254 <ContextMenu {...contextMenuProps} items={menuOptions} />
255
idillon07d31cc2022-12-06 22:40:14 -0500256 <DetailsDialog
257 {...detailsDialogHandler.props}
258 conversationId={conversationId}
259 conversationName={conversationName}
260 isSwarm={isSwarm}
261 />
idillonef9ab812022-11-18 13:46:24 -0500262
idillon07d31cc2022-12-06 22:40:14 -0500263 <RemoveConversationDialog {...RemoveConversationDialogHandler.props} conversationId={conversationId} />
idillonef9ab812022-11-18 13:46:24 -0500264 </>
265 );
266};
267
268interface DetailsDialogProps {
idillon07d31cc2022-12-06 22:40:14 -0500269 conversationId: string;
270 conversationName: string;
idillonef9ab812022-11-18 13:46:24 -0500271 open: boolean;
272 onClose: () => void;
273 isSwarm: boolean;
274}
275
idillon07d31cc2022-12-06 22:40:14 -0500276const DetailsDialog = ({ conversationId, conversationName, open, onClose, isSwarm }: DetailsDialogProps) => {
idillonef9ab812022-11-18 13:46:24 -0500277 const { t } = useTranslation();
278 const items = useMemo(
279 () => [
280 {
idillon07d31cc2022-12-06 22:40:14 -0500281 label: t('conversation_details_name'),
282 value: conversationName,
idillonef9ab812022-11-18 13:46:24 -0500283 },
284 {
285 label: t('conversation_details_identifier'),
idillon07d31cc2022-12-06 22:40:14 -0500286 value: conversationId,
idillonef9ab812022-11-18 13:46:24 -0500287 },
288 {
289 label: t('conversation_details_qr_code'),
idillon07d31cc2022-12-06 22:40:14 -0500290 value: <QRCodeCanvas size={80} value={`${conversationId}`} />,
idillonef9ab812022-11-18 13:46:24 -0500291 },
292 {
293 label: t('conversation_details_is_swarm'),
294 value: isSwarm ? t('conversation_details_is_swarm_true') : t('conversation_details_is_swarm_false'),
295 },
296 ],
idillon07d31cc2022-12-06 22:40:14 -0500297 [conversationId, conversationName, isSwarm, t]
idillonef9ab812022-11-18 13:46:24 -0500298 );
299 return (
300 <InfosDialog
301 open={open}
302 onClose={onClose}
idillon07d31cc2022-12-06 22:40:14 -0500303 icon={<ConversationAvatar sx={{ width: 'inherit', height: 'inherit' }} displayName={conversationName} />}
304 title={conversationName}
idillonef9ab812022-11-18 13:46:24 -0500305 content={<DialogContentList title={t('conversation_details_informations')} items={items} />}
306 />
307 );
308};
309
idillon07d31cc2022-12-06 22:40:14 -0500310interface RemoveConversationDialogProps {
311 conversationId: string;
idillonef9ab812022-11-18 13:46:24 -0500312 open: boolean;
313 onClose: () => void;
314}
315
idillon07d31cc2022-12-06 22:40:14 -0500316const RemoveConversationDialog = ({ conversationId, open, onClose }: RemoveConversationDialogProps) => {
idillonef9ab812022-11-18 13:46:24 -0500317 const { axiosInstance } = useAuthContext();
318 const { t } = useTranslation();
319 const dispatch = useAppDispatch();
320
321 const remove = async () => {
322 const controller = new AbortController();
323 try {
idillon07d31cc2022-12-06 22:40:14 -0500324 await axiosInstance.delete(`/conversations/${conversationId}`, {
idillonef9ab812022-11-18 13:46:24 -0500325 signal: controller.signal,
326 });
327 dispatch(setRefreshFromSlice());
328 } catch (e) {
idillon07d31cc2022-12-06 22:40:14 -0500329 console.error(`Error removing conversation : `, e);
idillonef9ab812022-11-18 13:46:24 -0500330 dispatch(setRefreshFromSlice());
331 }
332 onClose();
333 };
334
335 return (
336 <ConfirmationDialog
337 open={open}
338 onClose={onClose}
339 title={t('dialog_confirm_title_default')}
340 content={t('conversation_ask_confirm_remove')}
341 onConfirm={remove}
342 confirmButtonText={t('conversation_confirm_remove')}
343 />
344 );
345};