blob: 8807295fa3d9f14a5384358a32a9cd4363daed18 [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 */
idillonef9ab812022-11-18 13:46:24 -050018import { Box, Chip, Divider, Stack, Tooltip, Typography } from '@mui/material';
simond47ef9e2022-09-28 22:24:28 -040019import { styled } from '@mui/material/styles';
Michelle Sepkap Simeb3dd3122022-11-03 02:12:39 -040020import { Dayjs } from 'dayjs';
Misha Krieger-Raynauld6bbdacf2022-11-29 21:45:40 -050021import { Message } from 'jami-web-common';
idillon-sfl118ae442022-10-25 10:42:54 -040022import { ReactElement, ReactNode, useCallback, useMemo, useState } from 'react';
simond47ef9e2022-09-28 22:24:28 -040023import { useTranslation } from 'react-i18next';
simon07b4eb02022-09-29 17:50:26 -040024
Michelle Sepkap Simeb3dd3122022-11-03 02:12:39 -040025import dayjs from '../dayjsInitializer';
Misha Krieger-Raynauldcfa44302022-11-30 18:36:36 -050026import { Account } from '../models/account';
27import { Contact } from '../models/contact';
idillon-sfl9d956ab2022-10-20 16:33:24 -040028import { EmojiButton, MoreButton, ReplyMessageButton } from './Button';
simond47ef9e2022-09-28 22:24:28 -040029import ConversationAvatar from './ConversationAvatar';
idillonef9ab812022-11-18 13:46:24 -050030import PopoverList, { PopoverListItemData } from './PopoverList';
idillon-sflec735452022-10-27 13:18:41 -040031import {
32 ArrowLeftCurved,
33 ArrowLeftDown,
34 ArrowRightUp,
35 OppositeArrowsIcon,
36 TrashBinIcon,
37 TwoSheetsIcon,
38} from './SvgIcon';
Larbi Gharibe9af9732021-03-31 15:08:01 +010039
idillon-sfl9d956ab2022-10-20 16:33:24 -040040type MessagePosition = 'start' | 'end';
41
idillon-sfl118ae442022-10-25 10:42:54 -040042const notificationMessageTypes = ['initial', 'member'] as const;
43type NotificationMessageType = typeof notificationMessageTypes[number];
44const checkIsNotificationMessageType = (type: Message['type'] | undefined): type is NotificationMessageType => {
45 return notificationMessageTypes.includes(type as NotificationMessageType);
simond47ef9e2022-09-28 22:24:28 -040046};
Larbi Gharibe9af9732021-03-31 15:08:01 +010047
idillon-sfl118ae442022-10-25 10:42:54 -040048const invisibleMessageTypes = ['application/update-profile', 'merge', 'vote'] as const;
49type InvisibleMessageType = typeof invisibleMessageTypes[number];
50const checkIsInvisibleMessageType = (type: Message['type'] | undefined): type is InvisibleMessageType => {
51 return invisibleMessageTypes.includes(type as InvisibleMessageType);
simond47ef9e2022-09-28 22:24:28 -040052};
idillonbef18a52022-09-01 01:51:40 -040053
idillon-sfl118ae442022-10-25 10:42:54 -040054const userMessageTypes = ['text/plain', 'application/data-transfer+json', 'application/call-history+json'] as const;
55type UserMessageType = typeof userMessageTypes[number];
56const checkIsUserMessageType = (type: Message['type'] | undefined): type is UserMessageType => {
57 return userMessageTypes.includes(type as UserMessageType);
58};
59
60const checkShowsTime = (time: Dayjs, previousTime: Dayjs) => {
61 return !previousTime.isSame(time) && !time.isBetween(previousTime, previousTime?.add(1, 'minute'));
62};
63
64const findPreviousVisibleMessage = (messages: Message[], messageIndex: number) => {
65 for (let i = messageIndex + 1; i < messages.length; ++i) {
66 const message = messages[i];
67 if (!checkIsInvisibleMessageType(message?.type)) {
68 return message;
69 }
70 }
71};
72
73const findNextVisibleMessage = (messages: Message[], messageIndex: number) => {
74 for (let i = messageIndex - 1; i >= 0; --i) {
75 const message = messages[i];
76 if (!checkIsInvisibleMessageType(message?.type)) {
77 return message;
78 }
79 }
80};
81
82const avatarSize = '22px';
83const spacingBetweenAvatarAndBubble = '10px';
84const bubblePadding = '16px';
85
86interface MessageCallProps {
87 message: Message;
88 isAccountMessage: boolean;
idillon-sfl9d956ab2022-10-20 16:33:24 -040089 isFirstOfGroup: boolean;
90 isLastOfGroup: boolean;
91}
92
idillon-sflec735452022-10-27 13:18:41 -040093const MessageCall = ({ message, isAccountMessage, isFirstOfGroup, isLastOfGroup }: MessageCallProps) => {
idillon-sfl118ae442022-10-25 10:42:54 -040094 const position = isAccountMessage ? 'end' : 'start';
idillon-sflec735452022-10-27 13:18:41 -040095
96 const { t } = useTranslation();
97 const { bubbleColor, Icon, text, textColor } = useMemo(() => {
98 const callDuration = dayjs.duration(parseInt(message?.duration || ''));
99 if (callDuration.asSeconds() === 0) {
100 if (isAccountMessage) {
101 return {
102 text: t('message_call_outgoing_missed'),
103 Icon: ArrowLeftCurved,
104 textColor: 'white',
105 bubbleColor: '#005699' + '80', // opacity 50%
106 };
107 } else {
108 return {
109 text: t('message_call_incoming_missed'),
110 Icon: ArrowLeftCurved,
111 textColor: 'black',
112 bubbleColor: '#C6C6C6',
113 };
114 }
115 } else {
116 const minutes = Math.floor(callDuration.asMinutes()).toString().padStart(2, '0');
117 const seconds = callDuration.format('ss');
118 const interpolations = {
119 duration: `${minutes}:${seconds}`,
120 };
121 if (isAccountMessage) {
122 return {
123 text: t('message_call_outgoing', interpolations),
124 Icon: ArrowRightUp,
125 textColor: 'white',
126 bubbleColor: '#005699',
127 };
128 } else {
129 return {
130 text: t('message_call_incoming', interpolations),
131 Icon: ArrowLeftDown,
132 textcolor: 'black',
133 bubbleColor: '#E5E5E5',
134 };
135 }
136 }
137 }, [isAccountMessage, message, t]);
138
idillonbef18a52022-09-01 01:51:40 -0400139 return (
idillon-sfl118ae442022-10-25 10:42:54 -0400140 <Bubble position={position} isFirstOfGroup={isFirstOfGroup} isLastOfGroup={isLastOfGroup} bubbleColor={bubbleColor}>
idillon-sflec735452022-10-27 13:18:41 -0400141 <Stack direction="row" spacing="10px" alignItems="center">
142 <Icon sx={{ fontSize: '16px', color: textColor }} />
143 <Typography variant="body1" color={textColor} textAlign={position} fontWeight="bold" textTransform="uppercase">
144 {text}
145 </Typography>
146 </Stack>
idillon-sfl118ae442022-10-25 10:42:54 -0400147 </Bubble>
148 );
149};
150
151const MessageInitial = () => {
152 const { t } = useTranslation();
153 return <>{t('message_swarm_created')}</>;
154};
155
156interface MessageDataTransferProps {
157 message: Message;
158 isAccountMessage: boolean;
159 isFirstOfGroup: boolean;
160 isLastOfGroup: boolean;
161}
162
163const MessageDataTransfer = ({ isAccountMessage, isFirstOfGroup, isLastOfGroup }: MessageDataTransferProps) => {
164 const position = isAccountMessage ? 'end' : 'start';
165 return (
166 <Bubble bubbleColor="#E5E5E5" position={position} isFirstOfGroup={isFirstOfGroup} isLastOfGroup={isLastOfGroup}>
simon80b7b3b2022-09-28 17:50:10 -0400167 &quot;data-transfer&quot;
idillon-sfl118ae442022-10-25 10:42:54 -0400168 </Bubble>
simond47ef9e2022-09-28 22:24:28 -0400169 );
170};
idillonbef18a52022-09-01 01:51:40 -0400171
idillon-sfl9d956ab2022-10-20 16:33:24 -0400172interface MessageMemberProps {
173 message: Message;
174}
175
idillon-sfl118ae442022-10-25 10:42:54 -0400176const MessageMember = ({ message }: MessageMemberProps) => {
simond47ef9e2022-09-28 22:24:28 -0400177 const { t } = useTranslation();
idillonbef18a52022-09-01 01:51:40 -0400178 return (
idillon-sfl118ae442022-10-25 10:42:54 -0400179 <Chip
180 sx={{
181 width: 'fit-content',
182 }}
183 label={t('message_user_joined', { user: message.author })}
184 />
simond47ef9e2022-09-28 22:24:28 -0400185 );
186};
idillonbef18a52022-09-01 01:51:40 -0400187
idillon-sfl9d956ab2022-10-20 16:33:24 -0400188interface MessageTextProps {
189 message: Message;
idillon-sfl118ae442022-10-25 10:42:54 -0400190 isAccountMessage: boolean;
idillon-sfl9d956ab2022-10-20 16:33:24 -0400191 isFirstOfGroup: boolean;
192 isLastOfGroup: boolean;
idillon-sfl9d956ab2022-10-20 16:33:24 -0400193}
194
idillon-sfl118ae442022-10-25 10:42:54 -0400195const MessageText = ({ message, isAccountMessage, isFirstOfGroup, isLastOfGroup }: MessageTextProps) => {
196 const position = isAccountMessage ? 'end' : 'start';
197 const bubbleColor = isAccountMessage ? '#005699' : '#E5E5E5';
198 const textColor = isAccountMessage ? 'white' : 'black';
idillonbef18a52022-09-01 01:51:40 -0400199 return (
idillon-sfl118ae442022-10-25 10:42:54 -0400200 <MessageTooltip position={position}>
201 <Bubble
202 bubbleColor={bubbleColor}
203 position={position}
204 isFirstOfGroup={isFirstOfGroup}
205 isLastOfGroup={isLastOfGroup}
206 >
207 <Typography variant="body1" color={textColor} textAlign={position}>
208 {message.body}
209 </Typography>
210 </Bubble>
211 </MessageTooltip>
simond47ef9e2022-09-28 22:24:28 -0400212 );
213};
idillonbef18a52022-09-01 01:51:40 -0400214
idillon-sfl118ae442022-10-25 10:42:54 -0400215interface DateIndicatorProps {
idillon-sfl9d956ab2022-10-20 16:33:24 -0400216 time: Dayjs;
217}
218
idillon-sfl118ae442022-10-25 10:42:54 -0400219const DateIndicator = ({ time }: DateIndicatorProps) => {
idillon-sfl0e1a0d92022-10-25 16:52:44 -0400220 const { i18n } = useTranslation();
idillonbef18a52022-09-01 01:51:40 -0400221
idillon-sfl0e1a0d92022-10-25 16:52:44 -0400222 const textDate = useMemo(() => {
223 if (time.isToday()) {
224 return new Intl.RelativeTimeFormat(i18n.language, { numeric: 'auto' }).format(0, 'day');
225 } else if (time.isYesterday()) {
226 return new Intl.RelativeTimeFormat(i18n.language, { numeric: 'auto' }).format(-1, 'day');
227 } else {
228 return dayjs(time).locale(i18n.language).format('L');
229 }
230 }, [i18n, time]);
idillonbef18a52022-09-01 01:51:40 -0400231
232 return (
simond47ef9e2022-09-28 22:24:28 -0400233 <Box marginTop="30px">
idillon04245a12022-09-01 11:12:17 -0400234 <Divider
235 sx={{
simond47ef9e2022-09-28 22:24:28 -0400236 '.MuiDivider-wrapper': {
idillon04245a12022-09-01 11:12:17 -0400237 margin: 0,
238 padding: 0,
239 },
simond47ef9e2022-09-28 22:24:28 -0400240 '&::before': {
241 borderTop: '1px solid #E5E5E5',
idillon04245a12022-09-01 11:12:17 -0400242 },
simond47ef9e2022-09-28 22:24:28 -0400243 '&::after': {
244 borderTop: '1px solid #E5E5E5',
idillon04245a12022-09-01 11:12:17 -0400245 },
246 }}
247 >
248 <Typography
249 variant="caption"
250 fontWeight={700}
251 border="1px solid #E5E5E5"
252 borderRadius="5px"
253 padding="10px 16px"
idillon-sfl0e1a0d92022-10-25 16:52:44 -0400254 textTransform="capitalize"
idillon04245a12022-09-01 11:12:17 -0400255 >
256 {textDate}
257 </Typography>
idillonbef18a52022-09-01 01:51:40 -0400258 </Divider>
259 </Box>
simond47ef9e2022-09-28 22:24:28 -0400260 );
261};
idillonbef18a52022-09-01 01:51:40 -0400262
idillon-sfl118ae442022-10-25 10:42:54 -0400263interface TimeIndicatorProps {
idillon-sfl9d956ab2022-10-20 16:33:24 -0400264 time: Dayjs;
265 hasDateOnTop: boolean;
266}
267
idillon-sfl118ae442022-10-25 10:42:54 -0400268const TimeIndicator = ({ time, hasDateOnTop }: TimeIndicatorProps) => {
idillon-sfl0e1a0d92022-10-25 16:52:44 -0400269 const { i18n } = useTranslation();
270
271 const textTime = useMemo(() => {
272 return dayjs(time).locale(i18n.language).format('LT');
273 }, [i18n, time]);
idillonbef18a52022-09-01 01:51:40 -0400274
275 return (
idillon-sfl118ae442022-10-25 10:42:54 -0400276 <Stack direction="row" justifyContent="center" marginTop={hasDateOnTop ? '20px' : '30px'}>
simond47ef9e2022-09-28 22:24:28 -0400277 <Typography variant="caption" color="#A7A7A7" fontWeight={700}>
idillonbef18a52022-09-01 01:51:40 -0400278 {textTime}
279 </Typography>
280 </Stack>
simond47ef9e2022-09-28 22:24:28 -0400281 );
282};
idillonbef18a52022-09-01 01:51:40 -0400283
idillon-sfl118ae442022-10-25 10:42:54 -0400284interface NotificationMessageRowProps {
285 message: Message;
idillon-sfl9d956ab2022-10-20 16:33:24 -0400286}
287
idillon-sfl118ae442022-10-25 10:42:54 -0400288const NotificationMessageRow = ({ message }: NotificationMessageRowProps) => {
289 let messageComponent;
290 switch (message.type) {
291 case 'initial':
292 messageComponent = <MessageInitial />;
293 break;
294 case 'member':
295 messageComponent = <MessageMember message={message} />;
296 break;
297 default:
298 console.error(`${NotificationMessageRow.name} received unhandled message type: ${message.type}`);
299 return null;
idillonae655dd2022-10-14 18:11:02 -0400300 }
301
idillonbef18a52022-09-01 01:51:40 -0400302 return (
idillon-sfl118ae442022-10-25 10:42:54 -0400303 <Stack paddingTop={'30px'} alignItems="center">
304 {messageComponent}
305 </Stack>
306 );
307};
308
309interface UserMessageRowProps {
310 message: Message;
311 isAccountMessage: boolean;
312 previousMessage: Message | undefined;
313 nextMessage: Message | undefined;
314 time: Dayjs;
315 showsTime: boolean;
316 author: Account | Contact;
317}
318
319const UserMessageRow = ({
320 message,
321 previousMessage,
322 nextMessage,
323 isAccountMessage,
324 time,
325 showsTime,
326 author,
327}: UserMessageRowProps) => {
328 const authorName = author.getDisplayName();
329 const position = isAccountMessage ? 'end' : 'start';
330
331 const previousIsUserMessageType = checkIsUserMessageType(previousMessage?.type);
332 const nextIsUserMessageType = checkIsUserMessageType(nextMessage?.type);
333 const nextTime = dayjs.unix(Number(nextMessage?.timestamp));
334 const nextShowsTime = checkShowsTime(nextTime, time);
335 const isFirstOfGroup = showsTime || !previousIsUserMessageType || previousMessage?.author !== message.author;
336 const isLastOfGroup = nextShowsTime || !nextIsUserMessageType || message.author !== nextMessage?.author;
337
338 const props = {
339 message,
340 isAccountMessage,
341 isFirstOfGroup,
342 isLastOfGroup,
343 };
344
345 let MessageComponent;
346 switch (message.type) {
347 case 'text/plain':
348 MessageComponent = MessageText;
349 break;
350 case 'application/data-transfer+json':
351 MessageComponent = MessageDataTransfer;
352 break;
353 case 'application/call-history+json':
354 MessageComponent = MessageCall;
355 break;
356 default:
357 console.error(`${UserMessageRow.name} received unhandled message type: ${message.type}`);
358 return null;
359 }
360
361 const participantNamePadding = isAccountMessage
362 ? bubblePadding
363 : parseInt(avatarSize) + parseInt(spacingBetweenAvatarAndBubble) + parseInt(bubblePadding) + 'px';
364
365 return (
366 <Stack alignItems={position}>
367 {isFirstOfGroup && (
368 <Box padding={`30px ${participantNamePadding} 0 ${participantNamePadding}`}>
369 <ParticipantName name={authorName} />
370 </Box>
simond47ef9e2022-09-28 22:24:28 -0400371 )}
idillon-sfl118ae442022-10-25 10:42:54 -0400372 <Stack
373 direction="row"
374 justifyContent={position}
375 alignItems="end"
376 spacing={spacingBetweenAvatarAndBubble}
377 paddingTop="6px"
idillonbef18a52022-09-01 01:51:40 -0400378 width="66.66%"
idillonbef18a52022-09-01 01:51:40 -0400379 >
idillon-sfl118ae442022-10-25 10:42:54 -0400380 <Box sx={{ width: avatarSize }}>
381 {!isAccountMessage && isLastOfGroup && (
382 <ConversationAvatar
383 displayName={authorName}
384 sx={{ width: avatarSize, height: avatarSize, fontSize: '15px' }}
385 />
386 )}
387 </Box>
388 <MessageComponent {...props} />
idillonbef18a52022-09-01 01:51:40 -0400389 </Stack>
390 </Stack>
simond47ef9e2022-09-28 22:24:28 -0400391 );
392};
idillonbef18a52022-09-01 01:51:40 -0400393
idillon-sfl9d956ab2022-10-20 16:33:24 -0400394interface MessageTooltipProps {
395 className?: string;
396 position: MessagePosition;
397 children: ReactElement;
398}
399
400const MessageTooltip = styled(({ className, position, children }: MessageTooltipProps) => {
simond47ef9e2022-09-28 22:24:28 -0400401 const [open, setOpen] = useState(false);
402 const emojis = ['😎', '😄', '😍']; // Should be last three used emojis
idillonef9ab812022-11-18 13:46:24 -0500403 const additionalOptions: PopoverListItemData[] = [
idillon927b7592022-09-15 12:56:45 -0400404 {
405 Icon: TwoSheetsIcon,
idillonef9ab812022-11-18 13:46:24 -0500406 label: 'Copy',
407 onClick: () => {},
idillon927b7592022-09-15 12:56:45 -0400408 },
409 {
410 Icon: OppositeArrowsIcon,
idillonef9ab812022-11-18 13:46:24 -0500411 label: 'Transfer',
412 onClick: () => {},
idillon927b7592022-09-15 12:56:45 -0400413 },
414 {
415 Icon: TrashBinIcon,
idillonef9ab812022-11-18 13:46:24 -0500416 label: 'Delete message',
417 onClick: () => {},
idillon927b7592022-09-15 12:56:45 -0400418 },
simond47ef9e2022-09-28 22:24:28 -0400419 ];
idillon927b7592022-09-15 12:56:45 -0400420
simond47ef9e2022-09-28 22:24:28 -0400421 const toggleMoreMenu = useCallback(() => setOpen((open) => !open), [setOpen]);
idillon927b7592022-09-15 12:56:45 -0400422
simond47ef9e2022-09-28 22:24:28 -0400423 const onClose = useCallback(() => {
424 setOpen(false);
425 }, [setOpen]);
idillon927b7592022-09-15 12:56:45 -0400426
427 return (
428 <Tooltip
idillon927b7592022-09-15 12:56:45 -0400429 classes={{ tooltip: className }} // Required for styles. Don't know why
idillon-sfl9d956ab2022-10-20 16:33:24 -0400430 placement={position === 'start' ? 'right-start' : 'left-start'}
idillon927b7592022-09-15 12:56:45 -0400431 PopperProps={{
432 modifiers: [
433 {
simond47ef9e2022-09-28 22:24:28 -0400434 name: 'offset',
idillon927b7592022-09-15 12:56:45 -0400435 options: {
simond47ef9e2022-09-28 22:24:28 -0400436 offset: [-2, -30],
idillon927b7592022-09-15 12:56:45 -0400437 },
438 },
439 ],
440 }}
441 onClose={onClose}
442 title={
simond47ef9e2022-09-28 22:24:28 -0400443 <Stack>
idillon927b7592022-09-15 12:56:45 -0400444 <Stack // Main options
445 direction="row"
446 spacing="16px"
idillonef9ab812022-11-18 13:46:24 -0500447 padding="16px"
idillon927b7592022-09-15 12:56:45 -0400448 >
simond47ef9e2022-09-28 22:24:28 -0400449 {emojis.map((emoji) => (
450 <EmojiButton key={emoji} emoji={emoji} />
451 ))}
452 <ReplyMessageButton />
453 <MoreButton onClick={toggleMoreMenu} />
idillon927b7592022-09-15 12:56:45 -0400454 </Stack>
idillonef9ab812022-11-18 13:46:24 -0500455 {open && (
idillon927b7592022-09-15 12:56:45 -0400456 <>
idillonef9ab812022-11-18 13:46:24 -0500457 <Divider sx={{ marginX: '16px' }} />
458 <PopoverList items={additionalOptions} />
idillon927b7592022-09-15 12:56:45 -0400459 </>
simond47ef9e2022-09-28 22:24:28 -0400460 )}
idillon927b7592022-09-15 12:56:45 -0400461 </Stack>
462 }
idillon-sfl9d956ab2022-10-20 16:33:24 -0400463 >
idillon-sfl118ae442022-10-25 10:42:54 -0400464 {/* div fixes 'Function components cannot be given refs' error */}
465 <div>{children}</div>
idillon-sfl9d956ab2022-10-20 16:33:24 -0400466 </Tooltip>
simond47ef9e2022-09-28 22:24:28 -0400467 );
idillon-sfl9d956ab2022-10-20 16:33:24 -0400468})(({ position }) => {
simond47ef9e2022-09-28 22:24:28 -0400469 const largeRadius = '20px';
470 const smallRadius = '5px';
idillon927b7592022-09-15 12:56:45 -0400471 return {
simond47ef9e2022-09-28 22:24:28 -0400472 backgroundColor: 'white',
idillonef9ab812022-11-18 13:46:24 -0500473 padding: '0px',
simond47ef9e2022-09-28 22:24:28 -0400474 boxShadow: '3px 3px 7px #00000029',
idillon927b7592022-09-15 12:56:45 -0400475 borderRadius: largeRadius,
idillon-sfl9d956ab2022-10-20 16:33:24 -0400476 borderStartStartRadius: position === 'start' ? smallRadius : largeRadius,
477 borderStartEndRadius: position === 'end' ? smallRadius : largeRadius,
idillonef9ab812022-11-18 13:46:24 -0500478 overflow: 'hidden',
simond47ef9e2022-09-28 22:24:28 -0400479 };
idillon927b7592022-09-15 12:56:45 -0400480});
481
idillon-sfl118ae442022-10-25 10:42:54 -0400482interface BubbleProps {
idillon-sfl9d956ab2022-10-20 16:33:24 -0400483 position: MessagePosition;
484 isFirstOfGroup: boolean;
485 isLastOfGroup: boolean;
idillon-sfl118ae442022-10-25 10:42:54 -0400486 bubbleColor: string;
idillon-sfl9d956ab2022-10-20 16:33:24 -0400487 children: ReactNode;
488}
489
idillon-sfl118ae442022-10-25 10:42:54 -0400490const Bubble = ({ position, isFirstOfGroup, isLastOfGroup, bubbleColor, children }: BubbleProps) => {
simond47ef9e2022-09-28 22:24:28 -0400491 const largeRadius = '20px';
492 const smallRadius = '5px';
Adrien Béraud023f7cf2022-09-18 14:57:53 -0400493 const radius = useMemo(() => {
idillon-sfl9d956ab2022-10-20 16:33:24 -0400494 if (position === 'start') {
idillonbef18a52022-09-01 01:51:40 -0400495 return {
idillon-sfl9d956ab2022-10-20 16:33:24 -0400496 borderStartStartRadius: isFirstOfGroup ? largeRadius : smallRadius,
idillonbef18a52022-09-01 01:51:40 -0400497 borderStartEndRadius: largeRadius,
idillon-sfl9d956ab2022-10-20 16:33:24 -0400498 borderEndStartRadius: isLastOfGroup ? largeRadius : smallRadius,
idillonbef18a52022-09-01 01:51:40 -0400499 borderEndEndRadius: largeRadius,
simond47ef9e2022-09-28 22:24:28 -0400500 };
idillonbef18a52022-09-01 01:51:40 -0400501 }
502 return {
503 borderStartStartRadius: largeRadius,
idillon-sfl9d956ab2022-10-20 16:33:24 -0400504 borderStartEndRadius: isFirstOfGroup ? largeRadius : smallRadius,
idillonbef18a52022-09-01 01:51:40 -0400505 borderEndStartRadius: largeRadius,
idillon-sfl9d956ab2022-10-20 16:33:24 -0400506 borderEndEndRadius: isLastOfGroup ? largeRadius : smallRadius,
simond47ef9e2022-09-28 22:24:28 -0400507 };
idillon-sfl9d956ab2022-10-20 16:33:24 -0400508 }, [isFirstOfGroup, isLastOfGroup, position]);
idillonbef18a52022-09-01 01:51:40 -0400509
510 return (
idillon-sfl118ae442022-10-25 10:42:54 -0400511 <Box
512 sx={{
513 width: 'fit-content',
514 backgroundColor: bubbleColor,
515 padding: bubblePadding,
516 ...radius,
517 }}
518 >
519 {children}
520 </Box>
simond47ef9e2022-09-28 22:24:28 -0400521 );
522};
idillonbef18a52022-09-01 01:51:40 -0400523
idillon-sfl9d956ab2022-10-20 16:33:24 -0400524interface ParticipantNameProps {
525 name: string;
526}
527
528const ParticipantName = ({ name }: ParticipantNameProps) => {
idillonbef18a52022-09-01 01:51:40 -0400529 return (
idillon-sfl118ae442022-10-25 10:42:54 -0400530 <Typography variant="caption" color="#A7A7A7" fontWeight={700}>
531 {name}
532 </Typography>
533 );
534};
535
536interface MessageProps {
537 messageIndex: number;
538 messages: Message[];
539 isAccountMessage: boolean;
540 author: Account | Contact;
541}
542
543export const MessageRow = ({ messageIndex, messages, isAccountMessage, author }: MessageProps) => {
544 const message = messages[messageIndex];
545 const previousMessage = findPreviousVisibleMessage(messages, messageIndex);
546 const nextMessage = findNextVisibleMessage(messages, messageIndex);
547 const time = dayjs.unix(Number(message.timestamp));
548 const previousTime = dayjs.unix(Number(previousMessage?.timestamp));
549 const showDate =
550 message?.type === 'initial' || previousTime.year() !== time.year() || previousTime.dayOfYear() !== time.dayOfYear();
551 const showTime = checkShowsTime(time, previousTime);
552 let messageComponent;
553 if (checkIsUserMessageType(message.type)) {
554 messageComponent = (
555 <UserMessageRow
556 message={message}
557 previousMessage={previousMessage}
558 nextMessage={nextMessage}
559 time={time}
560 showsTime={showTime}
561 isAccountMessage={isAccountMessage}
562 author={author}
563 />
564 );
565 } else if (checkIsNotificationMessageType(message.type)) {
566 messageComponent = <NotificationMessageRow message={message} />;
567 } else if (checkIsInvisibleMessageType(message.type)) {
568 return null;
569 } else {
570 const _exhaustiveCheck: never = message.type;
571 return _exhaustiveCheck;
572 }
573
574 return (
575 <Stack>
576 {showDate && <DateIndicator time={time} />}
577 {showTime && <TimeIndicator time={time} hasDateOnTop={showDate} />}
578 {messageComponent}
579 </Stack>
simond47ef9e2022-09-28 22:24:28 -0400580 );
581};