convert Message and MessageList to Typescript

Change-Id: Idbca1335dccbf910c2e1715769ff46c575472d00
diff --git a/JamiDaemon.ts b/JamiDaemon.ts
index 340e2e5..bd8a537 100755
--- a/JamiDaemon.ts
+++ b/JamiDaemon.ts
@@ -86,7 +86,7 @@
         account.volatileDetails = details;
       },
       IncomingAccountMessage: (accountId: string, from: Account, message: Message) => {
-        console.log(`Received message: ${accountId} ${from} ${message['text/plain']}`);
+        console.log(`Received message: ${accountId} ${from} ${message}`);
         /*
                 if (parser.validate(message["text/plain"]) === true) {
                     console.log(message["text/plain"])
diff --git a/client/src/components/Message.jsx b/client/src/components/Message.tsx
similarity index 66%
rename from client/src/components/Message.jsx
rename to client/src/components/Message.tsx
index 7e14d10..bfc442f 100644
--- a/client/src/components/Message.jsx
+++ b/client/src/components/Message.tsx
@@ -15,44 +15,69 @@
  * License along with this program.  If not, see
  * <https://www.gnu.org/licenses/>.
  */
-import { Box, Chip, Divider, List, ListItemButton, ListItemText, Stack, Tooltip, Typography } from '@mui/material';
+import {
+  Box,
+  Chip,
+  Divider,
+  List,
+  ListItemButton,
+  ListItemText,
+  Stack,
+  Theme,
+  Tooltip,
+  Typography,
+} from '@mui/material';
 import { styled } from '@mui/material/styles';
-import dayjs from 'dayjs';
+import dayjs, { Dayjs } from 'dayjs';
 import isToday from 'dayjs/plugin/isToday';
 import isYesterday from 'dayjs/plugin/isYesterday';
-import { useCallback, useMemo, useState } from 'react';
+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.tsx';
+import { EmojiButton, MoreButton, ReplyMessageButton } from './Button';
 import ConversationAvatar from './ConversationAvatar';
-import { OppositeArrowsIcon, TrashBinIcon, TwoSheetsIcon } from './SvgIcon.tsx';
+import { OppositeArrowsIcon, TrashBinIcon, TwoSheetsIcon } from './SvgIcon';
 
 dayjs.extend(isToday);
 dayjs.extend(isYesterday);
 
-export const MessageCall = (props) => {
+type MessagePosition = 'start' | 'end';
+
+export const MessageCall = () => {
   return <Stack alignItems="center">&quot;Appel&quot;</Stack>;
 };
 
-export const MessageInitial = (props) => {
+export const MessageInitial = () => {
   const { t } = useTranslation();
   return <Stack alignItems="center">{t('message_swarm_created')}</Stack>;
 };
 
-export const MessageDataTransfer = (props) => {
+interface MessageDataTransferProps {
+  position: MessagePosition;
+  isFirstOfGroup: boolean;
+  isLastOfGroup: boolean;
+}
+
+export const MessageDataTransfer = ({ position, isFirstOfGroup, isLastOfGroup }: MessageDataTransferProps) => {
   return (
     <MessageBubble
       backgroundColor={'#E5E5E5'}
-      position={props.position}
-      isFirstOfGroup={props.isFirstOfGroup}
-      isLastOfGroup={props.isLastOfGroup}
+      position={position}
+      isFirstOfGroup={isFirstOfGroup}
+      isLastOfGroup={isLastOfGroup}
     >
       &quot;data-transfer&quot;
     </MessageBubble>
   );
 };
 
-export const MessageMember = (props) => {
+interface MessageMemberProps {
+  message: Message;
+}
+
+export const MessageMember = ({ message }: MessageMemberProps) => {
   const { t } = useTranslation();
   return (
     <Stack alignItems="center">
@@ -60,32 +85,52 @@
         sx={{
           width: 'fit-content',
         }}
-        label={t('message_user_joined', { user: props.message.author })}
+        label={t('message_user_joined', { user: message.author })}
       />
     </Stack>
   );
 };
 
-export const MessageMerge = (props) => {
+export const MessageMerge = () => {
   return <Stack alignItems="center">&quot;merge&quot;</Stack>;
 };
 
-export const MessageText = (props) => {
+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={props.bubbleColor}
-      position={props.position}
-      isFirstOfGroup={props.isFirstOfGroup}
-      isLastOfGroup={props.isLastOfGroup}
+      backgroundColor={bubbleColor}
+      position={position}
+      isFirstOfGroup={isFirstOfGroup}
+      isLastOfGroup={isLastOfGroup}
     >
-      <Typography variant="body1" color={props.textColor} textAlign={props.position}>
-        {props.message.body}
+      <Typography variant="body1" color={textColor} textAlign={position}>
+        {message.body}
       </Typography>
     </MessageBubble>
   );
 };
 
-export const MessageDate = ({ time }) => {
+interface MessageDateProps {
+  time: Dayjs;
+}
+
+export const MessageDate = ({ time }: MessageDateProps) => {
   let textDate;
 
   if (time.isToday()) {
@@ -128,7 +173,12 @@
   );
 };
 
-export const MessageTime = ({ time, hasDateOnTop }) => {
+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}`;
@@ -142,19 +192,24 @@
   );
 };
 
-export const MessageBubblesGroup = (props) => {
-  const isUser = props.messages[0]?.author === props.account.getUri();
+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 = props.account.getDisplayName();
+    authorName = account.getDisplayName();
   } else {
-    const member = props.members.find((member) => props.messages[0]?.author === member.contact.getUri());
-    const contact = member.contact;
-    authorName = contact.getDisplayName();
+    const member = members.find((member) => messages[0]?.author === member.contact.getUri());
+    authorName = member?.contact?.getDisplayName() || '';
   }
 
   return (
@@ -171,14 +226,14 @@
         width="66.66%"
         alignItems={position}
       >
-        <ParticipantName name={authorName} position={position} />
+        <ParticipantName name={authorName} />
         <Stack // Container for a group of message bubbles
           spacing="6px"
           alignItems={position}
           direction="column-reverse"
         >
-          {props.messages.map((message, index) => {
-            let Component;
+          {messages.map((message, index) => {
+            let Component: typeof MessageText | typeof MessageDataTransfer;
             switch (message.type) {
               case 'text/plain':
                 Component = MessageText;
@@ -186,6 +241,8 @@
               case 'application/data-transfer+json':
                 Component = MessageDataTransfer;
                 break;
+              default:
+                return null;
             }
             return (
               <Component // Single message
@@ -194,8 +251,8 @@
                 textColor={textColor}
                 position={position}
                 bubbleColor={bubbleColor}
-                isFirstOfGroup={index == props.messages.length - 1}
-                isLastOfGroup={index == 0}
+                isFirstOfGroup={index === messages.length - 1}
+                isLastOfGroup={index === 0}
               />
             );
           })}
@@ -205,7 +262,13 @@
   );
 };
 
-const MessageTooltip = styled(({ className, ...props }) => {
+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 = [
@@ -234,9 +297,8 @@
 
   return (
     <Tooltip
-      {...props}
       classes={{ tooltip: className }} // Required for styles. Don't know why
-      placement={props.position == 'start' ? 'right-start' : 'left-start'}
+      placement={position === 'start' ? 'right-start' : 'left-start'}
       PopperProps={{
         modifiers: [
           {
@@ -250,7 +312,6 @@
       onClose={onClose}
       title={
         <Stack>
-          {' '}
           {/* Whole tooltip's content */}
           <Stack // Main options
             direction="row"
@@ -281,7 +342,7 @@
                         sx={{
                           height: '16px',
                           margin: 0,
-                          color: (theme) => theme.palette.primary.dark,
+                          color: (theme: Theme) => theme?.palette?.primary?.dark,
                         }}
                       />
                       <ListItemText
@@ -303,9 +364,11 @@
           )}
         </Stack>
       }
-    />
+    >
+      {children}
+    </Tooltip>
   );
-})(({ theme, position }) => {
+})(({ position }) => {
   const largeRadius = '20px';
   const smallRadius = '5px';
   return {
@@ -313,52 +376,64 @@
     padding: '16px',
     boxShadow: '3px 3px 7px #00000029',
     borderRadius: largeRadius,
-    borderStartStartRadius: position == 'start' ? smallRadius : largeRadius,
-    borderStartEndRadius: position == 'end' ? smallRadius : largeRadius,
+    borderStartStartRadius: position === 'start' ? smallRadius : largeRadius,
+    borderStartEndRadius: position === 'end' ? smallRadius : largeRadius,
   };
 });
 
-const MessageBubble = (props) => {
+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 (props.position == 'start') {
+    if (position === 'start') {
       return {
-        borderStartStartRadius: props.isFirstOfGroup ? largeRadius : smallRadius,
+        borderStartStartRadius: isFirstOfGroup ? largeRadius : smallRadius,
         borderStartEndRadius: largeRadius,
-        borderEndStartRadius: props.isLastOfGroup ? largeRadius : smallRadius,
+        borderEndStartRadius: isLastOfGroup ? largeRadius : smallRadius,
         borderEndEndRadius: largeRadius,
       };
     }
     return {
       borderStartStartRadius: largeRadius,
-      borderStartEndRadius: props.isFirstOfGroup ? largeRadius : smallRadius,
+      borderStartEndRadius: isFirstOfGroup ? largeRadius : smallRadius,
       borderEndStartRadius: largeRadius,
-      borderEndEndRadius: props.isLastOfGroup ? largeRadius : smallRadius,
+      borderEndEndRadius: isLastOfGroup ? largeRadius : smallRadius,
     };
-  }, [props.isFirstOfGroup, props.isLastOfGroup, props.position]);
+  }, [isFirstOfGroup, isLastOfGroup, position]);
 
   return (
-    <MessageTooltip position={props.position}>
+    <MessageTooltip position={position}>
       <Box
         sx={{
           width: 'fit-content',
-          backgroundColor: props.backgroundColor,
+          backgroundColor: backgroundColor,
           padding: '16px',
           ...radius,
         }}
       >
-        {props.children}
+        {children}
       </Box>
     </MessageTooltip>
   );
 };
 
-const ParticipantName = (props) => {
+interface ParticipantNameProps {
+  name: string;
+}
+
+const ParticipantName = ({ name }: ParticipantNameProps) => {
   return (
     <Box marginBottom="6px" marginLeft="16px" marginRight="16px">
       <Typography variant="caption" color="#A7A7A7" fontWeight={700}>
-        {props.name}
+        {name}
       </Typography>
     </Box>
   );
diff --git a/client/src/components/MessageList.jsx b/client/src/components/MessageList.jsx
deleted file mode 100644
index 513af54..0000000
--- a/client/src/components/MessageList.jsx
+++ /dev/null
@@ -1,178 +0,0 @@
-/*
- * 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 { Stack } from '@mui/system';
-import dayjs from 'dayjs';
-import dayOfYear from 'dayjs/plugin/dayOfYear';
-import isBetween from 'dayjs/plugin/isBetween';
-import { useMemo } from 'react';
-
-import {
-  MessageBubblesGroup,
-  MessageCall,
-  MessageDate,
-  MessageInitial,
-  MessageMember,
-  MessageMerge,
-  MessageTime,
-} from './Message';
-
-dayjs.extend(dayOfYear);
-dayjs.extend(isBetween);
-
-export default function MessageList(props) {
-  const messagesComponents = useMemo(
-    () => buildMessagesList(props.account, props.members, props.messages),
-    [props.account, props.members, props.messages]
-  );
-
-  return (
-    <Stack direction="column-reverse">
-      {messagesComponents?.map(({ Component, id, props }) => (
-        <Component key={id} {...props} />
-      ))}
-    </Stack>
-  );
-}
-
-const buildMessagesList = (account, members, messages) => {
-  if (messages.length == 0) {
-    return null;
-  }
-
-  const components = [];
-  let lastTime = dayjs.unix(messages[0].timestamp);
-  let lastAuthor = messages[0].author;
-  let messageBubblesGroup = [];
-
-  const pushMessageBubblesGroup = () => {
-    if (messageBubblesGroup.length == 0) {
-      return;
-    }
-    components.push({
-      id: `group-${messageBubblesGroup[0].id}`,
-      Component: MessageBubblesGroup,
-      props: { account, members, messages: messageBubblesGroup },
-    });
-    messageBubblesGroup = [];
-  };
-
-  const pushMessageCall = (message) => {
-    components.push({
-      id: `call-${message.id}`,
-      Component: MessageCall,
-      props: { message },
-    });
-  };
-
-  const pushMessageMember = (message) => {
-    components.push({
-      id: `member-${message.id}`,
-      Component: MessageMember,
-      props: { message },
-    });
-  };
-
-  const pushMessageMerge = (message) => {
-    components.push({
-      id: `merge-${message.id}`,
-      Component: MessageMerge,
-      props: { message },
-    });
-  };
-
-  const pushMessageTime = (message, time, hasDateOnTop = false) => {
-    components.push({
-      id: `time-${message.id}`,
-      Component: MessageTime,
-      props: { time, hasDateOnTop },
-    });
-  };
-
-  const pushMessageDate = (message, time) => {
-    components.push({
-      id: `date-${message.id}`,
-      Component: MessageDate,
-      props: { time },
-    });
-  };
-
-  const pushMessageInitial = (message) => {
-    components.push({
-      id: `initial-${message.id}`,
-      Component: MessageInitial,
-      props: { message },
-    });
-  };
-
-  messages.forEach((message) => {
-    // most recent messages first
-    switch (message.type) {
-      case 'text/plain':
-      case 'application/data-transfer+json':
-        if (lastAuthor != message.author) {
-          pushMessageBubblesGroup();
-        }
-        messageBubblesGroup.push(message);
-        break;
-      case 'application/call-history+json':
-        pushMessageBubblesGroup();
-        pushMessageCall(message);
-        break;
-      case 'member':
-        pushMessageBubblesGroup();
-        pushMessageMember(message);
-        break;
-      case 'merge':
-        pushMessageBubblesGroup();
-        pushMessageMerge(message);
-        break;
-      case 'initial':
-      default:
-        break;
-    }
-
-    const time = dayjs.unix(message.timestamp);
-    if (message.type == 'initial') {
-      pushMessageBubblesGroup();
-      pushMessageTime(message, time, true);
-      pushMessageDate(message, time);
-      pushMessageInitial(message);
-    } else {
-      if (
-        // If the date is different
-        lastTime?.year() != time.year() ||
-        lastTime?.dayOfYear() != time.dayOfYear()
-      ) {
-        pushMessageBubblesGroup();
-        pushMessageTime(message, time, true);
-        pushMessageDate(message, time);
-      } else if (
-        // If more than 5 minutes have passed since the last message
-        !lastTime.isBetween(time, time?.add(5, 'minute'))
-      ) {
-        pushMessageBubblesGroup();
-        pushMessageTime(message, time);
-      }
-
-      lastTime = time;
-      lastAuthor = message.author;
-    }
-  });
-
-  return components;
-};
diff --git a/client/src/components/MessageList.tsx b/client/src/components/MessageList.tsx
new file mode 100644
index 0000000..6c3c8bf
--- /dev/null
+++ b/client/src/components/MessageList.tsx
@@ -0,0 +1,154 @@
+/*
+ * 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 { Stack } from '@mui/system';
+import dayjs, { Dayjs } from 'dayjs';
+import dayOfYear from 'dayjs/plugin/dayOfYear';
+import isBetween from 'dayjs/plugin/isBetween';
+import { Account, ConversationMember, Message } from 'jami-web-common';
+import { ReactNode, useMemo } from 'react';
+
+import {
+  MessageBubblesGroup,
+  MessageCall,
+  MessageDate,
+  MessageInitial,
+  MessageMember,
+  MessageMerge,
+  MessageTime,
+} from './Message';
+
+dayjs.extend(dayOfYear);
+dayjs.extend(isBetween);
+
+interface MessageListProps {
+  account: Account;
+  members: ConversationMember[];
+  messages: Message[];
+}
+
+export default function MessageList({ account, members, messages }: MessageListProps) {
+  const messageComponents = useMemo(() => buildMessagesList(account, members, messages), [account, members, messages]);
+  return <Stack direction="column-reverse">{messageComponents}</Stack>;
+}
+
+const buildMessagesList = (account: Account, members: ConversationMember[], messages: Message[]) => {
+  if (messages.length === 0) {
+    return [];
+  }
+
+  const messageComponents: ReactNode[] = [];
+  let lastTime = dayjs.unix(Number(messages[0].timestamp));
+  let lastAuthor = messages[0].author;
+  let messagesGroup: Message[] = [];
+
+  const pushMessageBubblesGroup = () => {
+    if (messagesGroup.length === 0) {
+      return;
+    }
+    const props = { account, members, messages: messagesGroup };
+    messageComponents.push(<MessageBubblesGroup key={`group-${messagesGroup[0].id}`} {...props} />);
+    messagesGroup = [];
+  };
+
+  const pushMessageCall = (message: Message) => {
+    const props = { message };
+    messageComponents.push(<MessageCall key={`call-${message.id}`} {...props} />);
+  };
+
+  const pushMessageMember = (message: Message) => {
+    const props = { message };
+    messageComponents.push(<MessageMember key={`member-${message.id}`} {...props} />);
+  };
+
+  const pushMessageMerge = (message: Message) => {
+    const props = { message };
+    messageComponents.push(<MessageMerge key={`merge-${message.id}`} {...props} />);
+  };
+
+  const pushMessageTime = (message: Message, time: Dayjs, hasDateOnTop = false) => {
+    const props = { time, hasDateOnTop };
+    messageComponents.push(<MessageTime key={`time-${message.id}`} {...props} />);
+  };
+
+  const pushMessageDate = (message: Message, time: Dayjs) => {
+    const props = { time };
+    messageComponents.push(<MessageDate key={`date-${message.id}`} {...props} />);
+  };
+
+  const pushMessageInitial = (message: Message) => {
+    const props = { message };
+    messageComponents.push(<MessageInitial key={`initial-${message.id}`} {...props} />);
+  };
+
+  messages.forEach((message) => {
+    // most recent messages first
+    switch (message.type) {
+      case 'text/plain':
+      case 'application/data-transfer+json':
+        if (lastAuthor !== message.author) {
+          pushMessageBubblesGroup();
+        }
+        messagesGroup.push(message);
+        break;
+      case 'application/call-history+json':
+        pushMessageBubblesGroup();
+        pushMessageCall(message);
+        break;
+      case 'member':
+        pushMessageBubblesGroup();
+        pushMessageMember(message);
+        break;
+      case 'merge':
+        pushMessageBubblesGroup();
+        pushMessageMerge(message);
+        break;
+      case 'initial':
+      default:
+        break;
+    }
+
+    const time = dayjs.unix(Number(message.timestamp));
+    if (message.type === 'initial') {
+      pushMessageBubblesGroup();
+      pushMessageTime(message, time, true);
+      pushMessageDate(message, time);
+      pushMessageInitial(message);
+    } else {
+      if (
+        // If the date is different
+        lastTime?.year() !== time.year() ||
+        lastTime?.dayOfYear() !== time.dayOfYear()
+      ) {
+        pushMessageBubblesGroup();
+        pushMessageTime(message, time, true);
+        pushMessageDate(message, time);
+      } else if (
+        // If more than 5 minutes have passed since the last message
+        !lastTime.isBetween(time, time?.add(5, 'minute'))
+      ) {
+        pushMessageBubblesGroup();
+        pushMessageTime(message, time);
+      }
+
+      lastTime = time;
+      lastAuthor = message.author;
+    }
+  });
+
+  return messageComponents;
+};
diff --git a/common/src/Conversation.ts b/common/src/Conversation.ts
index b5ef9d7..5776c59 100644
--- a/common/src/Conversation.ts
+++ b/common/src/Conversation.ts
@@ -25,7 +25,27 @@
 
 type ConversationInfos = Record<string, unknown>;
 
-export type Message = Record<string, string>;
+export type Message = {
+  id: string;
+  author: string;
+  timestamp: string;
+  type:
+    | 'application/call-history+json'
+    | 'application/data-transfer+json'
+    | 'application/update-profile'
+    | 'initial'
+    | 'member'
+    | 'merge'
+    | 'text/plain'
+    | 'vote';
+  linearizedParent: string;
+  parents: string;
+  body?: string;
+  duration?: string;
+  to?: string;
+  invited?: string;
+};
+
 type ConversationRequest = PromiseExecutor<Message[]>;
 
 type ConversationListeners = Record<