blob: 6b2fad87c59f2f4205240c52ec876a400bbc7940 [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';
Michelle Sepkap Simeb3dd3122022-11-03 02:12:39 -040031import { 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
Michelle Sepkap Simeb3dd3122022-11-03 02:12:39 -040036import dayjs from '../dayjsInitializer';
idillon-sfl9d956ab2022-10-20 16:33:24 -040037import { EmojiButton, MoreButton, ReplyMessageButton } from './Button';
simond47ef9e2022-09-28 22:24:28 -040038import ConversationAvatar from './ConversationAvatar';
idillon-sflec735452022-10-27 13:18:41 -040039import {
40 ArrowLeftCurved,
41 ArrowLeftDown,
42 ArrowRightUp,
43 OppositeArrowsIcon,
44 TrashBinIcon,
45 TwoSheetsIcon,
46} from './SvgIcon';
Larbi Gharibe9af9732021-03-31 15:08:01 +010047
idillon-sfl9d956ab2022-10-20 16:33:24 -040048type MessagePosition = 'start' | 'end';
49
idillon-sfl118ae442022-10-25 10:42:54 -040050const notificationMessageTypes = ['initial', 'member'] as const;
51type NotificationMessageType = typeof notificationMessageTypes[number];
52const checkIsNotificationMessageType = (type: Message['type'] | undefined): type is NotificationMessageType => {
53 return notificationMessageTypes.includes(type as NotificationMessageType);
simond47ef9e2022-09-28 22:24:28 -040054};
Larbi Gharibe9af9732021-03-31 15:08:01 +010055
idillon-sfl118ae442022-10-25 10:42:54 -040056const invisibleMessageTypes = ['application/update-profile', 'merge', 'vote'] as const;
57type InvisibleMessageType = typeof invisibleMessageTypes[number];
58const checkIsInvisibleMessageType = (type: Message['type'] | undefined): type is InvisibleMessageType => {
59 return invisibleMessageTypes.includes(type as InvisibleMessageType);
simond47ef9e2022-09-28 22:24:28 -040060};
idillonbef18a52022-09-01 01:51:40 -040061
idillon-sfl118ae442022-10-25 10:42:54 -040062const userMessageTypes = ['text/plain', 'application/data-transfer+json', 'application/call-history+json'] as const;
63type UserMessageType = typeof userMessageTypes[number];
64const checkIsUserMessageType = (type: Message['type'] | undefined): type is UserMessageType => {
65 return userMessageTypes.includes(type as UserMessageType);
66};
67
68const checkShowsTime = (time: Dayjs, previousTime: Dayjs) => {
69 return !previousTime.isSame(time) && !time.isBetween(previousTime, previousTime?.add(1, 'minute'));
70};
71
72const findPreviousVisibleMessage = (messages: Message[], messageIndex: number) => {
73 for (let i = messageIndex + 1; i < messages.length; ++i) {
74 const message = messages[i];
75 if (!checkIsInvisibleMessageType(message?.type)) {
76 return message;
77 }
78 }
79};
80
81const findNextVisibleMessage = (messages: Message[], messageIndex: number) => {
82 for (let i = messageIndex - 1; i >= 0; --i) {
83 const message = messages[i];
84 if (!checkIsInvisibleMessageType(message?.type)) {
85 return message;
86 }
87 }
88};
89
90const avatarSize = '22px';
91const spacingBetweenAvatarAndBubble = '10px';
92const bubblePadding = '16px';
93
94interface MessageCallProps {
95 message: Message;
96 isAccountMessage: boolean;
idillon-sfl9d956ab2022-10-20 16:33:24 -040097 isFirstOfGroup: boolean;
98 isLastOfGroup: boolean;
99}
100
idillon-sflec735452022-10-27 13:18:41 -0400101const MessageCall = ({ message, isAccountMessage, isFirstOfGroup, isLastOfGroup }: MessageCallProps) => {
idillon-sfl118ae442022-10-25 10:42:54 -0400102 const position = isAccountMessage ? 'end' : 'start';
idillon-sflec735452022-10-27 13:18:41 -0400103
104 const { t } = useTranslation();
105 const { bubbleColor, Icon, text, textColor } = useMemo(() => {
106 const callDuration = dayjs.duration(parseInt(message?.duration || ''));
107 if (callDuration.asSeconds() === 0) {
108 if (isAccountMessage) {
109 return {
110 text: t('message_call_outgoing_missed'),
111 Icon: ArrowLeftCurved,
112 textColor: 'white',
113 bubbleColor: '#005699' + '80', // opacity 50%
114 };
115 } else {
116 return {
117 text: t('message_call_incoming_missed'),
118 Icon: ArrowLeftCurved,
119 textColor: 'black',
120 bubbleColor: '#C6C6C6',
121 };
122 }
123 } else {
124 const minutes = Math.floor(callDuration.asMinutes()).toString().padStart(2, '0');
125 const seconds = callDuration.format('ss');
126 const interpolations = {
127 duration: `${minutes}:${seconds}`,
128 };
129 if (isAccountMessage) {
130 return {
131 text: t('message_call_outgoing', interpolations),
132 Icon: ArrowRightUp,
133 textColor: 'white',
134 bubbleColor: '#005699',
135 };
136 } else {
137 return {
138 text: t('message_call_incoming', interpolations),
139 Icon: ArrowLeftDown,
140 textcolor: 'black',
141 bubbleColor: '#E5E5E5',
142 };
143 }
144 }
145 }, [isAccountMessage, message, t]);
146
idillonbef18a52022-09-01 01:51:40 -0400147 return (
idillon-sfl118ae442022-10-25 10:42:54 -0400148 <Bubble position={position} isFirstOfGroup={isFirstOfGroup} isLastOfGroup={isLastOfGroup} bubbleColor={bubbleColor}>
idillon-sflec735452022-10-27 13:18:41 -0400149 <Stack direction="row" spacing="10px" alignItems="center">
150 <Icon sx={{ fontSize: '16px', color: textColor }} />
151 <Typography variant="body1" color={textColor} textAlign={position} fontWeight="bold" textTransform="uppercase">
152 {text}
153 </Typography>
154 </Stack>
idillon-sfl118ae442022-10-25 10:42:54 -0400155 </Bubble>
156 );
157};
158
159const MessageInitial = () => {
160 const { t } = useTranslation();
161 return <>{t('message_swarm_created')}</>;
162};
163
164interface MessageDataTransferProps {
165 message: Message;
166 isAccountMessage: boolean;
167 isFirstOfGroup: boolean;
168 isLastOfGroup: boolean;
169}
170
171const MessageDataTransfer = ({ isAccountMessage, isFirstOfGroup, isLastOfGroup }: MessageDataTransferProps) => {
172 const position = isAccountMessage ? 'end' : 'start';
173 return (
174 <Bubble bubbleColor="#E5E5E5" position={position} isFirstOfGroup={isFirstOfGroup} isLastOfGroup={isLastOfGroup}>
simon80b7b3b2022-09-28 17:50:10 -0400175 &quot;data-transfer&quot;
idillon-sfl118ae442022-10-25 10:42:54 -0400176 </Bubble>
simond47ef9e2022-09-28 22:24:28 -0400177 );
178};
idillonbef18a52022-09-01 01:51:40 -0400179
idillon-sfl9d956ab2022-10-20 16:33:24 -0400180interface MessageMemberProps {
181 message: Message;
182}
183
idillon-sfl118ae442022-10-25 10:42:54 -0400184const MessageMember = ({ message }: MessageMemberProps) => {
simond47ef9e2022-09-28 22:24:28 -0400185 const { t } = useTranslation();
idillonbef18a52022-09-01 01:51:40 -0400186 return (
idillon-sfl118ae442022-10-25 10:42:54 -0400187 <Chip
188 sx={{
189 width: 'fit-content',
190 }}
191 label={t('message_user_joined', { user: message.author })}
192 />
simond47ef9e2022-09-28 22:24:28 -0400193 );
194};
idillonbef18a52022-09-01 01:51:40 -0400195
idillon-sfl9d956ab2022-10-20 16:33:24 -0400196interface MessageTextProps {
197 message: Message;
idillon-sfl118ae442022-10-25 10:42:54 -0400198 isAccountMessage: boolean;
idillon-sfl9d956ab2022-10-20 16:33:24 -0400199 isFirstOfGroup: boolean;
200 isLastOfGroup: boolean;
idillon-sfl9d956ab2022-10-20 16:33:24 -0400201}
202
idillon-sfl118ae442022-10-25 10:42:54 -0400203const MessageText = ({ message, isAccountMessage, isFirstOfGroup, isLastOfGroup }: MessageTextProps) => {
204 const position = isAccountMessage ? 'end' : 'start';
205 const bubbleColor = isAccountMessage ? '#005699' : '#E5E5E5';
206 const textColor = isAccountMessage ? 'white' : 'black';
idillonbef18a52022-09-01 01:51:40 -0400207 return (
idillon-sfl118ae442022-10-25 10:42:54 -0400208 <MessageTooltip position={position}>
209 <Bubble
210 bubbleColor={bubbleColor}
211 position={position}
212 isFirstOfGroup={isFirstOfGroup}
213 isLastOfGroup={isLastOfGroup}
214 >
215 <Typography variant="body1" color={textColor} textAlign={position}>
216 {message.body}
217 </Typography>
218 </Bubble>
219 </MessageTooltip>
simond47ef9e2022-09-28 22:24:28 -0400220 );
221};
idillonbef18a52022-09-01 01:51:40 -0400222
idillon-sfl118ae442022-10-25 10:42:54 -0400223interface DateIndicatorProps {
idillon-sfl9d956ab2022-10-20 16:33:24 -0400224 time: Dayjs;
225}
226
idillon-sfl118ae442022-10-25 10:42:54 -0400227const DateIndicator = ({ time }: DateIndicatorProps) => {
idillon-sfl0e1a0d92022-10-25 16:52:44 -0400228 const { i18n } = useTranslation();
idillonbef18a52022-09-01 01:51:40 -0400229
idillon-sfl0e1a0d92022-10-25 16:52:44 -0400230 const textDate = useMemo(() => {
231 if (time.isToday()) {
232 return new Intl.RelativeTimeFormat(i18n.language, { numeric: 'auto' }).format(0, 'day');
233 } else if (time.isYesterday()) {
234 return new Intl.RelativeTimeFormat(i18n.language, { numeric: 'auto' }).format(-1, 'day');
235 } else {
236 return dayjs(time).locale(i18n.language).format('L');
237 }
238 }, [i18n, time]);
idillonbef18a52022-09-01 01:51:40 -0400239
240 return (
simond47ef9e2022-09-28 22:24:28 -0400241 <Box marginTop="30px">
idillon04245a12022-09-01 11:12:17 -0400242 <Divider
243 sx={{
simond47ef9e2022-09-28 22:24:28 -0400244 '.MuiDivider-wrapper': {
idillon04245a12022-09-01 11:12:17 -0400245 margin: 0,
246 padding: 0,
247 },
simond47ef9e2022-09-28 22:24:28 -0400248 '&::before': {
249 borderTop: '1px solid #E5E5E5',
idillon04245a12022-09-01 11:12:17 -0400250 },
simond47ef9e2022-09-28 22:24:28 -0400251 '&::after': {
252 borderTop: '1px solid #E5E5E5',
idillon04245a12022-09-01 11:12:17 -0400253 },
254 }}
255 >
256 <Typography
257 variant="caption"
258 fontWeight={700}
259 border="1px solid #E5E5E5"
260 borderRadius="5px"
261 padding="10px 16px"
idillon-sfl0e1a0d92022-10-25 16:52:44 -0400262 textTransform="capitalize"
idillon04245a12022-09-01 11:12:17 -0400263 >
264 {textDate}
265 </Typography>
idillonbef18a52022-09-01 01:51:40 -0400266 </Divider>
267 </Box>
simond47ef9e2022-09-28 22:24:28 -0400268 );
269};
idillonbef18a52022-09-01 01:51:40 -0400270
idillon-sfl118ae442022-10-25 10:42:54 -0400271interface TimeIndicatorProps {
idillon-sfl9d956ab2022-10-20 16:33:24 -0400272 time: Dayjs;
273 hasDateOnTop: boolean;
274}
275
idillon-sfl118ae442022-10-25 10:42:54 -0400276const TimeIndicator = ({ time, hasDateOnTop }: TimeIndicatorProps) => {
idillon-sfl0e1a0d92022-10-25 16:52:44 -0400277 const { i18n } = useTranslation();
278
279 const textTime = useMemo(() => {
280 return dayjs(time).locale(i18n.language).format('LT');
281 }, [i18n, time]);
idillonbef18a52022-09-01 01:51:40 -0400282
283 return (
idillon-sfl118ae442022-10-25 10:42:54 -0400284 <Stack direction="row" justifyContent="center" marginTop={hasDateOnTop ? '20px' : '30px'}>
simond47ef9e2022-09-28 22:24:28 -0400285 <Typography variant="caption" color="#A7A7A7" fontWeight={700}>
idillonbef18a52022-09-01 01:51:40 -0400286 {textTime}
287 </Typography>
288 </Stack>
simond47ef9e2022-09-28 22:24:28 -0400289 );
290};
idillonbef18a52022-09-01 01:51:40 -0400291
idillon-sfl118ae442022-10-25 10:42:54 -0400292interface NotificationMessageRowProps {
293 message: Message;
idillon-sfl9d956ab2022-10-20 16:33:24 -0400294}
295
idillon-sfl118ae442022-10-25 10:42:54 -0400296const NotificationMessageRow = ({ message }: NotificationMessageRowProps) => {
297 let messageComponent;
298 switch (message.type) {
299 case 'initial':
300 messageComponent = <MessageInitial />;
301 break;
302 case 'member':
303 messageComponent = <MessageMember message={message} />;
304 break;
305 default:
306 console.error(`${NotificationMessageRow.name} received unhandled message type: ${message.type}`);
307 return null;
idillonae655dd2022-10-14 18:11:02 -0400308 }
309
idillonbef18a52022-09-01 01:51:40 -0400310 return (
idillon-sfl118ae442022-10-25 10:42:54 -0400311 <Stack paddingTop={'30px'} alignItems="center">
312 {messageComponent}
313 </Stack>
314 );
315};
316
317interface UserMessageRowProps {
318 message: Message;
319 isAccountMessage: boolean;
320 previousMessage: Message | undefined;
321 nextMessage: Message | undefined;
322 time: Dayjs;
323 showsTime: boolean;
324 author: Account | Contact;
325}
326
327const UserMessageRow = ({
328 message,
329 previousMessage,
330 nextMessage,
331 isAccountMessage,
332 time,
333 showsTime,
334 author,
335}: UserMessageRowProps) => {
336 const authorName = author.getDisplayName();
337 const position = isAccountMessage ? 'end' : 'start';
338
339 const previousIsUserMessageType = checkIsUserMessageType(previousMessage?.type);
340 const nextIsUserMessageType = checkIsUserMessageType(nextMessage?.type);
341 const nextTime = dayjs.unix(Number(nextMessage?.timestamp));
342 const nextShowsTime = checkShowsTime(nextTime, time);
343 const isFirstOfGroup = showsTime || !previousIsUserMessageType || previousMessage?.author !== message.author;
344 const isLastOfGroup = nextShowsTime || !nextIsUserMessageType || message.author !== nextMessage?.author;
345
346 const props = {
347 message,
348 isAccountMessage,
349 isFirstOfGroup,
350 isLastOfGroup,
351 };
352
353 let MessageComponent;
354 switch (message.type) {
355 case 'text/plain':
356 MessageComponent = MessageText;
357 break;
358 case 'application/data-transfer+json':
359 MessageComponent = MessageDataTransfer;
360 break;
361 case 'application/call-history+json':
362 MessageComponent = MessageCall;
363 break;
364 default:
365 console.error(`${UserMessageRow.name} received unhandled message type: ${message.type}`);
366 return null;
367 }
368
369 const participantNamePadding = isAccountMessage
370 ? bubblePadding
371 : parseInt(avatarSize) + parseInt(spacingBetweenAvatarAndBubble) + parseInt(bubblePadding) + 'px';
372
373 return (
374 <Stack alignItems={position}>
375 {isFirstOfGroup && (
376 <Box padding={`30px ${participantNamePadding} 0 ${participantNamePadding}`}>
377 <ParticipantName name={authorName} />
378 </Box>
simond47ef9e2022-09-28 22:24:28 -0400379 )}
idillon-sfl118ae442022-10-25 10:42:54 -0400380 <Stack
381 direction="row"
382 justifyContent={position}
383 alignItems="end"
384 spacing={spacingBetweenAvatarAndBubble}
385 paddingTop="6px"
idillonbef18a52022-09-01 01:51:40 -0400386 width="66.66%"
idillonbef18a52022-09-01 01:51:40 -0400387 >
idillon-sfl118ae442022-10-25 10:42:54 -0400388 <Box sx={{ width: avatarSize }}>
389 {!isAccountMessage && isLastOfGroup && (
390 <ConversationAvatar
391 displayName={authorName}
392 sx={{ width: avatarSize, height: avatarSize, fontSize: '15px' }}
393 />
394 )}
395 </Box>
396 <MessageComponent {...props} />
idillonbef18a52022-09-01 01:51:40 -0400397 </Stack>
398 </Stack>
simond47ef9e2022-09-28 22:24:28 -0400399 );
400};
idillonbef18a52022-09-01 01:51:40 -0400401
idillon-sfl9d956ab2022-10-20 16:33:24 -0400402interface MessageTooltipProps {
403 className?: string;
404 position: MessagePosition;
405 children: ReactElement;
406}
407
408const MessageTooltip = styled(({ className, position, children }: MessageTooltipProps) => {
simond47ef9e2022-09-28 22:24:28 -0400409 const [open, setOpen] = useState(false);
410 const emojis = ['😎', '😄', '😍']; // Should be last three used emojis
idillon927b7592022-09-15 12:56:45 -0400411 const additionalOptions = [
412 {
413 Icon: TwoSheetsIcon,
simond47ef9e2022-09-28 22:24:28 -0400414 text: 'Copy',
idillon927b7592022-09-15 12:56:45 -0400415 action: () => {},
416 },
417 {
418 Icon: OppositeArrowsIcon,
simond47ef9e2022-09-28 22:24:28 -0400419 text: 'Transfer',
idillon927b7592022-09-15 12:56:45 -0400420 action: () => {},
421 },
422 {
423 Icon: TrashBinIcon,
simond47ef9e2022-09-28 22:24:28 -0400424 text: 'Delete message',
idillon927b7592022-09-15 12:56:45 -0400425 action: () => {},
426 },
simond47ef9e2022-09-28 22:24:28 -0400427 ];
idillon927b7592022-09-15 12:56:45 -0400428
simond47ef9e2022-09-28 22:24:28 -0400429 const toggleMoreMenu = useCallback(() => setOpen((open) => !open), [setOpen]);
idillon927b7592022-09-15 12:56:45 -0400430
simond47ef9e2022-09-28 22:24:28 -0400431 const onClose = useCallback(() => {
432 setOpen(false);
433 }, [setOpen]);
idillon927b7592022-09-15 12:56:45 -0400434
435 return (
436 <Tooltip
idillon927b7592022-09-15 12:56:45 -0400437 classes={{ tooltip: className }} // Required for styles. Don't know why
idillon-sfl9d956ab2022-10-20 16:33:24 -0400438 placement={position === 'start' ? 'right-start' : 'left-start'}
idillon927b7592022-09-15 12:56:45 -0400439 PopperProps={{
440 modifiers: [
441 {
simond47ef9e2022-09-28 22:24:28 -0400442 name: 'offset',
idillon927b7592022-09-15 12:56:45 -0400443 options: {
simond47ef9e2022-09-28 22:24:28 -0400444 offset: [-2, -30],
idillon927b7592022-09-15 12:56:45 -0400445 },
446 },
447 ],
448 }}
449 onClose={onClose}
450 title={
simond47ef9e2022-09-28 22:24:28 -0400451 <Stack>
simond47ef9e2022-09-28 22:24:28 -0400452 {/* Whole tooltip's content */}
idillon927b7592022-09-15 12:56:45 -0400453 <Stack // Main options
454 direction="row"
455 spacing="16px"
456 >
simond47ef9e2022-09-28 22:24:28 -0400457 {emojis.map((emoji) => (
458 <EmojiButton key={emoji} emoji={emoji} />
459 ))}
460 <ReplyMessageButton />
461 <MoreButton onClick={toggleMoreMenu} />
idillon927b7592022-09-15 12:56:45 -0400462 </Stack>
simond47ef9e2022-09-28 22:24:28 -0400463 {open && ( // Additional menu options
idillon927b7592022-09-15 12:56:45 -0400464 <>
simond47ef9e2022-09-28 22:24:28 -0400465 <Divider sx={{ paddingTop: '16px' }} />
466 <List sx={{ padding: 0, paddingTop: '8px', marginBottom: '-8px' }}>
467 {additionalOptions.map((option) => (
468 <ListItemButton
469 key={option.text}
470 sx={{
471 padding: '8px',
472 }}
473 >
474 <Stack // Could not find proper way to set spacing between ListItemIcon and ListItemText
475 direction="row"
476 spacing="16px"
idillon927b7592022-09-15 12:56:45 -0400477 >
simond47ef9e2022-09-28 22:24:28 -0400478 <option.Icon
479 sx={{
480 height: '16px',
481 margin: 0,
idillon-sfl9d956ab2022-10-20 16:33:24 -0400482 color: (theme: Theme) => theme?.palette?.primary?.dark,
simond47ef9e2022-09-28 22:24:28 -0400483 }}
484 />
485 <ListItemText
486 primary={option.text}
487 primaryTypographyProps={{
488 fontSize: '12px',
489 lineHeight: '16px',
490 }}
491 sx={{
492 height: '16px',
493 margin: 0,
494 }}
495 />
496 </Stack>
497 </ListItemButton>
498 ))}
499 </List>
idillon927b7592022-09-15 12:56:45 -0400500 </>
simond47ef9e2022-09-28 22:24:28 -0400501 )}
idillon927b7592022-09-15 12:56:45 -0400502 </Stack>
503 }
idillon-sfl9d956ab2022-10-20 16:33:24 -0400504 >
idillon-sfl118ae442022-10-25 10:42:54 -0400505 {/* div fixes 'Function components cannot be given refs' error */}
506 <div>{children}</div>
idillon-sfl9d956ab2022-10-20 16:33:24 -0400507 </Tooltip>
simond47ef9e2022-09-28 22:24:28 -0400508 );
idillon-sfl9d956ab2022-10-20 16:33:24 -0400509})(({ position }) => {
simond47ef9e2022-09-28 22:24:28 -0400510 const largeRadius = '20px';
511 const smallRadius = '5px';
idillon927b7592022-09-15 12:56:45 -0400512 return {
simond47ef9e2022-09-28 22:24:28 -0400513 backgroundColor: 'white',
514 padding: '16px',
515 boxShadow: '3px 3px 7px #00000029',
idillon927b7592022-09-15 12:56:45 -0400516 borderRadius: largeRadius,
idillon-sfl9d956ab2022-10-20 16:33:24 -0400517 borderStartStartRadius: position === 'start' ? smallRadius : largeRadius,
518 borderStartEndRadius: position === 'end' ? smallRadius : largeRadius,
simond47ef9e2022-09-28 22:24:28 -0400519 };
idillon927b7592022-09-15 12:56:45 -0400520});
521
idillon-sfl118ae442022-10-25 10:42:54 -0400522interface BubbleProps {
idillon-sfl9d956ab2022-10-20 16:33:24 -0400523 position: MessagePosition;
524 isFirstOfGroup: boolean;
525 isLastOfGroup: boolean;
idillon-sfl118ae442022-10-25 10:42:54 -0400526 bubbleColor: string;
idillon-sfl9d956ab2022-10-20 16:33:24 -0400527 children: ReactNode;
528}
529
idillon-sfl118ae442022-10-25 10:42:54 -0400530const Bubble = ({ position, isFirstOfGroup, isLastOfGroup, bubbleColor, children }: BubbleProps) => {
simond47ef9e2022-09-28 22:24:28 -0400531 const largeRadius = '20px';
532 const smallRadius = '5px';
Adrien Béraud023f7cf2022-09-18 14:57:53 -0400533 const radius = useMemo(() => {
idillon-sfl9d956ab2022-10-20 16:33:24 -0400534 if (position === 'start') {
idillonbef18a52022-09-01 01:51:40 -0400535 return {
idillon-sfl9d956ab2022-10-20 16:33:24 -0400536 borderStartStartRadius: isFirstOfGroup ? largeRadius : smallRadius,
idillonbef18a52022-09-01 01:51:40 -0400537 borderStartEndRadius: largeRadius,
idillon-sfl9d956ab2022-10-20 16:33:24 -0400538 borderEndStartRadius: isLastOfGroup ? largeRadius : smallRadius,
idillonbef18a52022-09-01 01:51:40 -0400539 borderEndEndRadius: largeRadius,
simond47ef9e2022-09-28 22:24:28 -0400540 };
idillonbef18a52022-09-01 01:51:40 -0400541 }
542 return {
543 borderStartStartRadius: largeRadius,
idillon-sfl9d956ab2022-10-20 16:33:24 -0400544 borderStartEndRadius: isFirstOfGroup ? largeRadius : smallRadius,
idillonbef18a52022-09-01 01:51:40 -0400545 borderEndStartRadius: largeRadius,
idillon-sfl9d956ab2022-10-20 16:33:24 -0400546 borderEndEndRadius: isLastOfGroup ? largeRadius : smallRadius,
simond47ef9e2022-09-28 22:24:28 -0400547 };
idillon-sfl9d956ab2022-10-20 16:33:24 -0400548 }, [isFirstOfGroup, isLastOfGroup, position]);
idillonbef18a52022-09-01 01:51:40 -0400549
550 return (
idillon-sfl118ae442022-10-25 10:42:54 -0400551 <Box
552 sx={{
553 width: 'fit-content',
554 backgroundColor: bubbleColor,
555 padding: bubblePadding,
556 ...radius,
557 }}
558 >
559 {children}
560 </Box>
simond47ef9e2022-09-28 22:24:28 -0400561 );
562};
idillonbef18a52022-09-01 01:51:40 -0400563
idillon-sfl9d956ab2022-10-20 16:33:24 -0400564interface ParticipantNameProps {
565 name: string;
566}
567
568const ParticipantName = ({ name }: ParticipantNameProps) => {
idillonbef18a52022-09-01 01:51:40 -0400569 return (
idillon-sfl118ae442022-10-25 10:42:54 -0400570 <Typography variant="caption" color="#A7A7A7" fontWeight={700}>
571 {name}
572 </Typography>
573 );
574};
575
576interface MessageProps {
577 messageIndex: number;
578 messages: Message[];
579 isAccountMessage: boolean;
580 author: Account | Contact;
581}
582
583export const MessageRow = ({ messageIndex, messages, isAccountMessage, author }: MessageProps) => {
584 const message = messages[messageIndex];
585 const previousMessage = findPreviousVisibleMessage(messages, messageIndex);
586 const nextMessage = findNextVisibleMessage(messages, messageIndex);
587 const time = dayjs.unix(Number(message.timestamp));
588 const previousTime = dayjs.unix(Number(previousMessage?.timestamp));
589 const showDate =
590 message?.type === 'initial' || previousTime.year() !== time.year() || previousTime.dayOfYear() !== time.dayOfYear();
591 const showTime = checkShowsTime(time, previousTime);
592 let messageComponent;
593 if (checkIsUserMessageType(message.type)) {
594 messageComponent = (
595 <UserMessageRow
596 message={message}
597 previousMessage={previousMessage}
598 nextMessage={nextMessage}
599 time={time}
600 showsTime={showTime}
601 isAccountMessage={isAccountMessage}
602 author={author}
603 />
604 );
605 } else if (checkIsNotificationMessageType(message.type)) {
606 messageComponent = <NotificationMessageRow message={message} />;
607 } else if (checkIsInvisibleMessageType(message.type)) {
608 return null;
609 } else {
610 const _exhaustiveCheck: never = message.type;
611 return _exhaustiveCheck;
612 }
613
614 return (
615 <Stack>
616 {showDate && <DateIndicator time={time} />}
617 {showTime && <TimeIndicator time={time} hasDateOnTop={showDate} />}
618 {messageComponent}
619 </Stack>
simond47ef9e2022-09-28 22:24:28 -0400620 );
621};