set styles for messages

Change-Id: I8b86255a55a745a305f953f77122a98970de0958
diff --git a/client/package.json b/client/package.json
index 885e6ef..b60b40e 100644
--- a/client/package.json
+++ b/client/package.json
@@ -5,6 +5,8 @@
   "private": true,
   "dependencies": {
     "@babel/runtime": "^7.17.9",
+    "@emotion/react": "^11.9.0",
+    "@emotion/styled": "^11.8.1",
     "@mui/icons-material": "^5.6.2",
     "@mui/lab": "^5.0.0-alpha.79",
     "@mui/material": "^5.6.3",
@@ -12,6 +14,7 @@
     "@testing-library/jest-dom": "^4.2.4",
     "@testing-library/react": "^9.5.0",
     "@testing-library/user-event": "^7.2.1",
+    "dayjs": "^1.11.5",
     "emoji-picker-react": "^3.5.1",
     "react": "^17.0.2",
     "react-dom": "^17.0.2",
diff --git a/client/src/components/ConversationView.js b/client/src/components/ConversationView.js
index d0ab227..075e96a 100644
--- a/client/src/components/ConversationView.js
+++ b/client/src/components/ConversationView.js
@@ -5,9 +5,11 @@
 import Conversation from '../../../model/Conversation';
 import LoadingPage from './loading';
 import io from "socket.io-client";
+import { Box, Stack, Typography } from '@mui/material';
+import ConversationAvatar from './ConversationAvatar';
 
 const ConversationView = props => {
-  const [loadingMesages, setLoadingMesages] = useState(false)
+  const [loadingMessages, setLoadingMessages] = useState(false)
   const [socket, setSocket] = useState(undefined)
   const [state, setState] = useState({
     loaded: false,
@@ -65,7 +67,7 @@
   }, [state.conversation ? state.conversation.getId() : "", socket])
 
   useEffect(() => {
-    if (!loadingMesages || !state.conversation)
+    if (!loadingMessages || !state.conversation)
       return
     console.log(`Load more messages`)
     const controller = new AbortController()
@@ -73,7 +75,7 @@
       .then(res => res.json())
       .then(messages => {
         console.log(messages)
-        setLoadingMesages(false)
+        setLoadingMessages(false)
         setState(state => {
           if (state.conversation)
             state.conversation.addLoadedMessages(messages)
@@ -81,7 +83,7 @@
           })
       }).catch(e => console.log(e))
       return () => controller.abort()
-  }, [state, loadingMesages])
+  }, [state, loadingMessages])
 
   const sendMessage = (message) => {
     authManager.fetch(`/api/accounts/${props.accountId}/conversations/${props.conversationId}`, {
@@ -98,12 +100,35 @@
       return <LoadingPage />
   } else if (state.error === true) {
       return <div>Error loding {props.conversationId}</div>
-  } else {
-  return <div className="messenger">
-      <MessageList conversation={state.conversation} loading={loadingMesages} loadMore={() => setLoadingMesages(true)} messages={state.conversation.getMessages()} />
-      <SendMessageForm onSend={sendMessage} />
-    </div>
   }
+
+  return (
+    <Stack
+      flexGrow={1}
+      height="100%"
+    >
+      <Stack direction="row" flexGrow={0}>
+        <Box style={{ margin: 16, flexShrink: 0 }}>
+          <ConversationAvatar displayName={state.conversation.getDisplayNameNoFallback()} />
+        </Box>
+        <Box style={{ flex: "1 1 auto", overflow: 'hidden' }}>
+          <Typography className="title" variant="h6">{state.conversation.getDisplayName()}</Typography>
+          <Typography className="subtitle" variant="subtitle1" >{state.conversation.getId()}</Typography>
+        </Box>
+      </Stack>
+      <Stack flexGrow={1} overflow="auto">
+        <MessageList
+          conversationId={state.conversation.getId()}
+          loading={loadingMessages} 
+          loadMore={() => setLoadingMessages(true)}
+          messages={state.conversation.getMessages()}
+        />
+      </Stack>
+      <Stack flexGrow={0}>
+        <SendMessageForm onSend={sendMessage} />
+      </Stack>
+    </Stack>
+  )
 }
 
 export default ConversationView
\ No newline at end of file
diff --git a/client/src/components/Message.js b/client/src/components/Message.js
index 7cfbbdc..b86c43f 100644
--- a/client/src/components/Message.js
+++ b/client/src/components/Message.js
@@ -1,27 +1,236 @@
-import { Typography } from '@mui/material'
-import { GroupOutlined } from '@mui/icons-material'
-import React from 'react'
-import ConversationAvatar from './ConversationAvatar'
+import { Box, Chip, Divider, Stack, Typography } from "@mui/material"
+import dayjs from "dayjs"
+import isToday from "dayjs/plugin/isToday"
+import isYesterday from "dayjs/plugin/isYesterday"
+import React from "react"
 
-function Message(props) {
-    const message = props.message
-    if (message.type == 'text/plain')
-        return (<div className="message">
-            <div className="message-avatar">
-                <ConversationAvatar name={message.author} /></div>
-            <Typography className="message-text">{message.body}</Typography>
-        </div>)
-    else if (message.type == 'contact')
-        return (<div className="contact-event">
-            <Typography className="message-text">Contact event</Typography>
-        </div>)
-    else if (message.type == 'initial')
-        return (<div className="conversation-event">
-            <Typography variant="h6" className="message-text" color="textSecondary">
-                <div className="inline-avatar"><GroupOutlined color="action" style={{ fontSize: 32 }} /></div>Conversation created
-                </Typography>
-        </div>)
-    else return ''
+dayjs.extend(isToday)
+dayjs.extend(isYesterday)
+
+export const MessageCall = (props) => {
+  return (
+    <Stack
+      alignItems="center"
+    >
+      "Appel"
+    </Stack>
+  )
 }
 
-export default Message
\ No newline at end of file
+export const MessageInitial = (props) => {
+  return (
+    <Stack
+      alignItems="center"
+    >
+      "Le Swarm a été créé"
+    </Stack>
+  )
+}
+
+export const MessageDataTransfer = (props) => {
+  return (
+    <MessageBubble
+      backgroundColor={"#E5E5E5"}
+      position={props.position}
+      isFirstOfGroup={props.isFirstOfGroup}
+      isLastOfGroup={props.isLastOfGroup}
+    >
+      "data-transfer"
+    </MessageBubble>
+  )
+}
+
+export const MessageMember = (props) => {
+  return (
+    <Stack
+      alignItems="center"
+    >
+      <Chip
+        sx={{
+          width: "fit-content",
+        }}
+        label={`${props.message.author} s'est joint`}
+      />
+    </Stack>
+  )
+}
+
+export const MessageMerge = (props) => {
+  return (
+    <Stack
+      alignItems="center"
+    >
+      "merge"
+    </Stack>
+  )
+}
+
+export const MessageText = (props) => {
+  return (
+    <MessageBubble
+      backgroundColor={props.bubbleColor}
+      position={props.position}
+      isFirstOfGroup={props.isFirstOfGroup}
+      isLastOfGroup={props.isLastOfGroup}
+    >
+      <Typography variant="body1" color={props.textColor}>
+        {props.message.body}
+      </Typography>
+    </MessageBubble>
+  )
+}
+
+export const MessageDate = ({time}) => {
+  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>
+        {textDate}
+      </Divider>
+    </Box>
+  )
+}
+
+export const MessageTime = ({time, hasDateOnTop}) => {
+  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>
+  )
+}
+
+export const MessageBubblesGroup = (props) => {
+
+  const isUser = true // should access user from the store
+  const position = isUser ? "end" : "start"
+  const bubbleColor = isUser ? "#005699" : "#E5E5E5"
+  const textColor = isUser ? "white" : "black"
+
+  return (
+    <Stack // Container for an entire row with a single group of bubbles
+      direction="row"
+      justifyContent={position}
+    >
+      <Stack // Container for a group of bubbles with the partipants informations
+        width="66.66%"
+        paddingTop="30px"
+        alignItems={position}
+      >
+        <ParticipantName
+          name={props.messages[0]?.author}
+          position={position}
+        />
+        <Stack // Container for the bubbles alone
+          spacing="6px"
+          alignItems={position}
+          direction="column-reverse"
+        >
+          {props.messages.map(
+            (message, index) => {
+              let Component
+              switch (message.type) {
+                case "text/plain":
+                  Component = MessageText
+                  break
+                case "application/data-transfer+json":
+                  Component = MessageDataTransfer
+                  break
+              } 
+              return (
+                <Component // Container for a single bubble
+                  key={message.id} 
+                  message={message}
+                  textColor={textColor}
+                  position={position}
+                  bubbleColor={bubbleColor}
+                  isFirstOfGroup={index == props.messages.length-1}
+                  isLastOfGroup={index == 0}
+                />
+              )
+            }
+          )}
+        </Stack>
+      </Stack>
+    </Stack>
+  )
+}
+
+const MessageBubble = (props) => {
+  const largeRadius = "20px"
+  const smallRadius = "5px"
+  const radius = React.useMemo(() => {
+    if (props.position == "start") {
+      return {
+        borderStartStartRadius: props.isFirstOfGroup ? largeRadius : smallRadius,
+        borderStartEndRadius: largeRadius,
+        borderEndStartRadius: props.isLastOfGroup ? largeRadius : smallRadius,
+        borderEndEndRadius: largeRadius,
+      }
+    }
+    return {
+      borderStartStartRadius: largeRadius,
+      borderStartEndRadius: props.isFirstOfGroup ? largeRadius : smallRadius,
+      borderEndStartRadius: largeRadius,
+      borderEndEndRadius: props.isLastOfGroup ? largeRadius : smallRadius,
+    }
+  }, [props.isFirstOfGroup, props.isLastOfGroup, props.position])
+
+  return (
+    <Box
+      sx={{
+        width: "fit-content",
+        backgroundColor: props.backgroundColor,
+        padding: "16px",
+        ...radius,
+      }}
+    >
+      {props.children}
+    </Box>
+  )
+}
+
+const ParticipantName = (props) => {
+  return (
+    <Box
+      marginBottom="6px"
+      marginLeft="16px"
+      marginRight="16px"
+    >
+      <Typography
+        variant="caption"
+        color="#A7A7A7"
+        fontWeight={700}
+      >
+        {props.name}
+      </Typography>
+    </Box>
+  )
+}
\ No newline at end of file
diff --git a/client/src/components/MessageList.js b/client/src/components/MessageList.js
index 3ae1b1c..3f16e46 100644
--- a/client/src/components/MessageList.js
+++ b/client/src/components/MessageList.js
@@ -1,33 +1,157 @@
-import Message from './Message'
+import dayjs from "dayjs"
 import React, { useEffect } from 'react'
