convert Message and MessageList to Typescript
Change-Id: Idbca1335dccbf910c2e1715769ff46c575472d00
diff --git a/client/src/components/Message.tsx b/client/src/components/Message.tsx
new file mode 100644
index 0000000..bfc442f
--- /dev/null
+++ b/client/src/components/Message.tsx
@@ -0,0 +1,440 @@
+/*
+ * Copyright (C) 2022 Savoir-faire Linux Inc.
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation; either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public
+ * License along with this program. If not, see
+ * <https://www.gnu.org/licenses/>.
+ */
+import {
+ Box,
+ Chip,
+ Divider,
+ List,
+ ListItemButton,
+ ListItemText,
+ Stack,
+ Theme,
+ Tooltip,
+ Typography,
+} from '@mui/material';
+import { styled } from '@mui/material/styles';
+import dayjs, { Dayjs } from 'dayjs';
+import isToday from 'dayjs/plugin/isToday';
+import isYesterday from 'dayjs/plugin/isYesterday';
+import { Account, ConversationMember, Message } from 'jami-web-common';
+import { ReactElement } from 'react';
+import { ReactNode, useCallback, useMemo, useState } from 'react';
+import { useTranslation } from 'react-i18next';
+
+import { EmojiButton, MoreButton, ReplyMessageButton } from './Button';
+import ConversationAvatar from './ConversationAvatar';
+import { OppositeArrowsIcon, TrashBinIcon, TwoSheetsIcon } from './SvgIcon';
+
+dayjs.extend(isToday);
+dayjs.extend(isYesterday);
+
+type MessagePosition = 'start' | 'end';
+
+export const MessageCall = () => {
+ return <Stack alignItems="center">"Appel"</Stack>;
+};
+
+export const MessageInitial = () => {
+ const { t } = useTranslation();
+ return <Stack alignItems="center">{t('message_swarm_created')}</Stack>;
+};
+
+interface MessageDataTransferProps {
+ position: MessagePosition;
+ isFirstOfGroup: boolean;
+ isLastOfGroup: boolean;
+}
+
+export const MessageDataTransfer = ({ position, isFirstOfGroup, isLastOfGroup }: MessageDataTransferProps) => {
+ return (
+ <MessageBubble
+ backgroundColor={'#E5E5E5'}
+ position={position}
+ isFirstOfGroup={isFirstOfGroup}
+ isLastOfGroup={isLastOfGroup}
+ >
+ "data-transfer"
+ </MessageBubble>
+ );
+};
+
+interface MessageMemberProps {
+ message: Message;
+}
+
+export const MessageMember = ({ message }: MessageMemberProps) => {
+ const { t } = useTranslation();
+ return (
+ <Stack alignItems="center">
+ <Chip
+ sx={{
+ width: 'fit-content',
+ }}
+ label={t('message_user_joined', { user: message.author })}
+ />
+ </Stack>
+ );
+};
+
+export const MessageMerge = () => {
+ return <Stack alignItems="center">"merge"</Stack>;
+};
+
+interface MessageTextProps {
+ message: Message;
+ position: MessagePosition;
+ isFirstOfGroup: boolean;
+ isLastOfGroup: boolean;
+ textColor: string;
+ bubbleColor: string;
+}
+
+export const MessageText = ({
+ message,
+ position,
+ isFirstOfGroup,
+ isLastOfGroup,
+ textColor,
+ bubbleColor,
+}: MessageTextProps) => {
+ return (
+ <MessageBubble
+ backgroundColor={bubbleColor}
+ position={position}
+ isFirstOfGroup={isFirstOfGroup}
+ isLastOfGroup={isLastOfGroup}
+ >
+ <Typography variant="body1" color={textColor} textAlign={position}>
+ {message.body}
+ </Typography>
+ </MessageBubble>
+ );
+};
+
+interface MessageDateProps {
+ time: Dayjs;
+}
+
+export const MessageDate = ({ time }: MessageDateProps) => {
+ let textDate;
+
+ if (time.isToday()) {
+ textDate = 'Today';
+ } else if (time.isYesterday()) {
+ textDate = 'Yesterday';
+ } else {
+ const date = time.date().toString().padStart(2, '0');
+ const month = (time.month() + 1).toString().padStart(2, '0');
+ textDate = `${date}/${month}/${time.year()}`;
+ }
+
+ return (
+ <Box marginTop="30px">
+ <Divider
+ sx={{
+ '.MuiDivider-wrapper': {
+ margin: 0,
+ padding: 0,
+ },
+ '&::before': {
+ borderTop: '1px solid #E5E5E5',
+ },
+ '&::after': {
+ borderTop: '1px solid #E5E5E5',
+ },
+ }}
+ >
+ <Typography
+ variant="caption"
+ fontWeight={700}
+ border="1px solid #E5E5E5"
+ borderRadius="5px"
+ padding="10px 16px"
+ >
+ {textDate}
+ </Typography>
+ </Divider>
+ </Box>
+ );
+};
+
+interface MessageTimeProps {
+ time: Dayjs;
+ hasDateOnTop: boolean;
+}
+
+export const MessageTime = ({ time, hasDateOnTop }: MessageTimeProps) => {
+ const hour = time.hour().toString().padStart(2, '0');
+ const minute = time.minute().toString().padStart(2, '0');
+ const textTime = `${hour}:${minute}`;
+
+ return (
+ <Stack direction="row" justifyContent="center" margin="30px" marginTop={hasDateOnTop ? '20px' : '30px'}>
+ <Typography variant="caption" color="#A7A7A7" fontWeight={700}>
+ {textTime}
+ </Typography>
+ </Stack>
+ );
+};
+
+interface MessageBubblesGroupProps {
+ account: Account;
+ messages: Message[];
+ members: ConversationMember[];
+}
+
+export const MessageBubblesGroup = ({ account, messages, members }: MessageBubblesGroupProps) => {
+ const isUser = messages[0]?.author === account.getUri();
+ const position = isUser ? 'end' : 'start';
+ const bubbleColor = isUser ? '#005699' : '#E5E5E5';
+ const textColor = isUser ? 'white' : 'black';
+
+ let authorName;
+ if (isUser) {
+ authorName = account.getDisplayName();
+ } else {
+ const member = members.find((member) => messages[0]?.author === member.contact.getUri());
+ authorName = member?.contact?.getDisplayName() || '';
+ }
+
+ return (
+ <Stack // Row for a group of message bubbles with the user's infos
+ direction="row"
+ justifyContent={position}
+ alignItems="end"
+ spacing="10px"
+ >
+ {!isUser && (
+ <ConversationAvatar displayName={authorName} sx={{ width: '22px', height: '22px', fontSize: '15px' }} />
+ )}
+ <Stack // Container to align the bubbles to the same side of a row
+ width="66.66%"
+ alignItems={position}
+ >
+ <ParticipantName name={authorName} />
+ <Stack // Container for a group of message bubbles
+ spacing="6px"
+ alignItems={position}
+ direction="column-reverse"
+ >
+ {messages.map((message, index) => {
+ let Component: typeof MessageText | typeof MessageDataTransfer;
+ switch (message.type) {
+ case 'text/plain':
+ Component = MessageText;
+ break;
+ case 'application/data-transfer+json':
+ Component = MessageDataTransfer;
+ break;
+ default:
+ return null;
+ }
+ return (
+ <Component // Single message
+ key={message.id}
+ message={message}
+ textColor={textColor}
+ position={position}
+ bubbleColor={bubbleColor}
+ isFirstOfGroup={index === messages.length - 1}
+ isLastOfGroup={index === 0}
+ />
+ );
+ })}
+ </Stack>
+ </Stack>
+ </Stack>
+ );
+};
+
+interface MessageTooltipProps {
+ className?: string;
+ position: MessagePosition;
+ children: ReactElement;
+}
+
+const MessageTooltip = styled(({ className, position, children }: MessageTooltipProps) => {
+ const [open, setOpen] = useState(false);
+ const emojis = ['😎', '😄', '😍']; // Should be last three used emojis
+ const additionalOptions = [
+ {
+ Icon: TwoSheetsIcon,
+ text: 'Copy',
+ action: () => {},
+ },
+ {
+ Icon: OppositeArrowsIcon,
+ text: 'Transfer',
+ action: () => {},
+ },
+ {
+ Icon: TrashBinIcon,
+ text: 'Delete message',
+ action: () => {},
+ },
+ ];
+
+ const toggleMoreMenu = useCallback(() => setOpen((open) => !open), [setOpen]);
+
+ const onClose = useCallback(() => {
+ setOpen(false);
+ }, [setOpen]);
+
+ return (
+ <Tooltip
+ classes={{ tooltip: className }} // Required for styles. Don't know why
+ placement={position === 'start' ? 'right-start' : 'left-start'}
+ PopperProps={{
+ modifiers: [
+ {
+ name: 'offset',
+ options: {
+ offset: [-2, -30],
+ },
+ },
+ ],
+ }}
+ onClose={onClose}
+ title={
+ <Stack>
+ {/* Whole tooltip's content */}
+ <Stack // Main options
+ direction="row"
+ spacing="16px"
+ >
+ {emojis.map((emoji) => (
+ <EmojiButton key={emoji} emoji={emoji} />
+ ))}
+ <ReplyMessageButton />
+ <MoreButton onClick={toggleMoreMenu} />
+ </Stack>
+ {open && ( // Additional menu options
+ <>
+ <Divider sx={{ paddingTop: '16px' }} />
+ <List sx={{ padding: 0, paddingTop: '8px', marginBottom: '-8px' }}>
+ {additionalOptions.map((option) => (
+ <ListItemButton
+ key={option.text}
+ sx={{
+ padding: '8px',
+ }}
+ >
+ <Stack // Could not find proper way to set spacing between ListItemIcon and ListItemText
+ direction="row"
+ spacing="16px"
+ >
+ <option.Icon
+ sx={{
+ height: '16px',
+ margin: 0,
+ color: (theme: Theme) => theme?.palette?.primary?.dark,
+ }}
+ />
+ <ListItemText
+ primary={option.text}
+ primaryTypographyProps={{
+ fontSize: '12px',
+ lineHeight: '16px',
+ }}
+ sx={{
+ height: '16px',
+ margin: 0,
+ }}
+ />
+ </Stack>
+ </ListItemButton>
+ ))}
+ </List>
+ </>
+ )}
+ </Stack>
+ }
+ >
+ {children}
+ </Tooltip>
+ );
+})(({ position }) => {
+ const largeRadius = '20px';
+ const smallRadius = '5px';
+ return {
+ backgroundColor: 'white',
+ padding: '16px',
+ boxShadow: '3px 3px 7px #00000029',
+ borderRadius: largeRadius,
+ borderStartStartRadius: position === 'start' ? smallRadius : largeRadius,
+ borderStartEndRadius: position === 'end' ? smallRadius : largeRadius,
+ };
+});
+
+interface MessageBubbleProps {
+ position: MessagePosition;
+ isFirstOfGroup: boolean;
+ isLastOfGroup: boolean;
+ backgroundColor: string;
+ children: ReactNode;
+}
+
+const MessageBubble = ({ position, isFirstOfGroup, isLastOfGroup, backgroundColor, children }: MessageBubbleProps) => {
+ const largeRadius = '20px';
+ const smallRadius = '5px';
+ const radius = useMemo(() => {
+ if (position === 'start') {
+ return {
+ borderStartStartRadius: isFirstOfGroup ? largeRadius : smallRadius,
+ borderStartEndRadius: largeRadius,
+ borderEndStartRadius: isLastOfGroup ? largeRadius : smallRadius,
+ borderEndEndRadius: largeRadius,
+ };
+ }
+ return {
+ borderStartStartRadius: largeRadius,
+ borderStartEndRadius: isFirstOfGroup ? largeRadius : smallRadius,
+ borderEndStartRadius: largeRadius,
+ borderEndEndRadius: isLastOfGroup ? largeRadius : smallRadius,
+ };
+ }, [isFirstOfGroup, isLastOfGroup, position]);
+
+ return (
+ <MessageTooltip position={position}>
+ <Box
+ sx={{
+ width: 'fit-content',
+ backgroundColor: backgroundColor,
+ padding: '16px',
+ ...radius,
+ }}
+ >
+ {children}
+ </Box>
+ </MessageTooltip>
+ );
+};
+
+interface ParticipantNameProps {
+ name: string;
+}
+
+const ParticipantName = ({ name }: ParticipantNameProps) => {
+ return (
+ <Box marginBottom="6px" marginLeft="16px" marginRight="16px">
+ <Typography variant="caption" color="#A7A7A7" fontWeight={700}>
+ {name}
+ </Typography>
+ </Box>
+ );
+};