diff --git a/client/src/components/Message.tsx b/client/src/components/Message.tsx
index bfc442f..981d3d5 100644
--- a/client/src/components/Message.tsx
+++ b/client/src/components/Message.tsx
@@ -29,47 +29,107 @@
 } from '@mui/material';
 import { styled } from '@mui/material/styles';
 import dayjs, { Dayjs } from 'dayjs';
+import dayOfYear from 'dayjs/plugin/dayOfYear';
+import isBetween from 'dayjs/plugin/isBetween';
 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 { Account, Contact, Message } from 'jami-web-common';
+import { ReactElement, 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(dayOfYear);
+dayjs.extend(isBetween);
 dayjs.extend(isToday);
 dayjs.extend(isYesterday);
 
 type MessagePosition = 'start' | 'end';
 
-export const MessageCall = () => {
-  return <Stack alignItems="center">&quot;Appel&quot;</Stack>;
+const notificationMessageTypes = ['initial', 'member'] as const;
+type NotificationMessageType = typeof notificationMessageTypes[number];
+const checkIsNotificationMessageType = (type: Message['type'] | undefined): type is NotificationMessageType => {
+  return notificationMessageTypes.includes(type as NotificationMessageType);
 };
 
-export const MessageInitial = () => {
-  const { t } = useTranslation();
-  return <Stack alignItems="center">{t('message_swarm_created')}</Stack>;
+const invisibleMessageTypes = ['application/update-profile', 'merge', 'vote'] as const;
+type InvisibleMessageType = typeof invisibleMessageTypes[number];
+const checkIsInvisibleMessageType = (type: Message['type'] | undefined): type is InvisibleMessageType => {
+  return invisibleMessageTypes.includes(type as InvisibleMessageType);
 };
 
-interface MessageDataTransferProps {
-  position: MessagePosition;
+const userMessageTypes = ['text/plain', 'application/data-transfer+json', 'application/call-history+json'] as const;
+type UserMessageType = typeof userMessageTypes[number];
+const checkIsUserMessageType = (type: Message['type'] | undefined): type is UserMessageType => {
+  return userMessageTypes.includes(type as UserMessageType);
+};
+
+const checkShowsTime = (time: Dayjs, previousTime: Dayjs) => {
+  return !previousTime.isSame(time) && !time.isBetween(previousTime, previousTime?.add(1, 'minute'));
+};
+
+const findPreviousVisibleMessage = (messages: Message[], messageIndex: number) => {
+  for (let i = messageIndex + 1; i < messages.length; ++i) {
+    const message = messages[i];
+    if (!checkIsInvisibleMessageType(message?.type)) {
+      return message;
+    }
+  }
+};
+
+const findNextVisibleMessage = (messages: Message[], messageIndex: number) => {
+  for (let i = messageIndex - 1; i >= 0; --i) {
+    const message = messages[i];
+    if (!checkIsInvisibleMessageType(message?.type)) {
+      return message;
+    }
+  }
+};
+
+const avatarSize = '22px';
+const spacingBetweenAvatarAndBubble = '10px';
+const bubblePadding = '16px';
+
+interface MessageCallProps {
+  message: Message;
+  isAccountMessage: boolean;
   isFirstOfGroup: boolean;
   isLastOfGroup: boolean;
 }
 
-export const MessageDataTransfer = ({ position, isFirstOfGroup, isLastOfGroup }: MessageDataTransferProps) => {
+const MessageCall = ({ isAccountMessage, isFirstOfGroup, isLastOfGroup }: MessageCallProps) => {
+  const position = isAccountMessage ? 'end' : 'start';
+  const bubbleColor = isAccountMessage ? '#005699' : '#E5E5E5';
+  const textColor = isAccountMessage ? 'white' : 'black';
   return (
-    <MessageBubble
-      backgroundColor={'#E5E5E5'}
-      position={position}
-      isFirstOfGroup={isFirstOfGroup}
-      isLastOfGroup={isLastOfGroup}
-    >
+    <Bubble position={position} isFirstOfGroup={isFirstOfGroup} isLastOfGroup={isLastOfGroup} bubbleColor={bubbleColor}>
+      <Typography variant="body1" color={textColor} textAlign={position}>
+        &quot;Appel&quot;
+      </Typography>
+    </Bubble>
+  );
+};
+
+const MessageInitial = () => {
+  const { t } = useTranslation();
+  return <>{t('message_swarm_created')}</>;
+};
+
+interface MessageDataTransferProps {
+  message: Message;
+  isAccountMessage: boolean;
+  isFirstOfGroup: boolean;
+  isLastOfGroup: boolean;
+}
+
+const MessageDataTransfer = ({ isAccountMessage, isFirstOfGroup, isLastOfGroup }: MessageDataTransferProps) => {
+  const position = isAccountMessage ? 'end' : 'start';
+  return (
+    <Bubble bubbleColor="#E5E5E5" position={position} isFirstOfGroup={isFirstOfGroup} isLastOfGroup={isLastOfGroup}>
       &quot;data-transfer&quot;
-    </MessageBubble>
+    </Bubble>
   );
 };
 
@@ -77,60 +137,50 @@
   message: Message;
 }
 
-export const MessageMember = ({ message }: MessageMemberProps) => {
+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>
+    <Chip
+      sx={{
+        width: 'fit-content',
+      }}
+      label={t('message_user_joined', { user: message.author })}
+    />
   );
 };
 
-export const MessageMerge = () => {
-  return <Stack alignItems="center">&quot;merge&quot;</Stack>;
-};
-
 interface MessageTextProps {
   message: Message;
-  position: MessagePosition;
+  isAccountMessage: boolean;
   isFirstOfGroup: boolean;
   isLastOfGroup: boolean;
-  textColor: string;
-  bubbleColor: string;
 }
 
-export const MessageText = ({
-  message,
-  position,
-  isFirstOfGroup,
-  isLastOfGroup,
-  textColor,
-  bubbleColor,
-}: MessageTextProps) => {
+const MessageText = ({ message, isAccountMessage, isFirstOfGroup, isLastOfGroup }: MessageTextProps) => {
+  const position = isAccountMessage ? 'end' : 'start';
+  const bubbleColor = isAccountMessage ? '#005699' : '#E5E5E5';
+  const textColor = isAccountMessage ? 'white' : 'black';
   return (
-    <MessageBubble
-      backgroundColor={bubbleColor}
-      position={position}
-      isFirstOfGroup={isFirstOfGroup}
-      isLastOfGroup={isLastOfGroup}
-    >
-      <Typography variant="body1" color={textColor} textAlign={position}>
-        {message.body}
-      </Typography>
-    </MessageBubble>
+    <MessageTooltip position={position}>
+      <Bubble
+        bubbleColor={bubbleColor}
+        position={position}
+        isFirstOfGroup={isFirstOfGroup}
+        isLastOfGroup={isLastOfGroup}
+      >
+        <Typography variant="body1" color={textColor} textAlign={position}>
+          {message.body}
+        </Typography>
+      </Bubble>
+    </MessageTooltip>
   );
 };
 
-interface MessageDateProps {
+interface DateIndicatorProps {
   time: Dayjs;
 }
 
-export const MessageDate = ({ time }: MessageDateProps) => {
+const DateIndicator = ({ time }: DateIndicatorProps) => {
   let textDate;
 
   if (time.isToday()) {
@@ -173,18 +223,18 @@
   );
 };
 
-interface MessageTimeProps {
+interface TimeIndicatorProps {
   time: Dayjs;
   hasDateOnTop: boolean;
 }
 
-export const MessageTime = ({ time, hasDateOnTop }: MessageTimeProps) => {
+const TimeIndicator = ({ time, hasDateOnTop }: TimeIndicatorProps) => {
   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'}>
+    <Stack direction="row" justifyContent="center" marginTop={hasDateOnTop ? '20px' : '30px'}>
       <Typography variant="caption" color="#A7A7A7" fontWeight={700}>
         {textTime}
       </Typography>
@@ -192,71 +242,111 @@
   );
 };
 
-interface MessageBubblesGroupProps {
-  account: Account;
-  messages: Message[];
-  members: ConversationMember[];
+interface NotificationMessageRowProps {
+  message: Message;
 }
 
-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() || '';
+const NotificationMessageRow = ({ message }: NotificationMessageRowProps) => {
+  let messageComponent;
+  switch (message.type) {
+    case 'initial':
+      messageComponent = <MessageInitial />;
+      break;
+    case 'member':
+      messageComponent = <MessageMember message={message} />;
+      break;
+    default:
+      console.error(`${NotificationMessageRow.name} received unhandled message type: ${message.type}`);
+      return null;
   }
 
   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 paddingTop={'30px'} alignItems="center">
+      {messageComponent}
+    </Stack>
+  );
+};
+
+interface UserMessageRowProps {
+  message: Message;
+  isAccountMessage: boolean;
+  previousMessage: Message | undefined;
+  nextMessage: Message | undefined;
+  time: Dayjs;
+  showsTime: boolean;
+  author: Account | Contact;
+}
+
+const UserMessageRow = ({
+  message,
+  previousMessage,
+  nextMessage,
+  isAccountMessage,
+  time,
+  showsTime,
+  author,
+}: UserMessageRowProps) => {
+  const authorName = author.getDisplayName();
+  const position = isAccountMessage ? 'end' : 'start';
+
+  const previousIsUserMessageType = checkIsUserMessageType(previousMessage?.type);
+  const nextIsUserMessageType = checkIsUserMessageType(nextMessage?.type);
+  const nextTime = dayjs.unix(Number(nextMessage?.timestamp));
+  const nextShowsTime = checkShowsTime(nextTime, time);
+  const isFirstOfGroup = showsTime || !previousIsUserMessageType || previousMessage?.author !== message.author;
+  const isLastOfGroup = nextShowsTime || !nextIsUserMessageType || message.author !== nextMessage?.author;
+
+  const props = {
+    message,
+    isAccountMessage,
+    isFirstOfGroup,
+    isLastOfGroup,
+  };
+
+  let MessageComponent;
+  switch (message.type) {
+    case 'text/plain':
+      MessageComponent = MessageText;
+      break;
+    case 'application/data-transfer+json':
+      MessageComponent = MessageDataTransfer;
+      break;
+    case 'application/call-history+json':
+      MessageComponent = MessageCall;
+      break;
+    default:
+      console.error(`${UserMessageRow.name} received unhandled message type: ${message.type}`);
+      return null;
+  }
+
+  const participantNamePadding = isAccountMessage
+    ? bubblePadding
+    : parseInt(avatarSize) + parseInt(spacingBetweenAvatarAndBubble) + parseInt(bubblePadding) + 'px';
+
+  return (
+    <Stack alignItems={position}>
+      {isFirstOfGroup && (
+        <Box padding={`30px ${participantNamePadding} 0 ${participantNamePadding}`}>
+          <ParticipantName name={authorName} />
+        </Box>
       )}
-      <Stack // Container to align the bubbles to the same side of a row
+      <Stack
+        direction="row"
+        justifyContent={position}
+        alignItems="end"
+        spacing={spacingBetweenAvatarAndBubble}
+        paddingTop="6px"
         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>
+        <Box sx={{ width: avatarSize }}>
+          {!isAccountMessage && isLastOfGroup && (
+            <ConversationAvatar
+              displayName={authorName}
+              sx={{ width: avatarSize, height: avatarSize, fontSize: '15px' }}
+            />
+          )}
+        </Box>
+        <MessageComponent {...props} />
       </Stack>
     </Stack>
   );
@@ -365,7 +455,8 @@
         </Stack>
       }
     >
-      {children}
+      {/* div fixes 'Function components cannot be given refs' error */}
+      <div>{children}</div>
     </Tooltip>
   );
 })(({ position }) => {
@@ -381,15 +472,15 @@
   };
 });
 
-interface MessageBubbleProps {
+interface BubbleProps {
   position: MessagePosition;
   isFirstOfGroup: boolean;
   isLastOfGroup: boolean;
-  backgroundColor: string;
+  bubbleColor: string;
   children: ReactNode;
 }
 
-const MessageBubble = ({ position, isFirstOfGroup, isLastOfGroup, backgroundColor, children }: MessageBubbleProps) => {
+const Bubble = ({ position, isFirstOfGroup, isLastOfGroup, bubbleColor, children }: BubbleProps) => {
   const largeRadius = '20px';
   const smallRadius = '5px';
   const radius = useMemo(() => {
@@ -410,18 +501,16 @@
   }, [isFirstOfGroup, isLastOfGroup, position]);
 
   return (
-    <MessageTooltip position={position}>
-      <Box
-        sx={{
-          width: 'fit-content',
-          backgroundColor: backgroundColor,
-          padding: '16px',
-          ...radius,
-        }}
-      >
-        {children}
-      </Box>
-    </MessageTooltip>
+    <Box
+      sx={{
+        width: 'fit-content',
+        backgroundColor: bubbleColor,
+        padding: bubblePadding,
+        ...radius,
+      }}
+    >
+      {children}
+    </Box>
   );
 };
 
@@ -431,10 +520,55 @@
 
 const ParticipantName = ({ name }: ParticipantNameProps) => {
   return (
-    <Box marginBottom="6px" marginLeft="16px" marginRight="16px">
-      <Typography variant="caption" color="#A7A7A7" fontWeight={700}>
-        {name}
-      </Typography>
-    </Box>
+    <Typography variant="caption" color="#A7A7A7" fontWeight={700}>
+      {name}
+    </Typography>
+  );
+};
+
+interface MessageProps {
+  messageIndex: number;
+  messages: Message[];
+  isAccountMessage: boolean;
+  author: Account | Contact;
+}
+
+export const MessageRow = ({ messageIndex, messages, isAccountMessage, author }: MessageProps) => {
+  const message = messages[messageIndex];
+  const previousMessage = findPreviousVisibleMessage(messages, messageIndex);
+  const nextMessage = findNextVisibleMessage(messages, messageIndex);
+  const time = dayjs.unix(Number(message.timestamp));
+  const previousTime = dayjs.unix(Number(previousMessage?.timestamp));
+  const showDate =
+    message?.type === 'initial' || previousTime.year() !== time.year() || previousTime.dayOfYear() !== time.dayOfYear();
+  const showTime = checkShowsTime(time, previousTime);
+  let messageComponent;
+  if (checkIsUserMessageType(message.type)) {
+    messageComponent = (
+      <UserMessageRow
+        message={message}
+        previousMessage={previousMessage}
+        nextMessage={nextMessage}
+        time={time}
+        showsTime={showTime}
+        isAccountMessage={isAccountMessage}
+        author={author}
+      />
+    );
+  } else if (checkIsNotificationMessageType(message.type)) {
+    messageComponent = <NotificationMessageRow message={message} />;
+  } else if (checkIsInvisibleMessageType(message.type)) {
+    return null;
+  } else {
+    const _exhaustiveCheck: never = message.type;
+    return _exhaustiveCheck;
+  }
+
+  return (
+    <Stack>
+      {showDate && <DateIndicator time={time} />}
+      {showTime && <TimeIndicator time={time} hasDateOnTop={showDate} />}
+      {messageComponent}
+    </Stack>
   );
 };