-import { Box, Divider, Typography } from '@mui/material'
-import ConversationAvatar from './ConversationAvatar'
+import dayOfYear from 'dayjs/plugin/dayOfYear'
+import isBetween from 'dayjs/plugin/isBetween'
+import { Stack } from "@mui/system"
+import { MessageCall, MessageDate, MessageInitial, MessageMember, MessageBubblesGroup, MessageTime, MessageMerge } from "./Message"
+
+dayjs.extend(dayOfYear)
+dayjs.extend(isBetween)
 
 export default function MessageList(props) {
-  const displayName = props.conversation.getDisplayName()
-  const messages = props.conversation.getMessages()
+  const messagesComponents = buildMessagesList(props.messages)
 
   useEffect(() => {
     if (!props.loading)
       props.loadMore()
-  }, [props.conversation.getId()])
+  }, [props.conversationId])
 
   return (
-    <React.Fragment>
-      <Box className="conversation-header">
-        <Box style={{ margin: 16, flexShrink: 0 }}>
-          <ConversationAvatar displayName={props.conversation.getDisplayNameNoFallback()} />
-        </Box>
-        <Box style={{ flex: "1 1 auto", overflow: 'hidden' }}>
-          <Typography className="title" variant="h6">{displayName}</Typography>
-          <Typography className="subtitle" variant="subtitle1" >{props.conversation.getId()}</Typography>
-        </Box>
-        <Divider orientation="horizontal" />
-      </Box>
-      <div className="message-list">
-        {messages.map((message) => <Message key={message.id} message={message} />)}
-        <div style={{ border: "1px solid transparent" }}/>
-      </div>
-    </React.Fragment>
+    <Stack
+      marginLeft="16px"
+      marginRight="16px"
+      direction="column-reverse"
+    >
+      {messagesComponents?.map(
+        ({Component, id, props}) => <Component key={id} {...props}/>
+      )}
+    </Stack>
   )
