blob: 152f4552db9535af9528dba031df80fb94419c6a [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';
idillon-sfl118ae442022-10-25 10:42:54 -040032import { Account, Contact, Message } from 'jami-web-common';
33import { ReactElement, ReactNode, useCallback, useMemo, useState } from 'react';
simond47ef9e2022-09-28 22:24:28 -040034import { useTranslation } from 'react-i18next';
simon07b4eb02022-09-29 17:50:26 -040035
idillon-sfl9d956ab2022-10-20 16:33:24 -040036import { EmojiButton, MoreButton, ReplyMessageButton } from './Button';
simond47ef9e2022-09-28 22:24:28 -040037import ConversationAvatar from './ConversationAvatar';
idillon-sflec735452022-10-27 13:18:41 -040038import {
39 ArrowLeftCurved,
40 ArrowLeftDown,
41 ArrowRightUp,
42 OppositeArrowsIcon,
43 TrashBinIcon,
44 TwoSheetsIcon,
45} from './SvgIcon';
Larbi Gharibe9af9732021-03-31 15:08:01 +010046
idillon-sfl9d956ab2022-10-20 16:33:24 -040047type MessagePosition = 'start' | 'end';
48
idillon-sfl118ae442022-10-25 10:42:54 -040049const notificationMessageTypes = ['initial', 'member'] as const;
50type NotificationMessageType = typeof notificationMessageTypes[number];
51const checkIsNotificationMessageType = (type: Message['type'] | undefined): type is NotificationMessageType => {
52 return notificationMessageTypes.includes(type as NotificationMessageType);
simond47ef9e2022-09-28 22:24:28 -040053};
Larbi Gharibe9af9732021-03-31 15:08:01 +010054
idillon-sfl118ae442022-10-25 10:42:54 -040055const invisibleMessageTypes = ['application/update-profile', 'merge', 'vote'] as const;
56type InvisibleMessageType = typeof invisibleMessageTypes[number];
57const checkIsInvisibleMessageType = (type: Message['type'] | undefined): type is InvisibleMessageType => {
58 return invisibleMessageTypes.includes(type as InvisibleMessageType);
simond47ef9e2022-09-28 22:24:28 -040059};
idillonbef18a52022-09-01 01:51:40 -040060
idillon-sfl118ae442022-10-25 10:42:54 -040061const userMessageTypes = ['text/plain', 'application/data-transfer+json', 'application/call-history+json'] as const;
62type UserMessageType = typeof userMessageTypes[number];
63const checkIsUserMessageType = (type: Message['type'] | undefined): type is UserMessageType => {
64 return userMessageTypes.includes(type as UserMessageType);
65};
66
67const checkShowsTime = (time: Dayjs, previousTime: Dayjs) => {
68 return !previousTime.isSame(time) && !time.isBetween(previousTime, previousTime?.add(1, 'minute'));
69};
70
71const findPreviousVisibleMessage = (messages: Message[], messageIndex: number) => {
72 for (let i = messageIndex + 1; i < messages.length; ++i) {
73 const message = messages[i];
74 if (!checkIsInvisibleMessageType(message?.type)) {
75 return message;
76 }
77 }
78};
79
80const findNextVisibleMessage = (messages: Message[], messageIndex: number) => {
81 for (let i = messageIndex - 1; i >= 0; --i) {
82 const message = messages[i];
83 if (!checkIsInvisibleMessageType(message?.type)) {
84 return message;
85 }
86 }
87};
88
89const avatarSize = '22px';
90const spacingBetweenAvatarAndBubble = '10px';
91const bubblePadding = '16px';
92
93interface MessageCallProps {
94 message: Message;
95 isAccountMessage: boolean;
idillon-sfl9d956ab2022-10-20 16:33:24 -040096 isFirstOfGroup: boolean;
97 isLastOfGroup: boolean;
98}
99
idillon-sflec735452022-10-27 13:18:41 -0400100const MessageCall = ({ message, isAccountMessage, isFirstOfGroup, isLastOfGroup }: MessageCallProps) => {
idillon-sfl118ae442022-10-25 10:42:54 -0400101 const position = isAccountMessage ? 'end' : 'start';
idillon-sflec735452022-10-27 13:18:41 -0400102
103 const { t } = useTranslation();
104 const { bubbleColor, Icon, text, textColor } = useMemo(() => {
105 const callDuration = dayjs.duration(parseInt(message?.duration || ''));
106 if (callDuration.asSeconds() === 0) {
107 if (isAccountMessage) {
108 return {
109 text: t('message_call_outgoing_missed'),
110 Icon: ArrowLeftCurved,
111 textColor: 'white',
112 bubbleColor: '#005699' + '80', // opacity 50%
113 };
114 } else {
115 return {
116 text: t('message_call_incoming_missed'),
117 Icon: ArrowLeftCurved,
118 textColor: 'black',
119 bubbleColor: '#C6C6C6',
120 };
121 }
122 } else {
123 const minutes = Math.floor(callDuration.asMinutes()).toString().padStart(2, '0');
124 const seconds = callDuration.format('ss');
125 const interpolations = {
126 duration: `${minutes}:${seconds}`,
127 };
128 if (isAccountMessage) {
129 return {
130 text: t('message_call_outgoing', interpolations),
131 Icon: ArrowRightUp,
132 textColor: 'white',
133 bubbleColor: '#005699',
134 };
135 } else {
136 return {
137 text: t('message_call_incoming', interpolations),
138 Icon: ArrowLeftDown,
139 textcolor: 'black',
140 bubbleColor: '#E5E5E5',
141 };
142 }
143 }
144 }, [isAccountMessage, message, t]);
145
idillonbef18a52022-09-01 01:51:40 -0400146 return (
idillon-sfl118ae442022-10-25 10:42:54 -0400147 <Bubble position={position} isFirstOfGroup={isFirstOfGroup} isLastOfGroup={isLastOfGroup} bubbleColor={bubbleColor}>
idillon-sflec735452022-10-27 13:18:41 -0400148 <Stack direction="row" spacing="10px" alignItems="center">
149 <Icon sx={{ fontSize: '16px', color: textColor }} />
150 <Typography variant="body1" color={textColor} textAlign={position} fontWeight="bold" textTransform="uppercase">
151 {text}
152 </Typography>
153 </Stack>
idillon-sfl118ae442022-10-25 10:42:54 -0400154 </Bubble>
155 );
156};
157
158const MessageInitial = () => {
159 const { t } = useTranslation();
160 return <>{t('message_swarm_created')}</>;
161};
162
163interface MessageDataTransferProps {
164 message: Message;
165 isAccountMessage: boolean;
166 isFirstOfGroup: boolean;
167 isLastOfGroup: boolean;
168}
169
170const MessageDataTransfer = ({ isAccountMessage, isFirstOfGroup, isLastOfGroup }: MessageDataTransferProps) => {
171 const position = isAccountMessage ? 'end' : 'start';
172 return (
173 <Bubble bubbleColor="#E5E5E5" position={position} isFirstOfGroup={isFirstOfGroup} isLastOfGroup={isLastOfGroup}>
simon80b7b3b2022-09-28 17:50:10 -0400174 &quot;data-transfer&quot;
idillon-sfl118ae442022-10-25 10:42:54 -0400175 </Bubble>
simond47ef9e2022-09-28 22:24:28 -0400176 );
177};
idillonbef18a52022-09-01 01:51:40 -0400178
idillon-sfl9d956ab2022-10-20 16:33:24 -0400179interface MessageMemberProps {
180 message: Message;
181}
182
idillon-sfl118ae442022-10-25 10:42:54 -0400183const MessageMember = ({ message }: MessageMemberProps) => {
simond47ef9e2022-09-28 22:24:28 -0400184 const { t } = useTranslation();
idillonbef18a52022-09-01 01:51:40 -0400185 return (
idillon-sfl118ae442022-10-25 10:42:54 -0400186 <Chip
187 sx={{
188 width: 'fit-content',
189 }}
190 label={t('message_user_joined', { user: message.author })}
191 />
simond47ef9e2022-09-28 22:24:28 -0400192 );
193};
idillonbef18a52022-09-01 01:51:40 -0400194
idillon-sfl9d956ab2022-10-20 16:33:24 -0400195interface MessageTextProps {
196 message: Message;
idillon-sfl118ae442022-10-25 10:42:54 -0400197 isAccountMessage: boolean;
idillon-sfl9d956ab2022-10-20 16:33:24 -0400198 isFirstOfGroup: boolean;
199 isLastOfGroup: boolean;
idillon-sfl9d956ab2022-10-20 16:33:24 -0400200}
201
idillon-sfl118ae442022-10-25 10:42:54 -0400202const MessageText = ({ message, isAccountMessage, isFirstOfGroup, isLastOfGroup }: MessageTextProps) => {
203 const position = isAccountMessage ? 'end' : 'start';
204 const bubbleColor = isAccountMessage ? '#005699' : '#E5E5E5';
205 const textColor = isAccountMessage ? 'white' : 'black';
idillonbef18a52022-09-01 01:51:40 -0400206 return (
idillon-sfl118ae442022-10-25 10:42:54 -0400207 <MessageTooltip position={position}>
208 <Bubble
209 bubbleColor={bubbleColor}
210 position={position}
211 isFirstOfGroup={isFirstOfGroup}
212 isLastOfGroup={isLastOfGroup}
213 >
214 <Typography variant="body1" color={textColor} textAlign={position}>
215 {message.body}
216 </Typography>
217 </Bubble>
218 </MessageTooltip>
simond47ef9e2022-09-28 22:24:28 -0400219 );
220};
idillonbef18a52022-09-01 01:51:40 -0400221
idillon-sfl118ae442022-10-25 10:42:54 -0400222interface DateIndicatorProps {
idillon-sfl9d956ab2022-10-20 16:33:24 -0400223 time: Dayjs;
224}
225
idillon-sfl118ae442022-10-25 10:42:54 -0400226const DateIndicator = ({ time }: DateIndicatorProps) => {
idillon-sfl0e1a0d92022-10-25 16:52:44 -0400227 const { i18n } = useTranslation();
idillonbef18a52022-09-01 01:51:40 -0400228
idillon-sfl0e1a0d92022-10-25 16:52:44 -0400229 const textDate = useMemo(() => {
230 if (time.isToday()) {
231 return new Intl.RelativeTimeFormat(i18n.language, { numeric: 'auto' }).format(0, 'day');
232 } else if (time.isYesterday()) {
233 return new Intl.RelativeTimeFormat(i18n.language, { numeric: 'auto' }).format(-1, 'day');
234 } else {
235 return dayjs(time).locale(i18n.language).format('L');
236 }
237 }, [i18n, time]);
idillonbef18a52022-09-01 01:51:40 -0400238
239 return (
simond47ef9e2022-09-28 22:24:28 -0400240 <Box marginTop="30px">
idillon04245a12022-09-01 11:12:17 -0400241 <Divider
242 sx={{
simond47ef9e2022-09-28 22:24:28 -0400243 '.MuiDivider-wrapper': {
idillon04245a12022-09-01 11:12:17 -0400244 margin: 0,
245 padding: 0,
246 },
simond47ef9e2022-09-28 22:24:28 -0400247 '&::before': {
248 borderTop: '1px solid #E5E5E5',
idillon04245a12022-09-01 11:12:17 -0400249 },
simond47ef9e2022-09-28 22:24:28 -0400250 '&::after': {
251 borderTop: '1px solid #E5E5E5',
idillon04245a12022-09-01 11:12:17 -0400252 },
253 }}
254 >
255 <Typography
256 variant="caption"
257 fontWeight={700}
258 border="1px solid #E5E5E5"
259 borderRadius="5px"
260 padding="10px 16px"
idillon-sfl0e1a0d92022-10-25 16:52:44 -0400261 textTransform="capitalize"
idillon04245a12022-09-01 11:12:17 -0400262 >
263 {textDate}
264 </Typography>
idillonbef18a52022-09-01 01:51:40 -0400265 </Divider>
266 </Box>
simond47ef9e2022-09-28 22:24:28 -0400267 );
268};
idillonbef18a52022-09-01 01:51:40 -0400269
idillon-sfl118ae442022-10-25 10:42:54 -0400270interface TimeIndicatorProps {
idillon-sfl9d956ab2022-10-20 16:33:24 -0400271 time: Dayjs;
272 hasDateOnTop: boolean;
273}
274
idillon-sfl118ae442022-10-25 10:42:54 -0400275const TimeIndicator = ({ time, hasDateOnTop }: TimeIndicatorProps) => {
idillon-sfl0e1a0d92022-10-25 16:52:44 -0400276 const { i18n } = useTranslation();
277
278 const textTime = useMemo(() => {
279 return dayjs(time).locale(i18n.language).format('LT');
280 }, [i18n, time]);
idillonbef18a52022-09-01 01:51:40 -0400281
282 return (
idillon-sfl118ae442022-10-25 10:42:54 -0400283 <Stack direction="row" justifyContent="center" marginTop={hasDateOnTop ? '20px' : '30px'}>
simond47ef9e2022-09-28 22:24:28 -0400284 <Typography variant="caption" color="#A7A7A7" fontWeight={700}>
idillonbef18a52022-09-01 01:51:40 -0400285 {textTime}
286 </Typography>
287 </Stack>
simond47ef9e2022-09-28 22:24:28 -0400288 );
289};
idillonbef18a52022-09-01 01:51:40 -0400290
idillon-sfl118ae442022-10-25 10:42:54 -0400291interface NotificationMessageRowProps {
292 message: Message;
idillon-sfl9d956ab2022-10-20 16:33:24 -0400293}
294
idillon-sfl118ae442022-10-25 10:42:54 -0400295const NotificationMessageRow = ({ message }: NotificationMessageRowProps) => {
296 let messageComponent;
297 switch (message.type) {
298 case 'initial':
299 messageComponent = <MessageInitial />;
300 break;
301 case 'member':
302 messageComponent = <MessageMember message={message} />;
303 break;
304 default:
305 console.error(`${NotificationMessageRow.name} received unhandled message type: ${message.type}`);
306 return null;
idillonae655dd2022-10-14 18:11:02 -0400307 }
308
idillonbef18a52022-09-01 01:51:40 -0400309 return (
idillon-sfl118ae442022-10-25 10:42:54 -0400310 <Stack paddingTop={'30px'} alignItems="center">
311 {messageComponent}
312 </Stack>
313 );
314};
315
316interface UserMessageRowProps {
317 message: Message;
318 isAccountMessage: boolean;
319 previousMessage: Message | undefined;
320 nextMessage: Message | undefined;
321 time: Dayjs;
322 showsTime: boolean;
323 author: Account | Contact;
324}
325
326const UserMessageRow = ({
327 message,
328 previousMessage,
329 nextMessage,
330 isAccountMessage,
331 time,
332 showsTime,
333 author,
334}: UserMessageRowProps) => {
335 const authorName = author.getDisplayName();
336 const position = isAccountMessage ? 'end' : 'start';
337
338 const previousIsUserMessageType = checkIsUserMessageType(previousMessage?.type);
339 const nextIsUserMessageType = checkIsUserMessageType(nextMessage?.type);
340 const nextTime = dayjs.unix(Number(nextMessage?.timestamp));
341 const nextShowsTime = checkShowsTime(nextTime, time);
342 const isFirstOfGroup = showsTime || !previousIsUserMessageType || previousMessage?.author !== message.author;
343 const isLastOfGroup = nextShowsTime || !nextIsUserMessageType || message.author !== nextMessage?.author;
344
345 const props = {
346 message,
347 isAccountMessage,
348 isFirstOfGroup,
349 isLastOfGroup,
350 };
351
352 let MessageComponent;
353 switch (message.type) {
354 case 'text/plain':
355 MessageComponent = MessageText;
356 break;
357 case 'application/data-transfer+json':
358 MessageComponent = MessageDataTransfer;
359 break;
360 case 'application/call-history+json':
361 MessageComponent = MessageCall;
362 break;
363 default:
364 console.error(`${UserMessageRow.name} received unhandled message type: ${message.type}`);
365 return null;
366 }
367
368 const participantNamePadding = isAccountMessage
369 ? bubblePadding
370 : parseInt(avatarSize) + parseInt(spacingBetweenAvatarAndBubble) + parseInt(bubblePadding) + 'px';
371
372 return (
373 <Stack alignItems={position}>
374 {isFirstOfGroup && (
375 <Box padding={`30px ${participantNamePadding} 0 ${participantNamePadding}`}>
376 <ParticipantName name={authorName} />
377 </Box>
simond47ef9e2022-09-28 22:24:28 -0400378 )}
idillon-sfl118ae442022-10-25 10:42:54 -0400379 <Stack
380 direction="row"
381 justifyContent={position}
382 alignItems="end"
383 spacing={spacingBetweenAvatarAndBubble}
384 paddingTop="6px"
idillonbef18a52022-09-01 01:51:40 -0400385 width="66.66%"
idillonbef18a52022-09-01 01:51:40 -0400386 >
idillon-sfl118ae442022-10-25 10:42:54 -0400387 <Box sx={{ width: avatarSize }}>
388 {!isAccountMessage && isLastOfGroup && (
389 <ConversationAvatar
390 displayName={authorName}
391 sx={{ width: avatarSize, height: avatarSize, fontSize: '15px' }}
392 />
393 )}
394 </Box>
395 <MessageComponent {...props} />
idillonbef18a52022-09-01 01:51:40 -0400396 </Stack>
397 </Stack>
simond47ef9e2022-09-28 22:24:28 -0400398 );
399};
idillonbef18a52022-09-01 01:51:40 -0400400
idillon-sfl9d956ab2022-10-20 16:33:24 -0400401interface MessageTooltipProps {
402 className?: string;
403 position: MessagePosition;
404 children: ReactElement;
405}
406
407const MessageTooltip = styled(({ className, position, children }: MessageTooltipProps) => {
simond47ef9e2022-09-28 22:24:28 -0400408 const [open, setOpen] = useState(false);
409 const emojis = ['😎', '😄', '😍']; // Should be last three used emojis
idillon927b7592022-09-15 12:56:45 -0400410 const additionalOptions = [
411 {
412 Icon: TwoSheetsIcon,
simond47ef9e2022-09-28 22:24:28 -0400413 text: 'Copy',
idillon927b7592022-09-15 12:56:45 -0400414 action: () => {},
415 },
416 {
417 Icon: OppositeArrowsIcon,
simond47ef9e2022-09-28 22:24:28 -0400418 text: 'Transfer',
idillon927b7592022-09-15 12:56:45 -0400419 action: () => {},
420 },
421 {
422 Icon: TrashBinIcon,
simond47ef9e2022-09-28 22:24:28 -0400423 text: 'Delete message',
idillon927b7592022-09-15 12:56:45 -0400424 action: () => {},
425 },
simond47ef9e2022-09-28 22:24:28 -0400426 ];
idillon927b7592022-09-15 12:56:45 -0400427
simond47ef9e2022-09-28 22:24:28 -0400428 const toggleMoreMenu = useCallback(() => setOpen((open) => !open), [setOpen]);
idillon927b7592022-09-15 12:56:45 -0400429
simond47ef9e2022-09-28 22:24:28 -0400430 const onClose = useCallback(() => {
431 setOpen(false);
432 }, [setOpen]);
idillon927b7592022-09-15 12:56:45 -0400433
434 return (
435 <Tooltip
idillon927b7592022-09-15 12:56:45 -0400436 classes={{ tooltip: className }} // Required for styles. Don't know why
idillon-sfl9d956ab2022-10-20 16:33:24 -0400437 placement={position === 'start' ? 'right-start' : 'left-start'}
idillon927b7592022-09-15 12:56:45 -0400438 PopperProps={{
439 modifiers: [
440 {
simond47ef9e2022-09-28 22:24:28 -0400441 name: 'offset',
idillon927b7592022-09-15 12:56:45 -0400442 options: {
simond47ef9e2022-09-28 22:24:28 -0400443 offset: [-2, -30],
idillon927b7592022-09-15 12:56:45 -0400444 },
445 },
446 ],
447 }}
448 onClose={onClose}
449 title={
simond47ef9e2022-09-28 22:24:28 -0400450 <Stack>
simond47ef9e2022-09-28 22:24:28 -0400451 {/* Whole tooltip's content */}
idillon927b7592022-09-15 12:56:45 -0400452 <Stack // Main options
453 direction="row"
454 spacing="16px"
455 >
simond47ef9e2022-09-28 22:24:28 -0400456 {emojis.map((emoji) => (
457 <EmojiButton key={emoji} emoji={emoji} />
458 ))}
459 <ReplyMessageButton />
460 <MoreButton onClick={toggleMoreMenu} />
idillon927b7592022-09-15 12:56:45 -0400461 </Stack>
simond47ef9e2022-09-28 22:24:28 -0400462 {open && ( // Additional menu options
idillon927b7592022-09-15 12:56:45 -0400463 <>
simond47ef9e2022-09-28 22:24:28 -0400464 <Divider sx={{ paddingTop: '16px' }} />
465 <List sx={{ padding: 0, paddingTop: '8px', marginBottom: '-8px' }}>
466 {additionalOptions.map((option) => (
467 <ListItemButton
468 key={option.text}
469 sx={{
470 padding: '8px',
471 }}
472 >
473 <Stack // Could not find proper way to set spacing between ListItemIcon and ListItemText
474 direction="row"
475 spacing="16px"
idillon927b7592022-09-15 12:56:45 -0400476 >
simond47ef9e2022-09-28 22:24:28 -0400477 <option.Icon
478 sx={{
479 height: '16px',
480 margin: 0,
idillon-sfl9d956ab2022-10-20 16:33:24 -0400481 color: (theme: Theme) => theme?.palette?.primary?.dark,
simond47ef9e2022-09-28 22:24:28 -0400482 }}
483 />
484 <ListItemText
485 primary={option.text}
486 primaryTypographyProps={{
487 fontSize: '12px',
488 lineHeight: '16px',
489 }}
490 sx={{
491 height: '16px',
492 margin: 0,
493 }}
494 />
495 </Stack>
496 </ListItemButton>
497 ))}
498 </List>
idillon927b7592022-09-15 12:56:45 -0400499 </>
simond47ef9e2022-09-28 22:24:28 -0400500 )}
idillon927b7592022-09-15 12:56:45 -0400501 </Stack>
502 }
idillon-sfl9d956ab2022-10-20 16:33:24 -0400503 >
idillon-sfl118ae442022-10-25 10:42:54 -0400504 {/* div fixes 'Function components cannot be given refs' error */}
505 <div>{children}</div>
idillon-sfl9d956ab2022-10-20 16:33:24 -0400506 </Tooltip>
simond47ef9e2022-09-28 22:24:28 -0400507 );
idillon-sfl9d956ab2022-10-20 16:33:24 -0400508})(({ position }) => {
simond47ef9e2022-09-28 22:24:28 -0400509 const largeRadius = '20px';
510 const smallRadius = '5px';
idillon927b7592022-09-15 12:56:45 -0400511 return {
simond47ef9e2022-09-28 22:24:28 -0400512 backgroundColor: 'white',
513 padding: '16px',
514 boxShadow: '3px 3px 7px #00000029',
idillon927b7592022-09-15 12:56:45 -0400515 borderRadius: largeRadius,
idillon-sfl9d956ab2022-10-20 16:33:24 -0400516 borderStartStartRadius: position === 'start' ? smallRadius : largeRadius,
517 borderStartEndRadius: position === 'end' ? smallRadius : largeRadius,
simond47ef9e2022-09-28 22:24:28 -0400518 };
idillon927b7592022-09-15 12:56:45 -0400519});
520
idillon-sfl118ae442022-10-25 10:42:54 -0400521interface BubbleProps {
idillon-sfl9d956ab2022-10-20 16:33:24 -0400522 position: MessagePosition;
523 isFirstOfGroup: boolean;
524 isLastOfGroup: boolean;
idillon-sfl118ae442022-10-25 10:42:54 -0400525 bubbleColor: string;
idillon-sfl9d956ab2022-10-20 16:33:24 -0400526 children: ReactNode;
527}
528
idillon-sfl118ae442022-10-25 10:42:54 -0400529const Bubble = ({ position, isFirstOfGroup, isLastOfGroup, bubbleColor, children }: BubbleProps) => {
simond47ef9e2022-09-28 22:24:28 -0400530 const largeRadius = '20px';
531 const smallRadius = '5px';
Adrien Béraud023f7cf2022-09-18 14:57:53 -0400532 const radius = useMemo(() => {
idillon-sfl9d956ab2022-10-20 16:33:24 -0400533 if (position === 'start') {
idillonbef18a52022-09-01 01:51:40 -0400534 return {
idillon-sfl9d956ab2022-10-20 16:33:24 -0400535 borderStartStartRadius: isFirstOfGroup ? largeRadius : smallRadius,
idillonbef18a52022-09-01 01:51:40 -0400536 borderStartEndRadius: largeRadius,
idillon-sfl9d956ab2022-10-20 16:33:24 -0400537 borderEndStartRadius: isLastOfGroup ? largeRadius : smallRadius,
idillonbef18a52022-09-01 01:51:40 -0400538 borderEndEndRadius: largeRadius,
simond47ef9e2022-09-28 22:24:28 -0400539 };
idillonbef18a52022-09-01 01:51:40 -0400540 }
541 return {
542 borderStartStartRadius: largeRadius,
idillon-sfl9d956ab2022-10-20 16:33:24 -0400543 borderStartEndRadius: isFirstOfGroup ? largeRadius : smallRadius,
idillonbef18a52022-09-01 01:51:40 -0400544 borderEndStartRadius: largeRadius,
idillon-sfl9d956ab2022-10-20 16:33:24 -0400545 borderEndEndRadius: isLastOfGroup ? largeRadius : smallRadius,
simond47ef9e2022-09-28 22:24:28 -0400546 };
idillon-sfl9d956ab2022-10-20 16:33:24 -0400547 }, [isFirstOfGroup, isLastOfGroup, position]);
idillonbef18a52022-09-01 01:51:40 -0400548
549 return (
idillon-sfl118ae442022-10-25 10:42:54 -0400550 <Box
551 sx={{
552 width: 'fit-content',
553 backgroundColor: bubbleColor,
554 padding: bubblePadding,
555 ...radius,
556 }}
557 >
558 {children}
559 </Box>
simond47ef9e2022-09-28 22:24:28 -0400560 );
561};
idillonbef18a52022-09-01 01:51:40 -0400562
idillon-sfl9d956ab2022-10-20 16:33:24 -0400563interface ParticipantNameProps {
564 name: string;
565}
566
567const ParticipantName = ({ name }: ParticipantNameProps) => {
idillonbef18a52022-09-01 01:51:40 -0400568 return (
idillon-sfl118ae442022-10-25 10:42:54 -0400569 <Typography variant="caption" color="#A7A7A7" fontWeight={700}>
570 {name}
571 </Typography>
572 );
573};
574
575interface MessageProps {
576 messageIndex: number;
577 messages: Message[];
578 isAccountMessage: boolean;
579 author: Account | Contact;
580}
581
582export const MessageRow = ({ messageIndex, messages, isAccountMessage, author }: MessageProps) => {
583 const message = messages[messageIndex];
584 const previousMessage = findPreviousVisibleMessage(messages, messageIndex);
585 const nextMessage = findNextVisibleMessage(messages, messageIndex);
586 const time = dayjs.unix(Number(message.timestamp));
587 const previousTime = dayjs.unix(Number(previousMessage?.timestamp));
588 const showDate =
589 message?.type === 'initial' || previousTime.year() !== time.year() || previousTime.dayOfYear() !== time.dayOfYear();
590 const showTime = checkShowsTime(time, previousTime);
591 let messageComponent;
592 if (checkIsUserMessageType(message.type)) {
593 messageComponent = (
594 <UserMessageRow
595 message={message}
596 previousMessage={previousMessage}
597 nextMessage={nextMessage}
598 time={time}
599 showsTime={showTime}
600 isAccountMessage={isAccountMessage}
601 author={author}
602 />
603 );
604 } else if (checkIsNotificationMessageType(message.type)) {
605 messageComponent = <NotificationMessageRow message={message} />;
606 } else if (checkIsInvisibleMessageType(message.type)) {
607 return null;
608 } else {
609 const _exhaustiveCheck: never = message.type;
610 return _exhaustiveCheck;
611 }
612
613 return (
614 <Stack>
615 {showDate && <DateIndicator time={time} />}
616 {showTime && <TimeIndicator time={time} hasDateOnTop={showDate} />}
617 {messageComponent}
618 </Stack>
simond47ef9e2022-09-28 22:24:28 -0400619 );
620};