blob: a87d628a9747f9b6341c80843d564439ea1e0d22 [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';
idillon9e542ca2022-12-15 17:54:07 -050020import dayjs, { 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
Misha Krieger-Raynauldcfa44302022-11-30 18:36:36 -050025import { Account } from '../models/account';
26import { Contact } from '../models/contact';
idillon9e542ca2022-12-15 17:54:07 -050027import { getMessageCallText, getMessageMemberText } from '../utils/chatmessages';
28import { formatRelativeDate, formatTime } from '../utils/dates&times';
idillon-sfl9d956ab2022-10-20 16:33:24 -040029import { EmojiButton, MoreButton, ReplyMessageButton } from './Button';
simond47ef9e2022-09-28 22:24:28 -040030import ConversationAvatar from './ConversationAvatar';
idillonef9ab812022-11-18 13:46:24 -050031import PopoverList, { PopoverListItemData } from './PopoverList';
idillon-sflec735452022-10-27 13:18:41 -040032import {
33 ArrowLeftCurved,
34 ArrowLeftDown,
35 ArrowRightUp,
36 OppositeArrowsIcon,
37 TrashBinIcon,
38 TwoSheetsIcon,
39} from './SvgIcon';
Larbi Gharibe9af9732021-03-31 15:08:01 +010040
idillon-sfl9d956ab2022-10-20 16:33:24 -040041type MessagePosition = 'start' | 'end';
42
idillon-sfl118ae442022-10-25 10:42:54 -040043const notificationMessageTypes = ['initial', 'member'] as const;
44type NotificationMessageType = typeof notificationMessageTypes[number];
45const checkIsNotificationMessageType = (type: Message['type'] | undefined): type is NotificationMessageType => {
46 return notificationMessageTypes.includes(type as NotificationMessageType);
simond47ef9e2022-09-28 22:24:28 -040047};
Larbi Gharibe9af9732021-03-31 15:08:01 +010048
idillon-sfl118ae442022-10-25 10:42:54 -040049const invisibleMessageTypes = ['application/update-profile', 'merge', 'vote'] as const;
50type InvisibleMessageType = typeof invisibleMessageTypes[number];
51const checkIsInvisibleMessageType = (type: Message['type'] | undefined): type is InvisibleMessageType => {
52 return invisibleMessageTypes.includes(type as InvisibleMessageType);
simond47ef9e2022-09-28 22:24:28 -040053};
idillonbef18a52022-09-01 01:51:40 -040054
idillon-sfl118ae442022-10-25 10:42:54 -040055const userMessageTypes = ['text/plain', 'application/data-transfer+json', 'application/call-history+json'] as const;
56type UserMessageType = typeof userMessageTypes[number];
57const checkIsUserMessageType = (type: Message['type'] | undefined): type is UserMessageType => {
58 return userMessageTypes.includes(type as UserMessageType);
59};
60
61const checkShowsTime = (time: Dayjs, previousTime: Dayjs) => {
62 return !previousTime.isSame(time) && !time.isBetween(previousTime, previousTime?.add(1, 'minute'));
63};
64
65const findPreviousVisibleMessage = (messages: Message[], messageIndex: number) => {
66 for (let i = messageIndex + 1; i < messages.length; ++i) {
67 const message = messages[i];
68 if (!checkIsInvisibleMessageType(message?.type)) {
69 return message;
70 }
71 }
72};
73
74const findNextVisibleMessage = (messages: Message[], messageIndex: number) => {
75 for (let i = messageIndex - 1; i >= 0; --i) {
76 const message = messages[i];
77 if (!checkIsInvisibleMessageType(message?.type)) {
78 return message;
79 }
80 }
81};
82
83const avatarSize = '22px';
84const spacingBetweenAvatarAndBubble = '10px';
85const bubblePadding = '16px';
86
87interface MessageCallProps {
88 message: Message;
89 isAccountMessage: boolean;
idillon-sfl9d956ab2022-10-20 16:33:24 -040090 isFirstOfGroup: boolean;
91 isLastOfGroup: boolean;
92}
93
idillon-sflec735452022-10-27 13:18:41 -040094const MessageCall = ({ message, isAccountMessage, isFirstOfGroup, isLastOfGroup }: MessageCallProps) => {
idillon-sfl118ae442022-10-25 10:42:54 -040095 const position = isAccountMessage ? 'end' : 'start';
idillon-sflec735452022-10-27 13:18:41 -040096
idillon9e542ca2022-12-15 17:54:07 -050097 const { i18n } = useTranslation();
idillon-sflec735452022-10-27 13:18:41 -040098 const { bubbleColor, Icon, text, textColor } = useMemo(() => {
idillon9e542ca2022-12-15 17:54:07 -050099 const text = getMessageCallText(isAccountMessage, message, i18n);
idillon-sflec735452022-10-27 13:18:41 -0400100 const callDuration = dayjs.duration(parseInt(message?.duration || ''));
101 if (callDuration.asSeconds() === 0) {
102 if (isAccountMessage) {
103 return {
idillon9e542ca2022-12-15 17:54:07 -0500104 text,
idillon-sflec735452022-10-27 13:18:41 -0400105 Icon: ArrowLeftCurved,
106 textColor: 'white',
107 bubbleColor: '#005699' + '80', // opacity 50%
108 };
109 } else {
110 return {
idillon9e542ca2022-12-15 17:54:07 -0500111 text,
idillon-sflec735452022-10-27 13:18:41 -0400112 Icon: ArrowLeftCurved,
113 textColor: 'black',
114 bubbleColor: '#C6C6C6',
115 };
116 }
117 } else {
idillon-sflec735452022-10-27 13:18:41 -0400118 if (isAccountMessage) {
119 return {
idillon9e542ca2022-12-15 17:54:07 -0500120 text,
idillon-sflec735452022-10-27 13:18:41 -0400121 Icon: ArrowRightUp,
122 textColor: 'white',
123 bubbleColor: '#005699',
124 };
125 } else {
126 return {
idillon9e542ca2022-12-15 17:54:07 -0500127 text,
idillon-sflec735452022-10-27 13:18:41 -0400128 Icon: ArrowLeftDown,
129 textcolor: 'black',
130 bubbleColor: '#E5E5E5',
131 };
132 }
133 }
idillon9e542ca2022-12-15 17:54:07 -0500134 }, [isAccountMessage, message, i18n]);
idillon-sflec735452022-10-27 13:18:41 -0400135
idillonbef18a52022-09-01 01:51:40 -0400136 return (
idillon-sfl118ae442022-10-25 10:42:54 -0400137 <Bubble position={position} isFirstOfGroup={isFirstOfGroup} isLastOfGroup={isLastOfGroup} bubbleColor={bubbleColor}>
idillon-sflec735452022-10-27 13:18:41 -0400138 <Stack direction="row" spacing="10px" alignItems="center">
139 <Icon sx={{ fontSize: '16px', color: textColor }} />
140 <Typography variant="body1" color={textColor} textAlign={position} fontWeight="bold" textTransform="uppercase">
141 {text}
142 </Typography>
143 </Stack>
idillon-sfl118ae442022-10-25 10:42:54 -0400144 </Bubble>
145 );
146};
147
148const MessageInitial = () => {
149 const { t } = useTranslation();
150 return <>{t('message_swarm_created')}</>;
151};
152
153interface MessageDataTransferProps {
154 message: Message;
155 isAccountMessage: boolean;
156 isFirstOfGroup: boolean;
157 isLastOfGroup: boolean;
158}
159
160const MessageDataTransfer = ({ isAccountMessage, isFirstOfGroup, isLastOfGroup }: MessageDataTransferProps) => {
161 const position = isAccountMessage ? 'end' : 'start';
162 return (
163 <Bubble bubbleColor="#E5E5E5" position={position} isFirstOfGroup={isFirstOfGroup} isLastOfGroup={isLastOfGroup}>
simon80b7b3b2022-09-28 17:50:10 -0400164 &quot;data-transfer&quot;
idillon-sfl118ae442022-10-25 10:42:54 -0400165 </Bubble>
simond47ef9e2022-09-28 22:24:28 -0400166 );
167};
idillonbef18a52022-09-01 01:51:40 -0400168
idillon-sfl9d956ab2022-10-20 16:33:24 -0400169interface MessageMemberProps {
170 message: Message;
171}
172
idillon-sfl118ae442022-10-25 10:42:54 -0400173const MessageMember = ({ message }: MessageMemberProps) => {
idillon9e542ca2022-12-15 17:54:07 -0500174 const { i18n } = useTranslation();
175
176 const text = getMessageMemberText(message, i18n);
idillonbef18a52022-09-01 01:51:40 -0400177 return (
idillon-sfl118ae442022-10-25 10:42:54 -0400178 <Chip
179 sx={{
180 width: 'fit-content',
181 }}
idillon9e542ca2022-12-15 17:54:07 -0500182 label={text}
idillon-sfl118ae442022-10-25 10:42:54 -0400183 />
simond47ef9e2022-09-28 22:24:28 -0400184 );
185};
idillonbef18a52022-09-01 01:51:40 -0400186
idillon-sfl9d956ab2022-10-20 16:33:24 -0400187interface MessageTextProps {
188 message: Message;
idillon-sfl118ae442022-10-25 10:42:54 -0400189 isAccountMessage: boolean;
idillon-sfl9d956ab2022-10-20 16:33:24 -0400190 isFirstOfGroup: boolean;
191 isLastOfGroup: boolean;
idillon-sfl9d956ab2022-10-20 16:33:24 -0400192}
193
idillon-sfl118ae442022-10-25 10:42:54 -0400194const MessageText = ({ message, isAccountMessage, isFirstOfGroup, isLastOfGroup }: MessageTextProps) => {
195 const position = isAccountMessage ? 'end' : 'start';
196 const bubbleColor = isAccountMessage ? '#005699' : '#E5E5E5';
197 const textColor = isAccountMessage ? 'white' : 'black';
idillonbef18a52022-09-01 01:51:40 -0400198 return (
idillon-sfl118ae442022-10-25 10:42:54 -0400199 <MessageTooltip position={position}>
200 <Bubble
201 bubbleColor={bubbleColor}
202 position={position}
203 isFirstOfGroup={isFirstOfGroup}
204 isLastOfGroup={isLastOfGroup}
205 >
206 <Typography variant="body1" color={textColor} textAlign={position}>
207 {message.body}
208 </Typography>
209 </Bubble>
210 </MessageTooltip>
simond47ef9e2022-09-28 22:24:28 -0400211 );
212};
idillonbef18a52022-09-01 01:51:40 -0400213
idillon-sfl118ae442022-10-25 10:42:54 -0400214interface DateIndicatorProps {
idillon-sfl9d956ab2022-10-20 16:33:24 -0400215 time: Dayjs;
216}
217
idillon-sfl118ae442022-10-25 10:42:54 -0400218const DateIndicator = ({ time }: DateIndicatorProps) => {
idillon-sfl0e1a0d92022-10-25 16:52:44 -0400219 const { i18n } = useTranslation();
idillon9e542ca2022-12-15 17:54:07 -0500220 const textDate = useMemo(() => formatRelativeDate(time, i18n), [time, i18n]);
idillonbef18a52022-09-01 01:51:40 -0400221
222 return (
simond47ef9e2022-09-28 22:24:28 -0400223 <Box marginTop="30px">
idillon04245a12022-09-01 11:12:17 -0400224 <Divider
225 sx={{
simond47ef9e2022-09-28 22:24:28 -0400226 '.MuiDivider-wrapper': {
idillon04245a12022-09-01 11:12:17 -0400227 margin: 0,
228 padding: 0,
229 },
simond47ef9e2022-09-28 22:24:28 -0400230 '&::before': {
231 borderTop: '1px solid #E5E5E5',
idillon04245a12022-09-01 11:12:17 -0400232 },
simond47ef9e2022-09-28 22:24:28 -0400233 '&::after': {
234 borderTop: '1px solid #E5E5E5',
idillon04245a12022-09-01 11:12:17 -0400235 },
236 }}
237 >
238 <Typography
239 variant="caption"
240 fontWeight={700}
241 border="1px solid #E5E5E5"
242 borderRadius="5px"
243 padding="10px 16px"
idillon-sfl0e1a0d92022-10-25 16:52:44 -0400244 textTransform="capitalize"
idillon04245a12022-09-01 11:12:17 -0400245 >
246 {textDate}
247 </Typography>
idillonbef18a52022-09-01 01:51:40 -0400248 </Divider>
249 </Box>
simond47ef9e2022-09-28 22:24:28 -0400250 );
251};
idillonbef18a52022-09-01 01:51:40 -0400252
idillon-sfl118ae442022-10-25 10:42:54 -0400253interface TimeIndicatorProps {
idillon-sfl9d956ab2022-10-20 16:33:24 -0400254 time: Dayjs;
255 hasDateOnTop: boolean;
256}
257
idillon-sfl118ae442022-10-25 10:42:54 -0400258const TimeIndicator = ({ time, hasDateOnTop }: TimeIndicatorProps) => {
idillon-sfl0e1a0d92022-10-25 16:52:44 -0400259 const { i18n } = useTranslation();
idillon9e542ca2022-12-15 17:54:07 -0500260 const textTime = useMemo(() => formatTime(time, i18n), [time, i18n]);
idillonbef18a52022-09-01 01:51:40 -0400261
262 return (
idillon-sfl118ae442022-10-25 10:42:54 -0400263 <Stack direction="row" justifyContent="center" marginTop={hasDateOnTop ? '20px' : '30px'}>
simond47ef9e2022-09-28 22:24:28 -0400264 <Typography variant="caption" color="#A7A7A7" fontWeight={700}>
idillonbef18a52022-09-01 01:51:40 -0400265 {textTime}
266 </Typography>
267 </Stack>
simond47ef9e2022-09-28 22:24:28 -0400268 );
269};
idillonbef18a52022-09-01 01:51:40 -0400270
idillon-sfl118ae442022-10-25 10:42:54 -0400271interface NotificationMessageRowProps {
272 message: Message;
idillon-sfl9d956ab2022-10-20 16:33:24 -0400273}
274
idillon-sfl118ae442022-10-25 10:42:54 -0400275const NotificationMessageRow = ({ message }: NotificationMessageRowProps) => {
276 let messageComponent;
277 switch (message.type) {
278 case 'initial':
279 messageComponent = <MessageInitial />;
280 break;
281 case 'member':
282 messageComponent = <MessageMember message={message} />;
283 break;
284 default:
285 console.error(`${NotificationMessageRow.name} received unhandled message type: ${message.type}`);
286 return null;
idillonae655dd2022-10-14 18:11:02 -0400287 }
288
idillonbef18a52022-09-01 01:51:40 -0400289 return (
idillon-sfl118ae442022-10-25 10:42:54 -0400290 <Stack paddingTop={'30px'} alignItems="center">
291 {messageComponent}
292 </Stack>
293 );
294};
295
296interface UserMessageRowProps {
297 message: Message;
298 isAccountMessage: boolean;
299 previousMessage: Message | undefined;
300 nextMessage: Message | undefined;
301 time: Dayjs;
302 showsTime: boolean;
303 author: Account | Contact;
304}
305
306const UserMessageRow = ({
307 message,
308 previousMessage,
309 nextMessage,
310 isAccountMessage,
311 time,
312 showsTime,
313 author,
314}: UserMessageRowProps) => {
315 const authorName = author.getDisplayName();
316 const position = isAccountMessage ? 'end' : 'start';
317
318 const previousIsUserMessageType = checkIsUserMessageType(previousMessage?.type);
319 const nextIsUserMessageType = checkIsUserMessageType(nextMessage?.type);
320 const nextTime = dayjs.unix(Number(nextMessage?.timestamp));
321 const nextShowsTime = checkShowsTime(nextTime, time);
322 const isFirstOfGroup = showsTime || !previousIsUserMessageType || previousMessage?.author !== message.author;
323 const isLastOfGroup = nextShowsTime || !nextIsUserMessageType || message.author !== nextMessage?.author;
324
325 const props = {
326 message,
327 isAccountMessage,
328 isFirstOfGroup,
329 isLastOfGroup,
330 };
331
332 let MessageComponent;
333 switch (message.type) {
334 case 'text/plain':
335 MessageComponent = MessageText;
336 break;
337 case 'application/data-transfer+json':
338 MessageComponent = MessageDataTransfer;
339 break;
340 case 'application/call-history+json':
341 MessageComponent = MessageCall;
342 break;
343 default:
344 console.error(`${UserMessageRow.name} received unhandled message type: ${message.type}`);
345 return null;
346 }
347
348 const participantNamePadding = isAccountMessage
349 ? bubblePadding
350 : parseInt(avatarSize) + parseInt(spacingBetweenAvatarAndBubble) + parseInt(bubblePadding) + 'px';
351
352 return (
353 <Stack alignItems={position}>
354 {isFirstOfGroup && (
355 <Box padding={`30px ${participantNamePadding} 0 ${participantNamePadding}`}>
356 <ParticipantName name={authorName} />
357 </Box>
simond47ef9e2022-09-28 22:24:28 -0400358 )}
idillon-sfl118ae442022-10-25 10:42:54 -0400359 <Stack
360 direction="row"
361 justifyContent={position}
362 alignItems="end"
363 spacing={spacingBetweenAvatarAndBubble}
364 paddingTop="6px"
idillonbef18a52022-09-01 01:51:40 -0400365 width="66.66%"
idillonbef18a52022-09-01 01:51:40 -0400366 >
idillon-sfl118ae442022-10-25 10:42:54 -0400367 <Box sx={{ width: avatarSize }}>
368 {!isAccountMessage && isLastOfGroup && (
369 <ConversationAvatar
370 displayName={authorName}
371 sx={{ width: avatarSize, height: avatarSize, fontSize: '15px' }}
372 />
373 )}
374 </Box>
375 <MessageComponent {...props} />
idillonbef18a52022-09-01 01:51:40 -0400376 </Stack>
377 </Stack>
simond47ef9e2022-09-28 22:24:28 -0400378 );
379};
idillonbef18a52022-09-01 01:51:40 -0400380
idillon-sfl9d956ab2022-10-20 16:33:24 -0400381interface MessageTooltipProps {
382 className?: string;
383 position: MessagePosition;
384 children: ReactElement;
385}
386
387const MessageTooltip = styled(({ className, position, children }: MessageTooltipProps) => {
simond47ef9e2022-09-28 22:24:28 -0400388 const [open, setOpen] = useState(false);
389 const emojis = ['😎', '😄', '😍']; // Should be last three used emojis
idillonef9ab812022-11-18 13:46:24 -0500390 const additionalOptions: PopoverListItemData[] = [
idillon927b7592022-09-15 12:56:45 -0400391 {
392 Icon: TwoSheetsIcon,
idillonef9ab812022-11-18 13:46:24 -0500393 label: 'Copy',
394 onClick: () => {},
idillon927b7592022-09-15 12:56:45 -0400395 },
396 {
397 Icon: OppositeArrowsIcon,
idillonef9ab812022-11-18 13:46:24 -0500398 label: 'Transfer',
399 onClick: () => {},
idillon927b7592022-09-15 12:56:45 -0400400 },
401 {
402 Icon: TrashBinIcon,
idillonef9ab812022-11-18 13:46:24 -0500403 label: 'Delete message',
404 onClick: () => {},
idillon927b7592022-09-15 12:56:45 -0400405 },
simond47ef9e2022-09-28 22:24:28 -0400406 ];
idillon927b7592022-09-15 12:56:45 -0400407
simond47ef9e2022-09-28 22:24:28 -0400408 const toggleMoreMenu = useCallback(() => setOpen((open) => !open), [setOpen]);
idillon927b7592022-09-15 12:56:45 -0400409
simond47ef9e2022-09-28 22:24:28 -0400410 const onClose = useCallback(() => {
411 setOpen(false);
412 }, [setOpen]);
idillon927b7592022-09-15 12:56:45 -0400413
414 return (
415 <Tooltip
idillon927b7592022-09-15 12:56:45 -0400416 classes={{ tooltip: className }} // Required for styles. Don't know why
idillon-sfl9d956ab2022-10-20 16:33:24 -0400417 placement={position === 'start' ? 'right-start' : 'left-start'}
idillon927b7592022-09-15 12:56:45 -0400418 PopperProps={{
419 modifiers: [
420 {
simond47ef9e2022-09-28 22:24:28 -0400421 name: 'offset',
idillon927b7592022-09-15 12:56:45 -0400422 options: {
simond47ef9e2022-09-28 22:24:28 -0400423 offset: [-2, -30],
idillon927b7592022-09-15 12:56:45 -0400424 },
425 },
426 ],
427 }}
428 onClose={onClose}
429 title={
simond47ef9e2022-09-28 22:24:28 -0400430 <Stack>
idillon927b7592022-09-15 12:56:45 -0400431 <Stack // Main options
432 direction="row"
433 spacing="16px"
idillonef9ab812022-11-18 13:46:24 -0500434 padding="16px"
idillon927b7592022-09-15 12:56:45 -0400435 >
simond47ef9e2022-09-28 22:24:28 -0400436 {emojis.map((emoji) => (
437 <EmojiButton key={emoji} emoji={emoji} />
438 ))}
439 <ReplyMessageButton />
440 <MoreButton onClick={toggleMoreMenu} />
idillon927b7592022-09-15 12:56:45 -0400441 </Stack>
idillonef9ab812022-11-18 13:46:24 -0500442 {open && (
idillon927b7592022-09-15 12:56:45 -0400443 <>
idillonef9ab812022-11-18 13:46:24 -0500444 <Divider sx={{ marginX: '16px' }} />
445 <PopoverList items={additionalOptions} />
idillon927b7592022-09-15 12:56:45 -0400446 </>
simond47ef9e2022-09-28 22:24:28 -0400447 )}
idillon927b7592022-09-15 12:56:45 -0400448 </Stack>
449 }
idillon-sfl9d956ab2022-10-20 16:33:24 -0400450 >
idillon-sfl118ae442022-10-25 10:42:54 -0400451 {/* div fixes 'Function components cannot be given refs' error */}
452 <div>{children}</div>
idillon-sfl9d956ab2022-10-20 16:33:24 -0400453 </Tooltip>
simond47ef9e2022-09-28 22:24:28 -0400454 );
idillon-sfl9d956ab2022-10-20 16:33:24 -0400455})(({ position }) => {
simond47ef9e2022-09-28 22:24:28 -0400456 const largeRadius = '20px';
457 const smallRadius = '5px';
idillon927b7592022-09-15 12:56:45 -0400458 return {
simond47ef9e2022-09-28 22:24:28 -0400459 backgroundColor: 'white',
idillonef9ab812022-11-18 13:46:24 -0500460 padding: '0px',
simond47ef9e2022-09-28 22:24:28 -0400461 boxShadow: '3px 3px 7px #00000029',
idillon927b7592022-09-15 12:56:45 -0400462 borderRadius: largeRadius,
idillon-sfl9d956ab2022-10-20 16:33:24 -0400463 borderStartStartRadius: position === 'start' ? smallRadius : largeRadius,
464 borderStartEndRadius: position === 'end' ? smallRadius : largeRadius,
idillonef9ab812022-11-18 13:46:24 -0500465 overflow: 'hidden',
simond47ef9e2022-09-28 22:24:28 -0400466 };
idillon927b7592022-09-15 12:56:45 -0400467});
468
idillon-sfl118ae442022-10-25 10:42:54 -0400469interface BubbleProps {
idillon-sfl9d956ab2022-10-20 16:33:24 -0400470 position: MessagePosition;
471 isFirstOfGroup: boolean;
472 isLastOfGroup: boolean;
idillon-sfl118ae442022-10-25 10:42:54 -0400473 bubbleColor: string;
idillon-sfl9d956ab2022-10-20 16:33:24 -0400474 children: ReactNode;
475}
476
idillon-sfl118ae442022-10-25 10:42:54 -0400477const Bubble = ({ position, isFirstOfGroup, isLastOfGroup, bubbleColor, children }: BubbleProps) => {
simond47ef9e2022-09-28 22:24:28 -0400478 const largeRadius = '20px';
479 const smallRadius = '5px';
Adrien Béraud023f7cf2022-09-18 14:57:53 -0400480 const radius = useMemo(() => {
idillon-sfl9d956ab2022-10-20 16:33:24 -0400481 if (position === 'start') {
idillonbef18a52022-09-01 01:51:40 -0400482 return {
idillon-sfl9d956ab2022-10-20 16:33:24 -0400483 borderStartStartRadius: isFirstOfGroup ? largeRadius : smallRadius,
idillonbef18a52022-09-01 01:51:40 -0400484 borderStartEndRadius: largeRadius,
idillon-sfl9d956ab2022-10-20 16:33:24 -0400485 borderEndStartRadius: isLastOfGroup ? largeRadius : smallRadius,
idillonbef18a52022-09-01 01:51:40 -0400486 borderEndEndRadius: largeRadius,
simond47ef9e2022-09-28 22:24:28 -0400487 };
idillonbef18a52022-09-01 01:51:40 -0400488 }
489 return {
490 borderStartStartRadius: largeRadius,
idillon-sfl9d956ab2022-10-20 16:33:24 -0400491 borderStartEndRadius: isFirstOfGroup ? largeRadius : smallRadius,
idillonbef18a52022-09-01 01:51:40 -0400492 borderEndStartRadius: largeRadius,
idillon-sfl9d956ab2022-10-20 16:33:24 -0400493 borderEndEndRadius: isLastOfGroup ? largeRadius : smallRadius,
simond47ef9e2022-09-28 22:24:28 -0400494 };
idillon-sfl9d956ab2022-10-20 16:33:24 -0400495 }, [isFirstOfGroup, isLastOfGroup, position]);
idillonbef18a52022-09-01 01:51:40 -0400496
497 return (
idillon-sfl118ae442022-10-25 10:42:54 -0400498 <Box
499 sx={{
500 width: 'fit-content',
501 backgroundColor: bubbleColor,
502 padding: bubblePadding,
503 ...radius,
504 }}
505 >
506 {children}
507 </Box>
simond47ef9e2022-09-28 22:24:28 -0400508 );
509};
idillonbef18a52022-09-01 01:51:40 -0400510
idillon-sfl9d956ab2022-10-20 16:33:24 -0400511interface ParticipantNameProps {
512 name: string;
513}
514
515const ParticipantName = ({ name }: ParticipantNameProps) => {
idillonbef18a52022-09-01 01:51:40 -0400516 return (
idillon-sfl118ae442022-10-25 10:42:54 -0400517 <Typography variant="caption" color="#A7A7A7" fontWeight={700}>
518 {name}
519 </Typography>
520 );
521};
522
523interface MessageProps {
524 messageIndex: number;
525 messages: Message[];
526 isAccountMessage: boolean;
527 author: Account | Contact;
528}
529
530export const MessageRow = ({ messageIndex, messages, isAccountMessage, author }: MessageProps) => {
531 const message = messages[messageIndex];
532 const previousMessage = findPreviousVisibleMessage(messages, messageIndex);
533 const nextMessage = findNextVisibleMessage(messages, messageIndex);
534 const time = dayjs.unix(Number(message.timestamp));
535 const previousTime = dayjs.unix(Number(previousMessage?.timestamp));
536 const showDate =
537 message?.type === 'initial' || previousTime.year() !== time.year() || previousTime.dayOfYear() !== time.dayOfYear();
538 const showTime = checkShowsTime(time, previousTime);
539 let messageComponent;
540 if (checkIsUserMessageType(message.type)) {
541 messageComponent = (
542 <UserMessageRow
543 message={message}
544 previousMessage={previousMessage}
545 nextMessage={nextMessage}
546 time={time}
547 showsTime={showTime}
548 isAccountMessage={isAccountMessage}
549 author={author}
550 />
551 );
552 } else if (checkIsNotificationMessageType(message.type)) {
553 messageComponent = <NotificationMessageRow message={message} />;
554 } else if (checkIsInvisibleMessageType(message.type)) {
555 return null;
556 } else {
557 const _exhaustiveCheck: never = message.type;
558 return _exhaustiveCheck;
559 }
560
561 return (
562 <Stack>
563 {showDate && <DateIndicator time={time} />}
564 {showTime && <TimeIndicator time={time} hasDateOnTop={showDate} />}
565 {messageComponent}
566 </Stack>
simond47ef9e2022-09-28 22:24:28 -0400567 );
568};