-}
\ No newline at end of file
+}
+
+const buildMessagesList = (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: { 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/SendMessageForm.js b/client/src/components/SendMessageForm.js
index f447f34..8377575 100644
--- a/client/src/components/SendMessageForm.js
+++ b/client/src/components/SendMessageForm.js
@@ -1,6 +1,6 @@
 import React from 'react'
 import makeStyles from '@mui/styles/makeStyles';
-import { IconButton, InputBase, Paper, Popper } from '@mui/material'
+import { Box, IconButton, InputBase, Paper, Popper } from '@mui/material'
 import { Send, EmojiEmotionsRounded } from '@mui/icons-material'
 import EmojiPicker from 'emoji-picker-react'
 
@@ -50,7 +50,7 @@
   const id = open ? 'simple-popover' : undefined
 
   return (
-    <div className="send-message-form">
+    <Box >
       <Paper component="form"
         onSubmit={handleSubmit}
         className={classes.root}>
@@ -91,6 +91,6 @@
           <Send />
         </IconButton>
       </Paper>
-    </div>
+    </Box>
   );
 }
diff --git a/client/src/index.scss b/client/src/index.scss
index 5f0427e..a9b9681 100644
--- a/client/src/index.scss
+++ b/client/src/index.scss
@@ -1,158 +1,16 @@
-:root {
-  --main-color: #1f1f1f;
-  --secondary-color: white;
-  --third-color: #8b8b8b;
-  --main-text-color: #3e5869;
-  --secondary-text-color: #b0c7d6;
-  --send-message-form: #f5f5f5;
-  --scroll-bar-bg-color: #04000000;
-}
-
 html,
 body {
   height: 100%;
   margin: 0;
   padding: 0;
-  font-family: "Open Sans", sans-serif;
-  font-weight: 200;
-  color: #3e5869;
-  --scroll-bar-bg-color: rgba(0,0,0,0.2);
-}
-
-#root {
-  height: 100%;
-}
-
-.app {
-  display: grid;
-  height: 100%;
-  grid-template-columns: 320px 1fr;
-  grid-template-rows: 40px 50px 1fr;
-  grid-template-areas:
-    "h m"
-    "n m"
-    "r m";
-}
-
-.messenger {
-  grid-area: m;
-  display: flex;
-  flex-direction: column;
-  overflow: hidden;
-}
-
-.conversation-header {
-  grid-area: h;
-  display: flex;
-  align-items: center;
-  padding-right: 8px;
-
-  .title, .subtitle {
-    overflow: hidden;
-    text-overflow: ellipsis;
-  }
-}
-
-.send-message-form {
-  margin: 0 16px 16px 8px;
-}
-
-.MuiContainer-root {
-  padding-bottom: 32px;
-}
-
-.login {
-  background-color: var(--main-color);
 }
 
 .main-search {
   grid-area: n;
 }
 
