blob: bfc442f7d1306b3314d8954013acbc339e1ac6b7 [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 */
idillon-sfl9d956ab2022-10-20 16:33:24 -040018import {
19 Box,
20 Chip,
21 Divider,
22 List,
23 ListItemButton,
24 ListItemText,
25 Stack,
26 Theme,
27 Tooltip,
28 Typography,
29} from '@mui/material';
simond47ef9e2022-09-28 22:24:28 -040030import { styled } from '@mui/material/styles';
idillon-sfl9d956ab2022-10-20 16:33:24 -040031import dayjs, { Dayjs } from 'dayjs';
simond47ef9e2022-09-28 22:24:28 -040032import isToday from 'dayjs/plugin/isToday';
33import isYesterday from 'dayjs/plugin/isYesterday';
idillon-sfl9d956ab2022-10-20 16:33:24 -040034import { Account, ConversationMember, Message } from 'jami-web-common';
35import { ReactElement } from 'react';
36import { ReactNode, useCallback, useMemo, useState } from 'react';
simond47ef9e2022-09-28 22:24:28 -040037import { useTranslation } from 'react-i18next';
simon07b4eb02022-09-29 17:50:26 -040038
idillon-sfl9d956ab2022-10-20 16:33:24 -040039import { EmojiButton, MoreButton, ReplyMessageButton } from './Button';
simond47ef9e2022-09-28 22:24:28 -040040import ConversationAvatar from './ConversationAvatar';
idillon-sfl9d956ab2022-10-20 16:33:24 -040041import { OppositeArrowsIcon, TrashBinIcon, TwoSheetsIcon } from './SvgIcon';
Larbi Gharibe9af9732021-03-31 15:08:01 +010042
simond47ef9e2022-09-28 22:24:28 -040043dayjs.extend(isToday);
44dayjs.extend(isYesterday);
idillonbef18a52022-09-01 01:51:40 -040045
idillon-sfl9d956ab2022-10-20 16:33:24 -040046type MessagePosition = 'start' | 'end';
47
48export const MessageCall = () => {
simon80b7b3b2022-09-28 17:50:10 -040049 return <Stack alignItems="center">&quot;Appel&quot;</Stack>;
simond47ef9e2022-09-28 22:24:28 -040050};
Larbi Gharibe9af9732021-03-31 15:08:01 +010051
idillon-sfl9d956ab2022-10-20 16:33:24 -040052export const MessageInitial = () => {
simond47ef9e2022-09-28 22:24:28 -040053 const { t } = useTranslation();
54 return <Stack alignItems="center">{t('message_swarm_created')}</Stack>;
55};
idillonbef18a52022-09-01 01:51:40 -040056
idillon-sfl9d956ab2022-10-20 16:33:24 -040057interface MessageDataTransferProps {
58 position: MessagePosition;
59 isFirstOfGroup: boolean;
60 isLastOfGroup: boolean;
61}
62
63export const MessageDataTransfer = ({ position, isFirstOfGroup, isLastOfGroup }: MessageDataTransferProps) => {
idillonbef18a52022-09-01 01:51:40 -040064 return (
65 <MessageBubble
simond47ef9e2022-09-28 22:24:28 -040066 backgroundColor={'#E5E5E5'}
idillon-sfl9d956ab2022-10-20 16:33:24 -040067 position={position}
68 isFirstOfGroup={isFirstOfGroup}
69 isLastOfGroup={isLastOfGroup}
idillonbef18a52022-09-01 01:51:40 -040070 >
simon80b7b3b2022-09-28 17:50:10 -040071 &quot;data-transfer&quot;
idillonbef18a52022-09-01 01:51:40 -040072 </MessageBubble>
simond47ef9e2022-09-28 22:24:28 -040073 );
74};
idillonbef18a52022-09-01 01:51:40 -040075
idillon-sfl9d956ab2022-10-20 16:33:24 -040076interface MessageMemberProps {
77 message: Message;
78}
79
80export const MessageMember = ({ message }: MessageMemberProps) => {
simond47ef9e2022-09-28 22:24:28 -040081 const { t } = useTranslation();
idillonbef18a52022-09-01 01:51:40 -040082 return (
simond47ef9e2022-09-28 22:24:28 -040083 <Stack alignItems="center">
idillonbef18a52022-09-01 01:51:40 -040084 <Chip
85 sx={{
simond47ef9e2022-09-28 22:24:28 -040086 width: 'fit-content',
idillonbef18a52022-09-01 01:51:40 -040087 }}
idillon-sfl9d956ab2022-10-20 16:33:24 -040088 label={t('message_user_joined', { user: message.author })}
idillonbef18a52022-09-01 01:51:40 -040089 />
90 </Stack>
simond47ef9e2022-09-28 22:24:28 -040091 );
92};
idillonbef18a52022-09-01 01:51:40 -040093
idillon-sfl9d956ab2022-10-20 16:33:24 -040094export const MessageMerge = () => {
simon80b7b3b2022-09-28 17:50:10 -040095 return <Stack alignItems="center">&quot;merge&quot;</Stack>;
simond47ef9e2022-09-28 22:24:28 -040096};
idillonbef18a52022-09-01 01:51:40 -040097
idillon-sfl9d956ab2022-10-20 16:33:24 -040098interface MessageTextProps {
99 message: Message;
100 position: MessagePosition;
101 isFirstOfGroup: boolean;
102 isLastOfGroup: boolean;
103 textColor: string;
104 bubbleColor: string;
105}
106
107export const MessageText = ({
108 message,
109 position,
110 isFirstOfGroup,
111 isLastOfGroup,
112 textColor,
113 bubbleColor,
114}: MessageTextProps) => {
idillonbef18a52022-09-01 01:51:40 -0400115 return (
116 <MessageBubble
idillon-sfl9d956ab2022-10-20 16:33:24 -0400117 backgroundColor={bubbleColor}
118 position={position}
119 isFirstOfGroup={isFirstOfGroup}
120 isLastOfGroup={isLastOfGroup}
idillonbef18a52022-09-01 01:51:40 -0400121 >
idillon-sfl9d956ab2022-10-20 16:33:24 -0400122 <Typography variant="body1" color={textColor} textAlign={position}>
123 {message.body}
idillonbef18a52022-09-01 01:51:40 -0400124 </Typography>
125 </MessageBubble>
simond47ef9e2022-09-28 22:24:28 -0400126 );
127};
idillonbef18a52022-09-01 01:51:40 -0400128
idillon-sfl9d956ab2022-10-20 16:33:24 -0400129interface MessageDateProps {
130 time: Dayjs;
131}
132
133export const MessageDate = ({ time }: MessageDateProps) => {
simond47ef9e2022-09-28 22:24:28 -0400134 let textDate;
idillonbef18a52022-09-01 01:51:40 -0400135
136 if (time.isToday()) {
simond47ef9e2022-09-28 22:24:28 -0400137 textDate = 'Today';
138 } else if (time.isYesterday()) {
139 textDate = 'Yesterday';
140 } else {
141 const date = time.date().toString().padStart(2, '0');
142 const month = (time.month() + 1).toString().padStart(2, '0');
143 textDate = `${date}/${month}/${time.year()}`;
idillonbef18a52022-09-01 01:51:40 -0400144 }
145
146 return (
simond47ef9e2022-09-28 22:24:28 -0400147 <Box marginTop="30px">
idillon04245a12022-09-01 11:12:17 -0400148 <Divider
149 sx={{
simond47ef9e2022-09-28 22:24:28 -0400150 '.MuiDivider-wrapper': {
idillon04245a12022-09-01 11:12:17 -0400151 margin: 0,
152 padding: 0,
153 },
simond47ef9e2022-09-28 22:24:28 -0400154 '&::before': {
155 borderTop: '1px solid #E5E5E5',
idillon04245a12022-09-01 11:12:17 -0400156 },
simond47ef9e2022-09-28 22:24:28 -0400157 '&::after': {
158 borderTop: '1px solid #E5E5E5',
idillon04245a12022-09-01 11:12:17 -0400159 },
160 }}
161 >
162 <Typography
163 variant="caption"
164 fontWeight={700}
165 border="1px solid #E5E5E5"
166 borderRadius="5px"
167 padding="10px 16px"
168 >
169 {textDate}
170 </Typography>
idillonbef18a52022-09-01 01:51:40 -0400171 </Divider>
172 </Box>
simond47ef9e2022-09-28 22:24:28 -0400173 );
174};
idillonbef18a52022-09-01 01:51:40 -0400175
idillon-sfl9d956ab2022-10-20 16:33:24 -0400176interface MessageTimeProps {
177 time: Dayjs;
178 hasDateOnTop: boolean;
179}
180
181export const MessageTime = ({ time, hasDateOnTop }: MessageTimeProps) => {
simond47ef9e2022-09-28 22:24:28 -0400182 const hour = time.hour().toString().padStart(2, '0');
183 const minute = time.minute().toString().padStart(2, '0');
184 const textTime = `${hour}:${minute}`;
idillonbef18a52022-09-01 01:51:40 -0400185
186 return (
simond47ef9e2022-09-28 22:24:28 -0400187 <Stack direction="row" justifyContent="center" margin="30px" marginTop={hasDateOnTop ? '20px' : '30px'}>
188 <Typography variant="caption" color="#A7A7A7" fontWeight={700}>
idillonbef18a52022-09-01 01:51:40 -0400189 {textTime}
190 </Typography>
191 </Stack>
simond47ef9e2022-09-28 22:24:28 -0400192 );
193};
idillonbef18a52022-09-01 01:51:40 -0400194
idillon-sfl9d956ab2022-10-20 16:33:24 -0400195interface MessageBubblesGroupProps {
196 account: Account;
197 messages: Message[];
198 members: ConversationMember[];
199}
200
201export const MessageBubblesGroup = ({ account, messages, members }: MessageBubblesGroupProps) => {
202 const isUser = messages[0]?.author === account.getUri();
simond47ef9e2022-09-28 22:24:28 -0400203 const position = isUser ? 'end' : 'start';
204 const bubbleColor = isUser ? '#005699' : '#E5E5E5';
205 const textColor = isUser ? 'white' : 'black';
idillonbef18a52022-09-01 01:51:40 -0400206
idillonae655dd2022-10-14 18:11:02 -0400207 let authorName;
208 if (isUser) {
idillon-sfl9d956ab2022-10-20 16:33:24 -0400209 authorName = account.getDisplayName();
idillonae655dd2022-10-14 18:11:02 -0400210 } else {
idillon-sfl9d956ab2022-10-20 16:33:24 -0400211 const member = members.find((member) => messages[0]?.author === member.contact.getUri());
212 authorName = member?.contact?.getDisplayName() || '';
idillonae655dd2022-10-14 18:11:02 -0400213 }
214
idillonbef18a52022-09-01 01:51:40 -0400215 return (
idillon89720a82022-09-06 18:47:05 -0400216 <Stack // Row for a group of message bubbles with the user's infos
idillonbef18a52022-09-01 01:51:40 -0400217 direction="row"
218 justifyContent={position}
idillon89720a82022-09-06 18:47:05 -0400219 alignItems="end"
220 spacing="10px"
idillonbef18a52022-09-01 01:51:40 -0400221 >
simond47ef9e2022-09-28 22:24:28 -0400222 {!isUser && (
idillonae655dd2022-10-14 18:11:02 -0400223 <ConversationAvatar displayName={authorName} sx={{ width: '22px', height: '22px', fontSize: '15px' }} />
simond47ef9e2022-09-28 22:24:28 -0400224 )}
idillon89720a82022-09-06 18:47:05 -0400225 <Stack // Container to align the bubbles to the same side of a row
idillonbef18a52022-09-01 01:51:40 -0400226 width="66.66%"
idillonbef18a52022-09-01 01:51:40 -0400227 alignItems={position}
228 >
idillon-sfl9d956ab2022-10-20 16:33:24 -0400229 <ParticipantName name={authorName} />
idillon89720a82022-09-06 18:47:05 -0400230 <Stack // Container for a group of message bubbles
idillonbef18a52022-09-01 01:51:40 -0400231 spacing="6px"
232 alignItems={position}
233 direction="column-reverse"
234 >
idillon-sfl9d956ab2022-10-20 16:33:24 -0400235 {messages.map((message, index) => {
236 let Component: typeof MessageText | typeof MessageDataTransfer;
simond47ef9e2022-09-28 22:24:28 -0400237 switch (message.type) {
238 case 'text/plain':
239 Component = MessageText;
240 break;
241 case 'application/data-transfer+json':
242 Component = MessageDataTransfer;
243 break;
idillon-sfl9d956ab2022-10-20 16:33:24 -0400244 default:
245 return null;
idillonbef18a52022-09-01 01:51:40 -0400246 }
simond47ef9e2022-09-28 22:24:28 -0400247 return (
248 <Component // Single message
249 key={message.id}
250 message={message}
251 textColor={textColor}
252 position={position}
253 bubbleColor={bubbleColor}
idillon-sfl9d956ab2022-10-20 16:33:24 -0400254 isFirstOfGroup={index === messages.length - 1}
255 isLastOfGroup={index === 0}
simond47ef9e2022-09-28 22:24:28 -0400256 />
257 );
258 })}
idillonbef18a52022-09-01 01:51:40 -0400259 </Stack>
260 </Stack>
261 </Stack>
simond47ef9e2022-09-28 22:24:28 -0400262 );
263};
idillonbef18a52022-09-01 01:51:40 -0400264
idillon-sfl9d956ab2022-10-20 16:33:24 -0400265interface MessageTooltipProps {
266 className?: string;
267 position: MessagePosition;
268 children: ReactElement;
269}
270
271const MessageTooltip = styled(({ className, position, children }: MessageTooltipProps) => {
simond47ef9e2022-09-28 22:24:28 -0400272 const [open, setOpen] = useState(false);
273 const emojis = ['😎', '😄', '😍']; // Should be last three used emojis
idillon927b7592022-09-15 12:56:45 -0400274 const additionalOptions = [
275 {
276 Icon: TwoSheetsIcon,
simond47ef9e2022-09-28 22:24:28 -0400277 text: 'Copy',
idillon927b7592022-09-15 12:56:45 -0400278 action: () => {},
279 },
280 {
281 Icon: OppositeArrowsIcon,
simond47ef9e2022-09-28 22:24:28 -0400282 text: 'Transfer',
idillon927b7592022-09-15 12:56:45 -0400283 action: () => {},
284 },
285 {
286 Icon: TrashBinIcon,
simond47ef9e2022-09-28 22:24:28 -0400287 text: 'Delete message',
idillon927b7592022-09-15 12:56:45 -0400288 action: () => {},
289 },
simond47ef9e2022-09-28 22:24:28 -0400290 ];
idillon927b7592022-09-15 12:56:45 -0400291
simond47ef9e2022-09-28 22:24:28 -0400292 const toggleMoreMenu = useCallback(() => setOpen((open) => !open), [setOpen]);
idillon927b7592022-09-15 12:56:45 -0400293
simond47ef9e2022-09-28 22:24:28 -0400294 const onClose = useCallback(() => {
295 setOpen(false);
296 }, [setOpen]);
idillon927b7592022-09-15 12:56:45 -0400297
298 return (
299 <Tooltip
idillon927b7592022-09-15 12:56:45 -0400300 classes={{ tooltip: className }} // Required for styles. Don't know why
idillon-sfl9d956ab2022-10-20 16:33:24 -0400301 placement={position === 'start' ? 'right-start' : 'left-start'}
idillon927b7592022-09-15 12:56:45 -0400302 PopperProps={{
303 modifiers: [
304 {
simond47ef9e2022-09-28 22:24:28 -0400305 name: 'offset',
idillon927b7592022-09-15 12:56:45 -0400306 options: {
simond47ef9e2022-09-28 22:24:28 -0400307 offset: [-2, -30],
idillon927b7592022-09-15 12:56:45 -0400308 },
309 },
310 ],
311 }}
312 onClose={onClose}
313 title={
simond47ef9e2022-09-28 22:24:28 -0400314 <Stack>
simond47ef9e2022-09-28 22:24:28 -0400315 {/* Whole tooltip's content */}
idillon927b7592022-09-15 12:56:45 -0400316 <Stack // Main options
317 direction="row"
318 spacing="16px"
319 >
simond47ef9e2022-09-28 22:24:28 -0400320 {emojis.map((emoji) => (
321 <EmojiButton key={emoji} emoji={emoji} />
322 ))}
323 <ReplyMessageButton />
324 <MoreButton onClick={toggleMoreMenu} />
idillon927b7592022-09-15 12:56:45 -0400325 </Stack>
simond47ef9e2022-09-28 22:24:28 -0400326 {open && ( // Additional menu options
idillon927b7592022-09-15 12:56:45 -0400327 <>
simond47ef9e2022-09-28 22:24:28 -0400328 <Divider sx={{ paddingTop: '16px' }} />
329 <List sx={{ padding: 0, paddingTop: '8px', marginBottom: '-8px' }}>
330 {additionalOptions.map((option) => (
331 <ListItemButton
332 key={option.text}
333 sx={{
334 padding: '8px',
335 }}
336 >
337 <Stack // Could not find proper way to set spacing between ListItemIcon and ListItemText
338 direction="row"
339 spacing="16px"
idillon927b7592022-09-15 12:56:45 -0400340 >
simond47ef9e2022-09-28 22:24:28 -0400341 <option.Icon
342 sx={{
343 height: '16px',
344 margin: 0,
idillon-sfl9d956ab2022-10-20 16:33:24 -0400345 color: (theme: Theme) => theme?.palette?.primary?.dark,
simond47ef9e2022-09-28 22:24:28 -0400346 }}
347 />
348 <ListItemText
349 primary={option.text}
350 primaryTypographyProps={{
351 fontSize: '12px',
352 lineHeight: '16px',
353 }}
354 sx={{
355 height: '16px',
356 margin: 0,
357 }}
358 />
359 </Stack>
360 </ListItemButton>
361 ))}
362 </List>
idillon927b7592022-09-15 12:56:45 -0400363 </>
simond47ef9e2022-09-28 22:24:28 -0400364 )}
idillon927b7592022-09-15 12:56:45 -0400365 </Stack>
366 }
idillon-sfl9d956ab2022-10-20 16:33:24 -0400367 >
368 {children}
369 </Tooltip>
simond47ef9e2022-09-28 22:24:28 -0400370 );
idillon-sfl9d956ab2022-10-20 16:33:24 -0400371})(({ position }) => {
simond47ef9e2022-09-28 22:24:28 -0400372 const largeRadius = '20px';
373 const smallRadius = '5px';
idillon927b7592022-09-15 12:56:45 -0400374 return {
simond47ef9e2022-09-28 22:24:28 -0400375 backgroundColor: 'white',
376 padding: '16px',
377 boxShadow: '3px 3px 7px #00000029',
idillon927b7592022-09-15 12:56:45 -0400378 borderRadius: largeRadius,
idillon-sfl9d956ab2022-10-20 16:33:24 -0400379 borderStartStartRadius: position === 'start' ? smallRadius : largeRadius,
380 borderStartEndRadius: position === 'end' ? smallRadius : largeRadius,
simond47ef9e2022-09-28 22:24:28 -0400381 };
idillon927b7592022-09-15 12:56:45 -0400382});
383
idillon-sfl9d956ab2022-10-20 16:33:24 -0400384interface MessageBubbleProps {
385 position: MessagePosition;
386 isFirstOfGroup: boolean;
387 isLastOfGroup: boolean;
388 backgroundColor: string;
389 children: ReactNode;
390}
391
392const MessageBubble = ({ position, isFirstOfGroup, isLastOfGroup, backgroundColor, children }: MessageBubbleProps) => {
simond47ef9e2022-09-28 22:24:28 -0400393 const largeRadius = '20px';
394 const smallRadius = '5px';
Adrien Béraud023f7cf2022-09-18 14:57:53 -0400395 const radius = useMemo(() => {
idillon-sfl9d956ab2022-10-20 16:33:24 -0400396 if (position === 'start') {
idillonbef18a52022-09-01 01:51:40 -0400397 return {
idillon-sfl9d956ab2022-10-20 16:33:24 -0400398 borderStartStartRadius: isFirstOfGroup ? largeRadius : smallRadius,
idillonbef18a52022-09-01 01:51:40 -0400399 borderStartEndRadius: largeRadius,
idillon-sfl9d956ab2022-10-20 16:33:24 -0400400 borderEndStartRadius: isLastOfGroup ? largeRadius : smallRadius,
idillonbef18a52022-09-01 01:51:40 -0400401 borderEndEndRadius: largeRadius,
simond47ef9e2022-09-28 22:24:28 -0400402 };
idillonbef18a52022-09-01 01:51:40 -0400403 }
404 return {
405 borderStartStartRadius: largeRadius,
idillon-sfl9d956ab2022-10-20 16:33:24 -0400406 borderStartEndRadius: isFirstOfGroup ? largeRadius : smallRadius,
idillonbef18a52022-09-01 01:51:40 -0400407 borderEndStartRadius: largeRadius,
idillon-sfl9d956ab2022-10-20 16:33:24 -0400408 borderEndEndRadius: isLastOfGroup ? largeRadius : smallRadius,
simond47ef9e2022-09-28 22:24:28 -0400409 };
idillon-sfl9d956ab2022-10-20 16:33:24 -0400410 }, [isFirstOfGroup, isLastOfGroup, position]);
idillonbef18a52022-09-01 01:51:40 -0400411
412 return (
idillon-sfl9d956ab2022-10-20 16:33:24 -0400413 <MessageTooltip position={position}>
idillon927b7592022-09-15 12:56:45 -0400414 <Box
415 sx={{
simond47ef9e2022-09-28 22:24:28 -0400416 width: 'fit-content',
idillon-sfl9d956ab2022-10-20 16:33:24 -0400417 backgroundColor: backgroundColor,
simond47ef9e2022-09-28 22:24:28 -0400418 padding: '16px',
idillon927b7592022-09-15 12:56:45 -0400419 ...radius,
420 }}
421 >
idillon-sfl9d956ab2022-10-20 16:33:24 -0400422 {children}
idillon927b7592022-09-15 12:56:45 -0400423 </Box>
424 </MessageTooltip>
simond47ef9e2022-09-28 22:24:28 -0400425 );
426};
idillonbef18a52022-09-01 01:51:40 -0400427
idillon-sfl9d956ab2022-10-20 16:33:24 -0400428interface ParticipantNameProps {
429 name: string;
430}
431
432const ParticipantName = ({ name }: ParticipantNameProps) => {
idillonbef18a52022-09-01 01:51:40 -0400433 return (
simond47ef9e2022-09-28 22:24:28 -0400434 <Box marginBottom="6px" marginLeft="16px" marginRight="16px">
435 <Typography variant="caption" color="#A7A7A7" fontWeight={700}>
idillon-sfl9d956ab2022-10-20 16:33:24 -0400436 {name}
idillonbef18a52022-09-01 01:51:40 -0400437 </Typography>
438 </Box>
simond47ef9e2022-09-28 22:24:28 -0400439 );
440};