blob: d5980c6c1e2e16dc451d0dc6819e146ab892b2f8 [file] [log] [blame]
simon26e79f72022-10-05 22:16:08 -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 */
simond47ef9e2022-09-28 22:24:28 -040018import { Box, Chip, Divider, List, ListItemButton, ListItemText, Stack, Tooltip, Typography } from '@mui/material';
19import { styled } from '@mui/material/styles';
20import dayjs from 'dayjs';
21import isToday from 'dayjs/plugin/isToday';
22import isYesterday from 'dayjs/plugin/isYesterday';
23import React, { useCallback, useMemo, useState } from 'react';
24import { useTranslation } from 'react-i18next';
simon07b4eb02022-09-29 17:50:26 -040025
simon35378692022-10-02 23:25:57 -040026import { EmojiButton, MoreButton, ReplyMessageButton } from './Button.tsx';
simond47ef9e2022-09-28 22:24:28 -040027import ConversationAvatar from './ConversationAvatar';
simon35378692022-10-02 23:25:57 -040028import { OppositeArrowsIcon, TrashBinIcon, TwoSheetsIcon } from './SvgIcon.tsx';
Larbi Gharibe9af9732021-03-31 15:08:01 +010029
simond47ef9e2022-09-28 22:24:28 -040030dayjs.extend(isToday);
31dayjs.extend(isYesterday);
idillonbef18a52022-09-01 01:51:40 -040032
33export const MessageCall = (props) => {
simon80b7b3b2022-09-28 17:50:10 -040034 return <Stack alignItems="center">&quot;Appel&quot;</Stack>;
simond47ef9e2022-09-28 22:24:28 -040035};
Larbi Gharibe9af9732021-03-31 15:08:01 +010036
idillonbef18a52022-09-01 01:51:40 -040037export const MessageInitial = (props) => {
simond47ef9e2022-09-28 22:24:28 -040038 const { t } = useTranslation();
39 return <Stack alignItems="center">{t('message_swarm_created')}</Stack>;
40};
idillonbef18a52022-09-01 01:51:40 -040041
42export const MessageDataTransfer = (props) => {
43 return (
44 <MessageBubble
simond47ef9e2022-09-28 22:24:28 -040045 backgroundColor={'#E5E5E5'}
idillonbef18a52022-09-01 01:51:40 -040046 position={props.position}
47 isFirstOfGroup={props.isFirstOfGroup}
48 isLastOfGroup={props.isLastOfGroup}
49 >
simon80b7b3b2022-09-28 17:50:10 -040050 &quot;data-transfer&quot;
idillonbef18a52022-09-01 01:51:40 -040051 </MessageBubble>
simond47ef9e2022-09-28 22:24:28 -040052 );
53};
idillonbef18a52022-09-01 01:51:40 -040054
55export const MessageMember = (props) => {
simond47ef9e2022-09-28 22:24:28 -040056 const { t } = useTranslation();
idillonbef18a52022-09-01 01:51:40 -040057 return (
simond47ef9e2022-09-28 22:24:28 -040058 <Stack alignItems="center">
idillonbef18a52022-09-01 01:51:40 -040059 <Chip
60 sx={{
simond47ef9e2022-09-28 22:24:28 -040061 width: 'fit-content',
idillonbef18a52022-09-01 01:51:40 -040062 }}
simond47ef9e2022-09-28 22:24:28 -040063 label={t('message_user_joined', { user: props.message.author })}
idillonbef18a52022-09-01 01:51:40 -040064 />
65 </Stack>
simond47ef9e2022-09-28 22:24:28 -040066 );
67};
idillonbef18a52022-09-01 01:51:40 -040068
69export const MessageMerge = (props) => {
simon80b7b3b2022-09-28 17:50:10 -040070 return <Stack alignItems="center">&quot;merge&quot;</Stack>;
simond47ef9e2022-09-28 22:24:28 -040071};
idillonbef18a52022-09-01 01:51:40 -040072
73export const MessageText = (props) => {
74 return (
75 <MessageBubble
76 backgroundColor={props.bubbleColor}
77 position={props.position}
78 isFirstOfGroup={props.isFirstOfGroup}
79 isLastOfGroup={props.isLastOfGroup}
80 >
idillon89720a82022-09-06 18:47:05 -040081 <Typography variant="body1" color={props.textColor} textAlign={props.position}>
idillonbef18a52022-09-01 01:51:40 -040082 {props.message.body}
83 </Typography>
84 </MessageBubble>
simond47ef9e2022-09-28 22:24:28 -040085 );
86};
idillonbef18a52022-09-01 01:51:40 -040087
simond47ef9e2022-09-28 22:24:28 -040088export const MessageDate = ({ time }) => {
89 let textDate;
idillonbef18a52022-09-01 01:51:40 -040090
91 if (time.isToday()) {
simond47ef9e2022-09-28 22:24:28 -040092 textDate = 'Today';
93 } else if (time.isYesterday()) {
94 textDate = 'Yesterday';
95 } else {
96 const date = time.date().toString().padStart(2, '0');
97 const month = (time.month() + 1).toString().padStart(2, '0');
98 textDate = `${date}/${month}/${time.year()}`;
idillonbef18a52022-09-01 01:51:40 -040099 }
100
101 return (
simond47ef9e2022-09-28 22:24:28 -0400102 <Box marginTop="30px">
idillon04245a12022-09-01 11:12:17 -0400103 <Divider
104 sx={{
simond47ef9e2022-09-28 22:24:28 -0400105 '.MuiDivider-wrapper': {
idillon04245a12022-09-01 11:12:17 -0400106 margin: 0,
107 padding: 0,
108 },
simond47ef9e2022-09-28 22:24:28 -0400109 '&::before': {
110 borderTop: '1px solid #E5E5E5',
idillon04245a12022-09-01 11:12:17 -0400111 },
simond47ef9e2022-09-28 22:24:28 -0400112 '&::after': {
113 borderTop: '1px solid #E5E5E5',
idillon04245a12022-09-01 11:12:17 -0400114 },
115 }}
116 >
117 <Typography
118 variant="caption"
119 fontWeight={700}
120 border="1px solid #E5E5E5"
121 borderRadius="5px"
122 padding="10px 16px"
123 >
124 {textDate}
125 </Typography>
idillonbef18a52022-09-01 01:51:40 -0400126 </Divider>
127 </Box>
simond47ef9e2022-09-28 22:24:28 -0400128 );
129};
idillonbef18a52022-09-01 01:51:40 -0400130
simond47ef9e2022-09-28 22:24:28 -0400131export const MessageTime = ({ time, hasDateOnTop }) => {
132 const hour = time.hour().toString().padStart(2, '0');
133 const minute = time.minute().toString().padStart(2, '0');
134 const textTime = `${hour}:${minute}`;
idillonbef18a52022-09-01 01:51:40 -0400135
136 return (
simond47ef9e2022-09-28 22:24:28 -0400137 <Stack direction="row" justifyContent="center" margin="30px" marginTop={hasDateOnTop ? '20px' : '30px'}>
138 <Typography variant="caption" color="#A7A7A7" fontWeight={700}>
idillonbef18a52022-09-01 01:51:40 -0400139 {textTime}
140 </Typography>
141 </Stack>
simond47ef9e2022-09-28 22:24:28 -0400142 );
143};
idillonbef18a52022-09-01 01:51:40 -0400144
145export const MessageBubblesGroup = (props) => {
simond47ef9e2022-09-28 22:24:28 -0400146 const isUser = false; // should access user from the store
147 const position = isUser ? 'end' : 'start';
148 const bubbleColor = isUser ? '#005699' : '#E5E5E5';
149 const textColor = isUser ? 'white' : 'black';
idillonbef18a52022-09-01 01:51:40 -0400150
151 return (
idillon89720a82022-09-06 18:47:05 -0400152 <Stack // Row for a group of message bubbles with the user's infos
idillonbef18a52022-09-01 01:51:40 -0400153 direction="row"
154 justifyContent={position}
idillon89720a82022-09-06 18:47:05 -0400155 alignItems="end"
156 spacing="10px"
idillonbef18a52022-09-01 01:51:40 -0400157 >
simond47ef9e2022-09-28 22:24:28 -0400158 {!isUser && (
159 <ConversationAvatar displayName="TempDisplayName" sx={{ width: '22px', height: '22px', fontSize: '15px' }} />
160 )}
idillon89720a82022-09-06 18:47:05 -0400161 <Stack // Container to align the bubbles to the same side of a row
idillonbef18a52022-09-01 01:51:40 -0400162 width="66.66%"
idillonbef18a52022-09-01 01:51:40 -0400163 alignItems={position}
164 >
simond47ef9e2022-09-28 22:24:28 -0400165 <ParticipantName name={props.messages[0]?.author} position={position} />
idillon89720a82022-09-06 18:47:05 -0400166 <Stack // Container for a group of message bubbles
idillonbef18a52022-09-01 01:51:40 -0400167 spacing="6px"
168 alignItems={position}
169 direction="column-reverse"
170 >
simond47ef9e2022-09-28 22:24:28 -0400171 {props.messages.map((message, index) => {
172 let Component;
173 switch (message.type) {
174 case 'text/plain':
175 Component = MessageText;
176 break;
177 case 'application/data-transfer+json':
178 Component = MessageDataTransfer;
179 break;
idillonbef18a52022-09-01 01:51:40 -0400180 }
simond47ef9e2022-09-28 22:24:28 -0400181 return (
182 <Component // Single message
183 key={message.id}
184 message={message}
185 textColor={textColor}
186 position={position}
187 bubbleColor={bubbleColor}
188 isFirstOfGroup={index == props.messages.length - 1}
189 isLastOfGroup={index == 0}
190 />
191 );
192 })}
idillonbef18a52022-09-01 01:51:40 -0400193 </Stack>
194 </Stack>
195 </Stack>
simond47ef9e2022-09-28 22:24:28 -0400196 );
197};
idillonbef18a52022-09-01 01:51:40 -0400198
idillon927b7592022-09-15 12:56:45 -0400199const MessageTooltip = styled(({ className, ...props }) => {
simond47ef9e2022-09-28 22:24:28 -0400200 const [open, setOpen] = useState(false);
201 const emojis = ['😎', '😄', '😍']; // Should be last three used emojis
idillon927b7592022-09-15 12:56:45 -0400202 const additionalOptions = [
203 {
204 Icon: TwoSheetsIcon,
simond47ef9e2022-09-28 22:24:28 -0400205 text: 'Copy',
idillon927b7592022-09-15 12:56:45 -0400206 action: () => {},
207 },
208 {
209 Icon: OppositeArrowsIcon,
simond47ef9e2022-09-28 22:24:28 -0400210 text: 'Transfer',
idillon927b7592022-09-15 12:56:45 -0400211 action: () => {},
212 },
213 {
214 Icon: TrashBinIcon,
simond47ef9e2022-09-28 22:24:28 -0400215 text: 'Delete message',
idillon927b7592022-09-15 12:56:45 -0400216 action: () => {},
217 },
simond47ef9e2022-09-28 22:24:28 -0400218 ];
idillon927b7592022-09-15 12:56:45 -0400219
simond47ef9e2022-09-28 22:24:28 -0400220 const toggleMoreMenu = useCallback(() => setOpen((open) => !open), [setOpen]);
idillon927b7592022-09-15 12:56:45 -0400221
simond47ef9e2022-09-28 22:24:28 -0400222 const onClose = useCallback(() => {
223 setOpen(false);
224 }, [setOpen]);
idillon927b7592022-09-15 12:56:45 -0400225
226 return (
227 <Tooltip
228 {...props}
229 classes={{ tooltip: className }} // Required for styles. Don't know why
simond47ef9e2022-09-28 22:24:28 -0400230 placement={props.position == 'start' ? 'right-start' : 'left-start'}
idillon927b7592022-09-15 12:56:45 -0400231 PopperProps={{
232 modifiers: [
233 {
simond47ef9e2022-09-28 22:24:28 -0400234 name: 'offset',
idillon927b7592022-09-15 12:56:45 -0400235 options: {
simond47ef9e2022-09-28 22:24:28 -0400236 offset: [-2, -30],
idillon927b7592022-09-15 12:56:45 -0400237 },
238 },
239 ],
240 }}
241 onClose={onClose}
242 title={
simond47ef9e2022-09-28 22:24:28 -0400243 <Stack>
244 {' '}
245 {/* Whole tooltip's content */}
idillon927b7592022-09-15 12:56:45 -0400246 <Stack // Main options
247 direction="row"
248 spacing="16px"
249 >
simond47ef9e2022-09-28 22:24:28 -0400250 {emojis.map((emoji) => (
251 <EmojiButton key={emoji} emoji={emoji} />
252 ))}
253 <ReplyMessageButton />
254 <MoreButton onClick={toggleMoreMenu} />
idillon927b7592022-09-15 12:56:45 -0400255 </Stack>
simond47ef9e2022-09-28 22:24:28 -0400256 {open && ( // Additional menu options
idillon927b7592022-09-15 12:56:45 -0400257 <>
simond47ef9e2022-09-28 22:24:28 -0400258 <Divider sx={{ paddingTop: '16px' }} />
259 <List sx={{ padding: 0, paddingTop: '8px', marginBottom: '-8px' }}>
260 {additionalOptions.map((option) => (
261 <ListItemButton
262 key={option.text}
263 sx={{
264 padding: '8px',
265 }}
266 >
267 <Stack // Could not find proper way to set spacing between ListItemIcon and ListItemText
268 direction="row"
269 spacing="16px"
idillon927b7592022-09-15 12:56:45 -0400270 >
simond47ef9e2022-09-28 22:24:28 -0400271 <option.Icon
272 sx={{
273 height: '16px',
274 margin: 0,
275 color: (theme) => theme.palette.primary.dark,
276 }}
277 />
278 <ListItemText
279 primary={option.text}
280 primaryTypographyProps={{
281 fontSize: '12px',
282 lineHeight: '16px',
283 }}
284 sx={{
285 height: '16px',
286 margin: 0,
287 }}
288 />
289 </Stack>
290 </ListItemButton>
291 ))}
292 </List>
idillon927b7592022-09-15 12:56:45 -0400293 </>
simond47ef9e2022-09-28 22:24:28 -0400294 )}
idillon927b7592022-09-15 12:56:45 -0400295 </Stack>
296 }
297 />
simond47ef9e2022-09-28 22:24:28 -0400298 );
idillon927b7592022-09-15 12:56:45 -0400299})(({ theme, position }) => {
simond47ef9e2022-09-28 22:24:28 -0400300 const largeRadius = '20px';
301 const smallRadius = '5px';
idillon927b7592022-09-15 12:56:45 -0400302 return {
simond47ef9e2022-09-28 22:24:28 -0400303 backgroundColor: 'white',
304 padding: '16px',
305 boxShadow: '3px 3px 7px #00000029',
idillon927b7592022-09-15 12:56:45 -0400306 borderRadius: largeRadius,
simond47ef9e2022-09-28 22:24:28 -0400307 borderStartStartRadius: position == 'start' ? smallRadius : largeRadius,
308 borderStartEndRadius: position == 'end' ? smallRadius : largeRadius,
309 };
idillon927b7592022-09-15 12:56:45 -0400310});
311
idillonbef18a52022-09-01 01:51:40 -0400312const MessageBubble = (props) => {
simond47ef9e2022-09-28 22:24:28 -0400313 const largeRadius = '20px';
314 const smallRadius = '5px';
Adrien Béraud023f7cf2022-09-18 14:57:53 -0400315 const radius = useMemo(() => {
simond47ef9e2022-09-28 22:24:28 -0400316 if (props.position == 'start') {
idillonbef18a52022-09-01 01:51:40 -0400317 return {
318 borderStartStartRadius: props.isFirstOfGroup ? largeRadius : smallRadius,
319 borderStartEndRadius: largeRadius,
320 borderEndStartRadius: props.isLastOfGroup ? largeRadius : smallRadius,
321 borderEndEndRadius: largeRadius,
simond47ef9e2022-09-28 22:24:28 -0400322 };
idillonbef18a52022-09-01 01:51:40 -0400323 }
324 return {
325 borderStartStartRadius: largeRadius,
326 borderStartEndRadius: props.isFirstOfGroup ? largeRadius : smallRadius,
327 borderEndStartRadius: largeRadius,
328 borderEndEndRadius: props.isLastOfGroup ? largeRadius : smallRadius,
simond47ef9e2022-09-28 22:24:28 -0400329 };
330 }, [props.isFirstOfGroup, props.isLastOfGroup, props.position]);
idillonbef18a52022-09-01 01:51:40 -0400331
332 return (
simond47ef9e2022-09-28 22:24:28 -0400333 <MessageTooltip position={props.position}>
idillon927b7592022-09-15 12:56:45 -0400334 <Box
335 sx={{
simond47ef9e2022-09-28 22:24:28 -0400336 width: 'fit-content',
idillon927b7592022-09-15 12:56:45 -0400337 backgroundColor: props.backgroundColor,
simond47ef9e2022-09-28 22:24:28 -0400338 padding: '16px',
idillon927b7592022-09-15 12:56:45 -0400339 ...radius,
340 }}
341 >
342 {props.children}
343 </Box>
344 </MessageTooltip>
simond47ef9e2022-09-28 22:24:28 -0400345 );
346};
idillonbef18a52022-09-01 01:51:40 -0400347
348const ParticipantName = (props) => {
349 return (
simond47ef9e2022-09-28 22:24:28 -0400350 <Box marginBottom="6px" marginLeft="16px" marginRight="16px">
351 <Typography variant="caption" color="#A7A7A7" fontWeight={700}>
idillonbef18a52022-09-01 01:51:40 -0400352 {props.name}
353 </Typography>
354 </Box>
simond47ef9e2022-09-28 22:24:28 -0400355 );
356};