-/* REST OF CSS */
-
 .main-search-input {
   padding: 8px;
   background-color: var(--secondary-color);
   width: 100%;
 }
-
-.rooms-list {
-  grid-area: r;
-  overflow-y: scroll;
-
-  .MuiListItemText-primary,
-  .MuiListItemText-secondary {
-    overflow: hidden;
-    text-overflow: ellipsis;
-  }
-
-  scrollbar-color: var(--scroll-bar-bg-color) transparent;
-  scrollbar-width: thin;
-}
-::-webkit-scrollbar {
-  -webkit-appearance: none;
-  width: 8px;
-}
-::-webkit-scrollbar-thumb {
-    background-color: var(--scroll-bar-bg-color);
-    border-radius: 4px;
-}
-
-.list-placeholder {
-  margin: 32px auto;
-  width: 256px;
-  text-align: center;
-
-  .subtitle {
-    color: #a0a0a0;
-  }
-}
-
-.message-list {
-  flex: 1;
-  height: 100%;
-  overflow: auto;
-  display: flex;
-  flex-direction: column-reverse;
-  padding: 16px 32px;
-
-  .conversation-event {
-    margin: 8px auto;
-
-    .inline-avatar {
-      display: inline-block;
-      vertical-align: middle;
-      margin: 16px;
-    }
-  }
-
-  .message {
-    margin: 8px 0;
-
-    .message-avatar {
-      display: inline-block;
-      vertical-align: middle;
-    }
-    .message-username {
-      font-size: 11px;
-      font-weight: bold;
-      color: var(--secondary-color);
-      opacity: 0.9;
-      margin-bottom: 6px;
-    }
-    .message-text {
-      background: var(--third-color);
-      color: var(--secondary-color);
-      display: inline;
-      padding: 8px 16px;
-      border-radius: 16px;
-      margin: 8px;
-    }
-  }
-}
-
-.send-message-card {
-  border-radius: 8px;
-  margin: 16px;
-}
diff --git a/client/src/pages/messenger.jsx b/client/src/pages/messenger.jsx
index 3833956..48edb5d 100644
--- a/client/src/pages/messenger.jsx
+++ b/client/src/pages/messenger.jsx
@@ -11,6 +11,7 @@
 import AddContactPage from './addContactPage.jsx';
 import LoadingPage from '../components/loading';
 import { useParams } from 'react-router';
