blob: 7e14d1078d57f5ff1f2cdfced9d6614e4b8888dc [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';
idillonae655dd2022-10-14 18:11:02 -040023import { useCallback, useMemo, useState } from 'react';
simond47ef9e2022-09-28 22:24:28 -040024import { 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) => {
idillonae655dd2022-10-14 18:11:02 -0400146 const isUser = props.messages[0]?.author === props.account.getUri();
simond47ef9e2022-09-28 22:24:28 -0400147 const position = isUser ? 'end' : 'start';
148 const bubbleColor = isUser ? '#005699' : '#E5E5E5';
149 const textColor = isUser ? 'white' : 'black';
idillonbef18a52022-09-01 01:51:40 -0400150
idillonae655dd2022-10-14 18:11:02 -0400151 let authorName;
152 if (isUser) {
153 authorName = props.account.getDisplayName();
154 } else {
155 const member = props.members.find((member) => props.messages[0]?.author === member.contact.getUri());
156 const contact = member.contact;
157 authorName = contact.getDisplayName();
158 }
159
idillonbef18a52022-09-01 01:51:40 -0400160 return (
idillon89720a82022-09-06 18:47:05 -0400161 <Stack // Row for a group of message bubbles with the user's infos
idillonbef18a52022-09-01 01:51:40 -0400162 direction="row"
163 justifyContent={position}
idillon89720a82022-09-06 18:47:05 -0400164 alignItems="end"
165 spacing="10px"
idillonbef18a52022-09-01 01:51:40 -0400166 >
simond47ef9e2022-09-28 22:24:28 -0400167 {!isUser && (
idillonae655dd2022-10-14 18:11:02 -0400168 <ConversationAvatar displayName={authorName} sx={{ width: '22px', height: '22px', fontSize: '15px' }} />
simond47ef9e2022-09-28 22:24:28 -0400169 )}
idillon89720a82022-09-06 18:47:05 -0400170 <Stack // Container to align the bubbles to the same side of a row
idillonbef18a52022-09-01 01:51:40 -0400171 width="66.66%"
idillonbef18a52022-09-01 01:51:40 -0400172 alignItems={position}
173 >
idillonae655dd2022-10-14 18:11:02 -0400174 <ParticipantName name={authorName} position={position} />
idillon89720a82022-09-06 18:47:05 -0400175 <Stack // Container for a group of message bubbles
idillonbef18a52022-09-01 01:51:40 -0400176 spacing="6px"
177 alignItems={position}
178 direction="column-reverse"
179 >
simond47ef9e2022-09-28 22:24:28 -0400180 {props.messages.map((message, index) => {
181 let Component;
182 switch (message.type) {
183 case 'text/plain':
184 Component = MessageText;
185 break;
186 case 'application/data-transfer+json':
187 Component = MessageDataTransfer;
188 break;
idillonbef18a52022-09-01 01:51:40 -0400189 }
simond47ef9e2022-09-28 22:24:28 -0400190 return (
191 <Component // Single message
192 key={message.id}
193 message={message}
194 textColor={textColor}
195 position={position}
196 bubbleColor={bubbleColor}
197 isFirstOfGroup={index == props.messages.length - 1}
198 isLastOfGroup={index == 0}
199 />
200 );
201 })}
idillonbef18a52022-09-01 01:51:40 -0400202 </Stack>
203 </Stack>
204 </Stack>
simond47ef9e2022-09-28 22:24:28 -0400205 );
206};
idillonbef18a52022-09-01 01:51:40 -0400207
idillon927b7592022-09-15 12:56:45 -0400208const MessageTooltip = styled(({ className, ...props }) => {
simond47ef9e2022-09-28 22:24:28 -0400209 const [open, setOpen] = useState(false);
210 const emojis = ['😎', '😄', '😍']; // Should be last three used emojis
idillon927b7592022-09-15 12:56:45 -0400211 const additionalOptions = [
212 {
213 Icon: TwoSheetsIcon,
simond47ef9e2022-09-28 22:24:28 -0400214 text: 'Copy',
idillon927b7592022-09-15 12:56:45 -0400215 action: () => {},
216 },
217 {
218 Icon: OppositeArrowsIcon,
simond47ef9e2022-09-28 22:24:28 -0400219 text: 'Transfer',
idillon927b7592022-09-15 12:56:45 -0400220 action: () => {},
221 },
222 {
223 Icon: TrashBinIcon,
simond47ef9e2022-09-28 22:24:28 -0400224 text: 'Delete message',
idillon927b7592022-09-15 12:56:45 -0400225 action: () => {},
226 },
simond47ef9e2022-09-28 22:24:28 -0400227 ];
idillon927b7592022-09-15 12:56:45 -0400228
simond47ef9e2022-09-28 22:24:28 -0400229 const toggleMoreMenu = useCallback(() => setOpen((open) => !open), [setOpen]);
idillon927b7592022-09-15 12:56:45 -0400230
simond47ef9e2022-09-28 22:24:28 -0400231 const onClose = useCallback(() => {
232 setOpen(false);
233 }, [setOpen]);
idillon927b7592022-09-15 12:56:45 -0400234
235 return (
236 <Tooltip
237 {...props}
238 classes={{ tooltip: className }} // Required for styles. Don't know why
simond47ef9e2022-09-28 22:24:28 -0400239 placement={props.position == 'start' ? 'right-start' : 'left-start'}
idillon927b7592022-09-15 12:56:45 -0400240 PopperProps={{
241 modifiers: [
242 {
simond47ef9e2022-09-28 22:24:28 -0400243 name: 'offset',
idillon927b7592022-09-15 12:56:45 -0400244 options: {
simond47ef9e2022-09-28 22:24:28 -0400245 offset: [-2, -30],
idillon927b7592022-09-15 12:56:45 -0400246 },
247 },
248 ],
249 }}
250 onClose={onClose}
251 title={
simond47ef9e2022-09-28 22:24:28 -0400252 <Stack>
253 {' '}
254 {/* Whole tooltip's content */}
idillon927b7592022-09-15 12:56:45 -0400255 <Stack // Main options
256 direction="row"
257 spacing="16px"
258 >
simond47ef9e2022-09-28 22:24:28 -0400259 {emojis.map((emoji) => (
260 <EmojiButton key={emoji} emoji={emoji} />
261 ))}
262 <ReplyMessageButton />
263 <MoreButton onClick={toggleMoreMenu} />
idillon927b7592022-09-15 12:56:45 -0400264 </Stack>
simond47ef9e2022-09-28 22:24:28 -0400265 {open && ( // Additional menu options
idillon927b7592022-09-15 12:56:45 -0400266 <>
simond47ef9e2022-09-28 22:24:28 -0400267 <Divider sx={{ paddingTop: '16px' }} />
268 <List sx={{ padding: 0, paddingTop: '8px', marginBottom: '-8px' }}>
269 {additionalOptions.map((option) => (
270 <ListItemButton
271 key={option.text}
272 sx={{
273 padding: '8px',
274 }}
275 >
276 <Stack // Could not find proper way to set spacing between ListItemIcon and ListItemText
277 direction="row"
278 spacing="16px"
idillon927b7592022-09-15 12:56:45 -0400279 >
simond47ef9e2022-09-28 22:24:28 -0400280 <option.Icon
281 sx={{
282 height: '16px',
283 margin: 0,
284 color: (theme) => theme.palette.primary.dark,
285 }}
286 />
287 <ListItemText
288 primary={option.text}
289 primaryTypographyProps={{
290 fontSize: '12px',
291 lineHeight: '16px',
292 }}
293 sx={{
294 height: '16px',
295 margin: 0,
296 }}
297 />
298 </Stack>
299 </ListItemButton>
300 ))}
301 </List>
idillon927b7592022-09-15 12:56:45 -0400302 </>
simond47ef9e2022-09-28 22:24:28 -0400303 )}
idillon927b7592022-09-15 12:56:45 -0400304 </Stack>
305 }
306 />
simond47ef9e2022-09-28 22:24:28 -0400307 );
idillon927b7592022-09-15 12:56:45 -0400308})(({ theme, position }) => {
simond47ef9e2022-09-28 22:24:28 -0400309 const largeRadius = '20px';
310 const smallRadius = '5px';
idillon927b7592022-09-15 12:56:45 -0400311 return {
simond47ef9e2022-09-28 22:24:28 -0400312 backgroundColor: 'white',
313 padding: '16px',
314 boxShadow: '3px 3px 7px #00000029',
idillon927b7592022-09-15 12:56:45 -0400315 borderRadius: largeRadius,
simond47ef9e2022-09-28 22:24:28 -0400316 borderStartStartRadius: position == 'start' ? smallRadius : largeRadius,
317 borderStartEndRadius: position == 'end' ? smallRadius : largeRadius,
318 };
idillon927b7592022-09-15 12:56:45 -0400319});
320
idillonbef18a52022-09-01 01:51:40 -0400321const MessageBubble = (props) => {
simond47ef9e2022-09-28 22:24:28 -0400322 const largeRadius = '20px';
323 const smallRadius = '5px';
Adrien Béraud023f7cf2022-09-18 14:57:53 -0400324 const radius = useMemo(() => {
simond47ef9e2022-09-28 22:24:28 -0400325 if (props.position == 'start') {
idillonbef18a52022-09-01 01:51:40 -0400326 return {
327 borderStartStartRadius: props.isFirstOfGroup ? largeRadius : smallRadius,
328 borderStartEndRadius: largeRadius,
329 borderEndStartRadius: props.isLastOfGroup ? largeRadius : smallRadius,
330 borderEndEndRadius: largeRadius,
simond47ef9e2022-09-28 22:24:28 -0400331 };
idillonbef18a52022-09-01 01:51:40 -0400332 }
333 return {
334 borderStartStartRadius: largeRadius,
335 borderStartEndRadius: props.isFirstOfGroup ? largeRadius : smallRadius,
336 borderEndStartRadius: largeRadius,
337 borderEndEndRadius: props.isLastOfGroup ? largeRadius : smallRadius,
simond47ef9e2022-09-28 22:24:28 -0400338 };
339 }, [props.isFirstOfGroup, props.isLastOfGroup, props.position]);
idillonbef18a52022-09-01 01:51:40 -0400340
341 return (
simond47ef9e2022-09-28 22:24:28 -0400342 <MessageTooltip position={props.position}>
idillon927b7592022-09-15 12:56:45 -0400343 <Box
344 sx={{
simond47ef9e2022-09-28 22:24:28 -0400345 width: 'fit-content',
idillon927b7592022-09-15 12:56:45 -0400346 backgroundColor: props.backgroundColor,
simond47ef9e2022-09-28 22:24:28 -0400347 padding: '16px',
idillon927b7592022-09-15 12:56:45 -0400348 ...radius,
349 }}
350 >
351 {props.children}
352 </Box>
353 </MessageTooltip>
simond47ef9e2022-09-28 22:24:28 -0400354 );
355};
idillonbef18a52022-09-01 01:51:40 -0400356
357const ParticipantName = (props) => {
358 return (
simond47ef9e2022-09-28 22:24:28 -0400359 <Box marginBottom="6px" marginLeft="16px" marginRight="16px">
360 <Typography variant="caption" color="#A7A7A7" fontWeight={700}>
idillonbef18a52022-09-01 01:51:40 -0400361 {props.name}
362 </Typography>
363 </Box>
simond47ef9e2022-09-28 22:24:28 -0400364 );
365};