blob: 30339c93dfe83c746fa5dd43ad50beb22b6f1ea2 [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 */
idillona3c2fad2022-12-18 23:49:10 -050018import { Box, Chip, Divider, Link, 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';
idillona3c2fad2022-12-18 23:49:10 -050022import Linkify from 'linkify-react';
23import * as linkify from 'linkifyjs';
idillon-sfl118ae442022-10-25 10:42:54 -040024import { ReactElement, ReactNode, useCallback, useMemo, useState } from 'react';
simond47ef9e2022-09-28 22:24:28 -040025import { useTranslation } from 'react-i18next';
simon07b4eb02022-09-29 17:50:26 -040026
Misha Krieger-Raynauldcfa44302022-11-30 18:36:36 -050027import { Account } from '../models/account';
28import { Contact } from '../models/contact';
idillona3c2fad2022-12-18 23:49:10 -050029import { useLinkPreviewQuery } from '../services/linkPreviewQueries';
idillon9e542ca2022-12-15 17:54:07 -050030import { getMessageCallText, getMessageMemberText } from '../utils/chatmessages';
31import { formatRelativeDate, formatTime } from '../utils/dates&times';
idillon-sfl9d956ab2022-10-20 16:33:24 -040032import { EmojiButton, MoreButton, ReplyMessageButton } from './Button';
simond47ef9e2022-09-28 22:24:28 -040033import ConversationAvatar from './ConversationAvatar';
idillona3c2fad2022-12-18 23:49:10 -050034import LoadingPage from './Loading';
idillonef9ab812022-11-18 13:46:24 -050035import PopoverList, { PopoverListItemData } from './PopoverList';
idillon-sflec735452022-10-27 13:18:41 -040036import {
37 ArrowLeftCurved,
38 ArrowLeftDown,
39 ArrowRightUp,
40 OppositeArrowsIcon,
41 TrashBinIcon,
42 TwoSheetsIcon,
43} from './SvgIcon';
Larbi Gharibe9af9732021-03-31 15:08:01 +010044
idillon-sfl9d956ab2022-10-20 16:33:24 -040045type MessagePosition = 'start' | 'end';
46
idillon-sfl118ae442022-10-25 10:42:54 -040047const notificationMessageTypes = ['initial', 'member'] as const;
48type NotificationMessageType = typeof notificationMessageTypes[number];
49const checkIsNotificationMessageType = (type: Message['type'] | undefined): type is NotificationMessageType => {
50 return notificationMessageTypes.includes(type as NotificationMessageType);
simond47ef9e2022-09-28 22:24:28 -040051};
Larbi Gharibe9af9732021-03-31 15:08:01 +010052
idillon-sfl118ae442022-10-25 10:42:54 -040053const invisibleMessageTypes = ['application/update-profile', 'merge', 'vote'] as const;
54type InvisibleMessageType = typeof invisibleMessageTypes[number];
55const checkIsInvisibleMessageType = (type: Message['type'] | undefined): type is InvisibleMessageType => {
56 return invisibleMessageTypes.includes(type as InvisibleMessageType);
simond47ef9e2022-09-28 22:24:28 -040057};
idillonbef18a52022-09-01 01:51:40 -040058
idillon-sfl118ae442022-10-25 10:42:54 -040059const userMessageTypes = ['text/plain', 'application/data-transfer+json', 'application/call-history+json'] as const;
60type UserMessageType = typeof userMessageTypes[number];
61const checkIsUserMessageType = (type: Message['type'] | undefined): type is UserMessageType => {
62 return userMessageTypes.includes(type as UserMessageType);
63};
64
65const checkShowsTime = (time: Dayjs, previousTime: Dayjs) => {
66 return !previousTime.isSame(time) && !time.isBetween(previousTime, previousTime?.add(1, 'minute'));
67};
68
69const findPreviousVisibleMessage = (messages: Message[], messageIndex: number) => {
70 for (let i = messageIndex + 1; i < messages.length; ++i) {
71 const message = messages[i];
72 if (!checkIsInvisibleMessageType(message?.type)) {
73 return message;
74 }
75 }
76};
77
78const findNextVisibleMessage = (messages: Message[], messageIndex: number) => {
79 for (let i = messageIndex - 1; i >= 0; --i) {
80 const message = messages[i];
81 if (!checkIsInvisibleMessageType(message?.type)) {
82 return message;
83 }
84 }
85};
86
87const avatarSize = '22px';
88const spacingBetweenAvatarAndBubble = '10px';
89const bubblePadding = '16px';
90
91interface MessageCallProps {
92 message: Message;
93 isAccountMessage: boolean;
idillon-sfl9d956ab2022-10-20 16:33:24 -040094 isFirstOfGroup: boolean;
95 isLastOfGroup: boolean;
96}
97
idillon-sflec735452022-10-27 13:18:41 -040098const MessageCall = ({ message, isAccountMessage, isFirstOfGroup, isLastOfGroup }: MessageCallProps) => {
idillon-sfl118ae442022-10-25 10:42:54 -040099 const position = isAccountMessage ? 'end' : 'start';
idillon-sflec735452022-10-27 13:18:41 -0400100
idillon9e542ca2022-12-15 17:54:07 -0500101 const { i18n } = useTranslation();
idillon-sflec735452022-10-27 13:18:41 -0400102 const { bubbleColor, Icon, text, textColor } = useMemo(() => {
idillon9e542ca2022-12-15 17:54:07 -0500103 const text = getMessageCallText(isAccountMessage, message, i18n);
idillon-sflec735452022-10-27 13:18:41 -0400104 const callDuration = dayjs.duration(parseInt(message?.duration || ''));
105 if (callDuration.asSeconds() === 0) {
106 if (isAccountMessage) {
107 return {
idillon9e542ca2022-12-15 17:54:07 -0500108 text,
idillon-sflec735452022-10-27 13:18:41 -0400109 Icon: ArrowLeftCurved,
110 textColor: 'white',
111 bubbleColor: '#005699' + '80', // opacity 50%
112 };
113 } else {
114 return {
idillon9e542ca2022-12-15 17:54:07 -0500115 text,
idillon-sflec735452022-10-27 13:18:41 -0400116 Icon: ArrowLeftCurved,
117 textColor: 'black',
118 bubbleColor: '#C6C6C6',
119 };
120 }
121 } else {
idillon-sflec735452022-10-27 13:18:41 -0400122 if (isAccountMessage) {
123 return {
idillon9e542ca2022-12-15 17:54:07 -0500124 text,
idillon-sflec735452022-10-27 13:18:41 -0400125 Icon: ArrowRightUp,
126 textColor: 'white',
127 bubbleColor: '#005699',
128 };
129 } else {
130 return {
idillon9e542ca2022-12-15 17:54:07 -0500131 text,
idillon-sflec735452022-10-27 13:18:41 -0400132 Icon: ArrowLeftDown,
133 textcolor: 'black',
134 bubbleColor: '#E5E5E5',
135 };
136 }
137 }
idillon9e542ca2022-12-15 17:54:07 -0500138 }, [isAccountMessage, message, i18n]);
idillon-sflec735452022-10-27 13:18:41 -0400139
idillonbef18a52022-09-01 01:51:40 -0400140 return (
idillon-sfl118ae442022-10-25 10:42:54 -0400141 <Bubble position={position} isFirstOfGroup={isFirstOfGroup} isLastOfGroup={isLastOfGroup} bubbleColor={bubbleColor}>
idillon-sflec735452022-10-27 13:18:41 -0400142 <Stack direction="row" spacing="10px" alignItems="center">
143 <Icon sx={{ fontSize: '16px', color: textColor }} />
144 <Typography variant="body1" color={textColor} textAlign={position} fontWeight="bold" textTransform="uppercase">
145 {text}
146 </Typography>
147 </Stack>
idillon-sfl118ae442022-10-25 10:42:54 -0400148 </Bubble>
149 );
150};
151
152const MessageInitial = () => {
153 const { t } = useTranslation();
154 return <>{t('message_swarm_created')}</>;
155};
156
157interface MessageDataTransferProps {
158 message: Message;
159 isAccountMessage: boolean;
160 isFirstOfGroup: boolean;
161 isLastOfGroup: boolean;
162}
163
164const MessageDataTransfer = ({ isAccountMessage, isFirstOfGroup, isLastOfGroup }: MessageDataTransferProps) => {
165 const position = isAccountMessage ? 'end' : 'start';
166 return (
167 <Bubble bubbleColor="#E5E5E5" position={position} isFirstOfGroup={isFirstOfGroup} isLastOfGroup={isLastOfGroup}>
simon80b7b3b2022-09-28 17:50:10 -0400168 &quot;data-transfer&quot;
idillon-sfl118ae442022-10-25 10:42:54 -0400169 </Bubble>
simond47ef9e2022-09-28 22:24:28 -0400170 );
171};
idillonbef18a52022-09-01 01:51:40 -0400172
idillon-sfl9d956ab2022-10-20 16:33:24 -0400173interface MessageMemberProps {
174 message: Message;
175}
176
idillon-sfl118ae442022-10-25 10:42:54 -0400177const MessageMember = ({ message }: MessageMemberProps) => {
idillon9e542ca2022-12-15 17:54:07 -0500178 const { i18n } = useTranslation();
179
180 const text = getMessageMemberText(message, i18n);
idillonbef18a52022-09-01 01:51:40 -0400181 return (
idillon-sfl118ae442022-10-25 10:42:54 -0400182 <Chip
183 sx={{
184 width: 'fit-content',
185 }}
idillon9e542ca2022-12-15 17:54:07 -0500186 label={text}
idillon-sfl118ae442022-10-25 10:42:54 -0400187 />
simond47ef9e2022-09-28 22:24:28 -0400188 );
189};
idillonbef18a52022-09-01 01:51:40 -0400190
idillona3c2fad2022-12-18 23:49:10 -0500191type LinkPreviewProps = {
192 isAccountMessage: boolean;
193 link: string;
194};
195
196const LinkPreview = ({ isAccountMessage, link }: LinkPreviewProps) => {
197 const [imageIsWorking, setImageIsWorking] = useState(true);
198 const linkPreviewQuery = useLinkPreviewQuery(link);
199 if (linkPreviewQuery.isLoading) {
200 return <LoadingPage />;
201 }
202 if (!linkPreviewQuery.isSuccess) {
203 return null;
204 }
205 const linkPreview = linkPreviewQuery.data;
206
207 return (
208 <a href={link} style={{ textDecorationLine: 'none' }}>
209 <Stack>
210 {imageIsWorking && linkPreview.image && (
211 <img
212 style={{
213 padding: '15px 0',
214 }}
215 alt={linkPreview.title}
216 src={linkPreview.image}
217 onError={() => setImageIsWorking(false)}
218 />
219 )}
220 <Typography variant="body1" color={isAccountMessage ? '#cccccc' : 'black'}>
221 {linkPreview.title}
222 </Typography>
223 {linkPreview.description && (
224 <Typography variant="body1" sx={{ color: isAccountMessage ? '#ffffff' : '#005699' }}>
225 {linkPreview.description}
226 </Typography>
227 )}
228 <Typography variant="body1" color={isAccountMessage ? '#cccccc' : 'black'}>
229 {new URL(link).hostname}
230 </Typography>
231 </Stack>
232 </a>
233 );
234};
235
236type RenderLinkProps = {
237 attributes: {
238 isAccountMessage: boolean;
239 href: string;
240 };
241 content: ReactNode;
242};
243
244const RenderLink = ({ attributes, content }: RenderLinkProps) => {
245 const { href, isAccountMessage, ...props } = attributes;
246 return (
247 <Link href={href} {...props} variant="body1" color={isAccountMessage ? '#ffffff' : undefined}>
248 {content}
249 </Link>
250 );
251};
252
idillon-sfl9d956ab2022-10-20 16:33:24 -0400253interface MessageTextProps {
254 message: Message;
idillon-sfl118ae442022-10-25 10:42:54 -0400255 isAccountMessage: boolean;
idillon-sfl9d956ab2022-10-20 16:33:24 -0400256 isFirstOfGroup: boolean;
257 isLastOfGroup: boolean;
idillon-sfl9d956ab2022-10-20 16:33:24 -0400258}
259
idillon-sfl118ae442022-10-25 10:42:54 -0400260const MessageText = ({ message, isAccountMessage, isFirstOfGroup, isLastOfGroup }: MessageTextProps) => {
261 const position = isAccountMessage ? 'end' : 'start';
262 const bubbleColor = isAccountMessage ? '#005699' : '#E5E5E5';
263 const textColor = isAccountMessage ? 'white' : 'black';
idillona3c2fad2022-12-18 23:49:10 -0500264
265 const link = useMemo(() => linkify.find(message?.body ?? '', 'url')[0]?.href, [message]);
266
idillonbef18a52022-09-01 01:51:40 -0400267 return (
idillon-sfl118ae442022-10-25 10:42:54 -0400268 <MessageTooltip position={position}>
269 <Bubble
270 bubbleColor={bubbleColor}
271 position={position}
272 isFirstOfGroup={isFirstOfGroup}
273 isLastOfGroup={isLastOfGroup}
idillona3c2fad2022-12-18 23:49:10 -0500274 maxWidth={link ? '400px' : undefined}
idillon-sfl118ae442022-10-25 10:42:54 -0400275 >
276 <Typography variant="body1" color={textColor} textAlign={position}>
idillona3c2fad2022-12-18 23:49:10 -0500277 <Linkify options={{ render: RenderLink as any, attributes: { isAccountMessage } }}>{message.body}</Linkify>
idillon-sfl118ae442022-10-25 10:42:54 -0400278 </Typography>
idillona3c2fad2022-12-18 23:49:10 -0500279 {link && <LinkPreview isAccountMessage={isAccountMessage} link={link} />}
idillon-sfl118ae442022-10-25 10:42:54 -0400280 </Bubble>
281 </MessageTooltip>
simond47ef9e2022-09-28 22:24:28 -0400282 );
283};
idillonbef18a52022-09-01 01:51:40 -0400284
idillon-sfl118ae442022-10-25 10:42:54 -0400285interface DateIndicatorProps {
idillon-sfl9d956ab2022-10-20 16:33:24 -0400286 time: Dayjs;
287}
288
idillon-sfl118ae442022-10-25 10:42:54 -0400289const DateIndicator = ({ time }: DateIndicatorProps) => {
idillon-sfl0e1a0d92022-10-25 16:52:44 -0400290 const { i18n } = useTranslation();
idillon9e542ca2022-12-15 17:54:07 -0500291 const textDate = useMemo(() => formatRelativeDate(time, i18n), [time, i18n]);
idillonbef18a52022-09-01 01:51:40 -0400292
293 return (
simond47ef9e2022-09-28 22:24:28 -0400294 <Box marginTop="30px">
idillon04245a12022-09-01 11:12:17 -0400295 <Divider
296 sx={{
simond47ef9e2022-09-28 22:24:28 -0400297 '.MuiDivider-wrapper': {
idillon04245a12022-09-01 11:12:17 -0400298 margin: 0,
299 padding: 0,
300 },
simond47ef9e2022-09-28 22:24:28 -0400301 '&::before': {
302 borderTop: '1px solid #E5E5E5',
idillon04245a12022-09-01 11:12:17 -0400303 },
simond47ef9e2022-09-28 22:24:28 -0400304 '&::after': {
305 borderTop: '1px solid #E5E5E5',
idillon04245a12022-09-01 11:12:17 -0400306 },
307 }}
308 >
309 <Typography
310 variant="caption"
311 fontWeight={700}
312 border="1px solid #E5E5E5"
313 borderRadius="5px"
314 padding="10px 16px"
idillon-sfl0e1a0d92022-10-25 16:52:44 -0400315 textTransform="capitalize"
idillon04245a12022-09-01 11:12:17 -0400316 >
317 {textDate}
318 </Typography>
idillonbef18a52022-09-01 01:51:40 -0400319 </Divider>
320 </Box>
simond47ef9e2022-09-28 22:24:28 -0400321 );
322};
idillonbef18a52022-09-01 01:51:40 -0400323
idillon-sfl118ae442022-10-25 10:42:54 -0400324interface TimeIndicatorProps {
idillon-sfl9d956ab2022-10-20 16:33:24 -0400325 time: Dayjs;
326 hasDateOnTop: boolean;
327}
328
idillon-sfl118ae442022-10-25 10:42:54 -0400329const TimeIndicator = ({ time, hasDateOnTop }: TimeIndicatorProps) => {
idillon-sfl0e1a0d92022-10-25 16:52:44 -0400330 const { i18n } = useTranslation();
idillon9e542ca2022-12-15 17:54:07 -0500331 const textTime = useMemo(() => formatTime(time, i18n), [time, i18n]);
idillonbef18a52022-09-01 01:51:40 -0400332
333 return (
idillon-sfl118ae442022-10-25 10:42:54 -0400334 <Stack direction="row" justifyContent="center" marginTop={hasDateOnTop ? '20px' : '30px'}>
simond47ef9e2022-09-28 22:24:28 -0400335 <Typography variant="caption" color="#A7A7A7" fontWeight={700}>
idillonbef18a52022-09-01 01:51:40 -0400336 {textTime}
337 </Typography>
338 </Stack>
simond47ef9e2022-09-28 22:24:28 -0400339 );
340};
idillonbef18a52022-09-01 01:51:40 -0400341
idillon-sfl118ae442022-10-25 10:42:54 -0400342interface NotificationMessageRowProps {
343 message: Message;
idillon-sfl9d956ab2022-10-20 16:33:24 -0400344}
345
idillon-sfl118ae442022-10-25 10:42:54 -0400346const NotificationMessageRow = ({ message }: NotificationMessageRowProps) => {
347 let messageComponent;
348 switch (message.type) {
349 case 'initial':
350 messageComponent = <MessageInitial />;
351 break;
352 case 'member':
353 messageComponent = <MessageMember message={message} />;
354 break;
355 default:
356 console.error(`${NotificationMessageRow.name} received unhandled message type: ${message.type}`);
357 return null;
idillonae655dd2022-10-14 18:11:02 -0400358 }
359
idillonbef18a52022-09-01 01:51:40 -0400360 return (
idillon-sfl118ae442022-10-25 10:42:54 -0400361 <Stack paddingTop={'30px'} alignItems="center">
362 {messageComponent}
363 </Stack>
364 );
365};
366
367interface UserMessageRowProps {
368 message: Message;
369 isAccountMessage: boolean;
370 previousMessage: Message | undefined;
371 nextMessage: Message | undefined;
372 time: Dayjs;
373 showsTime: boolean;
374 author: Account | Contact;
375}
376
377const UserMessageRow = ({
378 message,
379 previousMessage,
380 nextMessage,
381 isAccountMessage,
382 time,
383 showsTime,
384 author,
385}: UserMessageRowProps) => {
386 const authorName = author.getDisplayName();
387 const position = isAccountMessage ? 'end' : 'start';
388
389 const previousIsUserMessageType = checkIsUserMessageType(previousMessage?.type);
390 const nextIsUserMessageType = checkIsUserMessageType(nextMessage?.type);
391 const nextTime = dayjs.unix(Number(nextMessage?.timestamp));
392 const nextShowsTime = checkShowsTime(nextTime, time);
393 const isFirstOfGroup = showsTime || !previousIsUserMessageType || previousMessage?.author !== message.author;
394 const isLastOfGroup = nextShowsTime || !nextIsUserMessageType || message.author !== nextMessage?.author;
395
396 const props = {
397 message,
398 isAccountMessage,
399 isFirstOfGroup,
400 isLastOfGroup,
401 };
402
403 let MessageComponent;
404 switch (message.type) {
405 case 'text/plain':
406 MessageComponent = MessageText;
407 break;
408 case 'application/data-transfer+json':
409 MessageComponent = MessageDataTransfer;
410 break;
411 case 'application/call-history+json':
412 MessageComponent = MessageCall;
413 break;
414 default:
415 console.error(`${UserMessageRow.name} received unhandled message type: ${message.type}`);
416 return null;
417 }
418
419 const participantNamePadding = isAccountMessage
420 ? bubblePadding
421 : parseInt(avatarSize) + parseInt(spacingBetweenAvatarAndBubble) + parseInt(bubblePadding) + 'px';
422
423 return (
424 <Stack alignItems={position}>
425 {isFirstOfGroup && (
426 <Box padding={`30px ${participantNamePadding} 0 ${participantNamePadding}`}>
427 <ParticipantName name={authorName} />
428 </Box>
simond47ef9e2022-09-28 22:24:28 -0400429 )}
idillon-sfl118ae442022-10-25 10:42:54 -0400430 <Stack
431 direction="row"
432 justifyContent={position}
433 alignItems="end"
434 spacing={spacingBetweenAvatarAndBubble}
435 paddingTop="6px"
idillonbef18a52022-09-01 01:51:40 -0400436 width="66.66%"
idillonbef18a52022-09-01 01:51:40 -0400437 >
idillon-sfl118ae442022-10-25 10:42:54 -0400438 <Box sx={{ width: avatarSize }}>
439 {!isAccountMessage && isLastOfGroup && (
440 <ConversationAvatar
441 displayName={authorName}
442 sx={{ width: avatarSize, height: avatarSize, fontSize: '15px' }}
443 />
444 )}
445 </Box>
446 <MessageComponent {...props} />
idillonbef18a52022-09-01 01:51:40 -0400447 </Stack>
448 </Stack>
simond47ef9e2022-09-28 22:24:28 -0400449 );
450};
idillonbef18a52022-09-01 01:51:40 -0400451
idillon-sfl9d956ab2022-10-20 16:33:24 -0400452interface MessageTooltipProps {
453 className?: string;
454 position: MessagePosition;
455 children: ReactElement;
456}
457
458const MessageTooltip = styled(({ className, position, children }: MessageTooltipProps) => {
simond47ef9e2022-09-28 22:24:28 -0400459 const [open, setOpen] = useState(false);
460 const emojis = ['😎', '😄', '😍']; // Should be last three used emojis
idillonef9ab812022-11-18 13:46:24 -0500461 const additionalOptions: PopoverListItemData[] = [
idillon927b7592022-09-15 12:56:45 -0400462 {
463 Icon: TwoSheetsIcon,
idillonef9ab812022-11-18 13:46:24 -0500464 label: 'Copy',
465 onClick: () => {},
idillon927b7592022-09-15 12:56:45 -0400466 },
467 {
468 Icon: OppositeArrowsIcon,
idillonef9ab812022-11-18 13:46:24 -0500469 label: 'Transfer',
470 onClick: () => {},
idillon927b7592022-09-15 12:56:45 -0400471 },
472 {
473 Icon: TrashBinIcon,
idillonef9ab812022-11-18 13:46:24 -0500474 label: 'Delete message',
475 onClick: () => {},
idillon927b7592022-09-15 12:56:45 -0400476 },
simond47ef9e2022-09-28 22:24:28 -0400477 ];
idillon927b7592022-09-15 12:56:45 -0400478
simond47ef9e2022-09-28 22:24:28 -0400479 const toggleMoreMenu = useCallback(() => setOpen((open) => !open), [setOpen]);
idillon927b7592022-09-15 12:56:45 -0400480
simond47ef9e2022-09-28 22:24:28 -0400481 const onClose = useCallback(() => {
482 setOpen(false);
483 }, [setOpen]);
idillon927b7592022-09-15 12:56:45 -0400484
485 return (
486 <Tooltip
idillon927b7592022-09-15 12:56:45 -0400487 classes={{ tooltip: className }} // Required for styles. Don't know why
idillon-sfl9d956ab2022-10-20 16:33:24 -0400488 placement={position === 'start' ? 'right-start' : 'left-start'}
idillon927b7592022-09-15 12:56:45 -0400489 PopperProps={{
490 modifiers: [
491 {
simond47ef9e2022-09-28 22:24:28 -0400492 name: 'offset',
idillon927b7592022-09-15 12:56:45 -0400493 options: {
simond47ef9e2022-09-28 22:24:28 -0400494 offset: [-2, -30],
idillon927b7592022-09-15 12:56:45 -0400495 },
496 },
497 ],
498 }}
499 onClose={onClose}
500 title={
simond47ef9e2022-09-28 22:24:28 -0400501 <Stack>
idillon927b7592022-09-15 12:56:45 -0400502 <Stack // Main options
503 direction="row"
504 spacing="16px"
idillonef9ab812022-11-18 13:46:24 -0500505 padding="16px"
idillon927b7592022-09-15 12:56:45 -0400506 >
simond47ef9e2022-09-28 22:24:28 -0400507 {emojis.map((emoji) => (
508 <EmojiButton key={emoji} emoji={emoji} />
509 ))}
510 <ReplyMessageButton />
511 <MoreButton onClick={toggleMoreMenu} />
idillon927b7592022-09-15 12:56:45 -0400512 </Stack>
idillonef9ab812022-11-18 13:46:24 -0500513 {open && (
idillon927b7592022-09-15 12:56:45 -0400514 <>
idillonef9ab812022-11-18 13:46:24 -0500515 <Divider sx={{ marginX: '16px' }} />
516 <PopoverList items={additionalOptions} />
idillon927b7592022-09-15 12:56:45 -0400517 </>
simond47ef9e2022-09-28 22:24:28 -0400518 )}
idillon927b7592022-09-15 12:56:45 -0400519 </Stack>
520 }
idillon-sfl9d956ab2022-10-20 16:33:24 -0400521 >
idillon-sfl118ae442022-10-25 10:42:54 -0400522 {/* div fixes 'Function components cannot be given refs' error */}
523 <div>{children}</div>
idillon-sfl9d956ab2022-10-20 16:33:24 -0400524 </Tooltip>
simond47ef9e2022-09-28 22:24:28 -0400525 );
idillon-sfl9d956ab2022-10-20 16:33:24 -0400526})(({ position }) => {
simond47ef9e2022-09-28 22:24:28 -0400527 const largeRadius = '20px';
528 const smallRadius = '5px';
idillon927b7592022-09-15 12:56:45 -0400529 return {
simond47ef9e2022-09-28 22:24:28 -0400530 backgroundColor: 'white',
idillonef9ab812022-11-18 13:46:24 -0500531 padding: '0px',
simond47ef9e2022-09-28 22:24:28 -0400532 boxShadow: '3px 3px 7px #00000029',
idillon927b7592022-09-15 12:56:45 -0400533 borderRadius: largeRadius,
idillon-sfl9d956ab2022-10-20 16:33:24 -0400534 borderStartStartRadius: position === 'start' ? smallRadius : largeRadius,
535 borderStartEndRadius: position === 'end' ? smallRadius : largeRadius,
idillonef9ab812022-11-18 13:46:24 -0500536 overflow: 'hidden',
simond47ef9e2022-09-28 22:24:28 -0400537 };
idillon927b7592022-09-15 12:56:45 -0400538});
539
idillon-sfl118ae442022-10-25 10:42:54 -0400540interface BubbleProps {
idillon-sfl9d956ab2022-10-20 16:33:24 -0400541 position: MessagePosition;
542 isFirstOfGroup: boolean;
543 isLastOfGroup: boolean;
idillon-sfl118ae442022-10-25 10:42:54 -0400544 bubbleColor: string;
idillona3c2fad2022-12-18 23:49:10 -0500545 maxWidth?: string;
idillon-sfl9d956ab2022-10-20 16:33:24 -0400546 children: ReactNode;
547}
548
idillona3c2fad2022-12-18 23:49:10 -0500549const Bubble = ({ position, isFirstOfGroup, isLastOfGroup, bubbleColor, maxWidth, children }: BubbleProps) => {
simond47ef9e2022-09-28 22:24:28 -0400550 const largeRadius = '20px';
551 const smallRadius = '5px';
Adrien Béraud023f7cf2022-09-18 14:57:53 -0400552 const radius = useMemo(() => {
idillon-sfl9d956ab2022-10-20 16:33:24 -0400553 if (position === 'start') {
idillonbef18a52022-09-01 01:51:40 -0400554 return {
idillon-sfl9d956ab2022-10-20 16:33:24 -0400555 borderStartStartRadius: isFirstOfGroup ? largeRadius : smallRadius,
idillonbef18a52022-09-01 01:51:40 -0400556 borderStartEndRadius: largeRadius,
idillon-sfl9d956ab2022-10-20 16:33:24 -0400557 borderEndStartRadius: isLastOfGroup ? largeRadius : smallRadius,
idillonbef18a52022-09-01 01:51:40 -0400558 borderEndEndRadius: largeRadius,
simond47ef9e2022-09-28 22:24:28 -0400559 };
idillonbef18a52022-09-01 01:51:40 -0400560 }
561 return {
562 borderStartStartRadius: largeRadius,
idillon-sfl9d956ab2022-10-20 16:33:24 -0400563 borderStartEndRadius: isFirstOfGroup ? largeRadius : smallRadius,
idillonbef18a52022-09-01 01:51:40 -0400564 borderEndStartRadius: largeRadius,
idillon-sfl9d956ab2022-10-20 16:33:24 -0400565 borderEndEndRadius: isLastOfGroup ? largeRadius : smallRadius,
simond47ef9e2022-09-28 22:24:28 -0400566 };
idillon-sfl9d956ab2022-10-20 16:33:24 -0400567 }, [isFirstOfGroup, isLastOfGroup, position]);
idillonbef18a52022-09-01 01:51:40 -0400568
569 return (
idillon-sfl118ae442022-10-25 10:42:54 -0400570 <Box
571 sx={{
572 width: 'fit-content',
idillona3c2fad2022-12-18 23:49:10 -0500573 maxWidth,
idillon-sfl118ae442022-10-25 10:42:54 -0400574 backgroundColor: bubbleColor,
575 padding: bubblePadding,
idillona3c2fad2022-12-18 23:49:10 -0500576 overflow: 'hidden',
577 wordWrap: 'break-word',
idillon-sfl118ae442022-10-25 10:42:54 -0400578 ...radius,
579 }}
580 >
581 {children}
582 </Box>
simond47ef9e2022-09-28 22:24:28 -0400583 );
584};
idillonbef18a52022-09-01 01:51:40 -0400585
idillon-sfl9d956ab2022-10-20 16:33:24 -0400586interface ParticipantNameProps {
587 name: string;
588}
589
590const ParticipantName = ({ name }: ParticipantNameProps) => {
idillonbef18a52022-09-01 01:51:40 -0400591 return (
idillon-sfl118ae442022-10-25 10:42:54 -0400592 <Typography variant="caption" color="#A7A7A7" fontWeight={700}>
593 {name}
594 </Typography>
595 );
596};
597
598interface MessageProps {
599 messageIndex: number;
600 messages: Message[];
601 isAccountMessage: boolean;
602 author: Account | Contact;
603}
604
605export const MessageRow = ({ messageIndex, messages, isAccountMessage, author }: MessageProps) => {
606 const message = messages[messageIndex];
607 const previousMessage = findPreviousVisibleMessage(messages, messageIndex);
608 const nextMessage = findNextVisibleMessage(messages, messageIndex);
609 const time = dayjs.unix(Number(message.timestamp));
610 const previousTime = dayjs.unix(Number(previousMessage?.timestamp));
611 const showDate =
612 message?.type === 'initial' || previousTime.year() !== time.year() || previousTime.dayOfYear() !== time.dayOfYear();
613 const showTime = checkShowsTime(time, previousTime);
614 let messageComponent;
615 if (checkIsUserMessageType(message.type)) {
616 messageComponent = (
617 <UserMessageRow
618 message={message}
619 previousMessage={previousMessage}
620 nextMessage={nextMessage}
621 time={time}
622 showsTime={showTime}
623 isAccountMessage={isAccountMessage}
624 author={author}
625 />
626 );
627 } else if (checkIsNotificationMessageType(message.type)) {
628 messageComponent = <NotificationMessageRow message={message} />;
629 } else if (checkIsInvisibleMessageType(message.type)) {
630 return null;
631 } else {
632 const _exhaustiveCheck: never = message.type;
633 return _exhaustiveCheck;
634 }
635
636 return (
637 <Stack>
638 {showDate && <DateIndicator time={time} />}
639 {showTime && <TimeIndicator time={time} hasDateOnTop={showDate} />}
640 {messageComponent}
641 </Stack>
simond47ef9e2022-09-28 22:24:28 -0400642 );
643};