+import { Stack } from '@mui/material';
 
 const Messenger = (props) => {
   const [conversations, setConversations] = useState(undefined)
@@ -57,15 +58,30 @@
 
   console.log("Messenger render")
   return (
-    <div className="app" >
-      <Header />
-      <NewContactForm onChange={setSearchQuery} />
-      {conversations ?
-        <ConversationList search={searchResult} conversations={conversations} accountId={accountId} /> :
-        <div className="rooms-list"><LoadingPage /></div>}
-      {conversationId && <ConversationView accountId={accountId} conversationId={conversationId} />}
-      {contactId && <AddContactPage accountId={accountId} contactId={contactId} />}
-    </div>
+    <Stack
+      direction="row"
+      height="100vh"
+      width="100vw"
+    >
+      <Stack
+        flexGrow={0}
+        flexShrink={0}
+        overflow="auto"
+      >
+        <Header />
+        <NewContactForm onChange={setSearchQuery} />
+        {contactId && <AddContactPage accountId={accountId} contactId={contactId} />}
+        {conversations ?
+          <ConversationList search={searchResult} conversations={conversations} accountId={accountId} /> :
+          <div className="rooms-list"><LoadingPage /></div>
+        }
+      </Stack>
+      <Stack
+        flexGrow={1}
+      >
+        {conversationId && <ConversationView accountId={accountId} conversationId={conversationId} />}
+      </Stack>
+    </Stack>
   )
 }