Divide Conversation into ConversationInfos, ConversationMember, and ConversationSummary

- ConversationSummary is used to display ConversationList.
- Having the three separated will help managing queries.
- Adding ConversationSummary required to solve some inconsistencies in ConversationList, which was mixing contacts and conversations. ContactSearchResultList has been added as a quick fix . It will need more work.
- Some tools to uniformize conversation names have been introduced. They will need more work.

Note the diplaying of ConversationList is left broken in this commit.

Change-Id: I29337906cc43781a9c4790735490a6ee2cc51cb0
diff --git a/client/src/components/CallChatDrawer.tsx b/client/src/components/CallChatDrawer.tsx
index b87f37d..d094554 100644
--- a/client/src/components/CallChatDrawer.tsx
+++ b/client/src/components/CallChatDrawer.tsx
@@ -44,10 +44,7 @@
 
 const CallChatDrawerHeader = () => {
   const { setIsChatShown } = useCallContext();
-  const { conversation } = useConversationContext();
-
-  // TODO: Improve this to support multiple members
-  const contact = conversation.getFirstMember().contact;
+  const { conversationDisplayName } = useConversationContext();
 
   return (
     <Stack direction="row" padding={2} spacing={2} alignItems="center">
@@ -58,7 +55,7 @@
       />
       <Stack direction="column">
         <Typography variant="h3" textOverflow="ellipsis">
-          {contact.getDisplayName()}
+          {conversationDisplayName}
         </Typography>
       </Stack>
     </Stack>
diff --git a/client/src/components/ContactSearchResultList.tsx b/client/src/components/ContactSearchResultList.tsx
new file mode 100644
index 0000000..cff4f6b
--- /dev/null
+++ b/client/src/components/ContactSearchResultList.tsx
@@ -0,0 +1,77 @@
+/*
+ * 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 { Dialog, DialogProps, List, ListItem, ListItemAvatar, ListItemText } from '@mui/material';
+
+import { Contact } from '../models/contact';
+import AddContactPage from '../pages/AddContactPage';
+import ConversationAvatar from './ConversationAvatar';
+import { useDialogHandler } from './Dialog';
+
+type ContactSearchResultListProps = {
+  contacts: Contact[];
+};
+
+export default ({ contacts }: ContactSearchResultListProps) => {
+  return (
+    <List>
+      {contacts?.map((contact) => (
+        <ContactSearchResultListItem key={contact.uri} contact={contact} />
+      ))}
+    </List>
+  );
+};
+
+type ContactSearchResultListItemProps = {
+  contact: Contact;
+};
+
+const ContactSearchResultListItem = ({ contact }: ContactSearchResultListItemProps) => {
+  const dialogHandler = useDialogHandler();
+
+  return (
+    <>
+      <AddContactDialog {...dialogHandler.props} contactId={contact.uri} />
+      <ListItem
+        button
+        alignItems="flex-start"
+        key={contact.uri}
+        onClick={() => {
+          dialogHandler.openDialog();
+        }}
+      >
+        <ListItemAvatar>
+          <ConversationAvatar />
+        </ListItemAvatar>
+        <ListItemText primary={contact.getDisplayName()} secondary={contact.uri} />
+      </ListItem>
+    </>
+  );
+};
+
+type AddContactDialogProps = DialogProps & {
+  contactId: string;
+};
+
+const AddContactDialog = ({ contactId, ...props }: AddContactDialogProps) => {
+  return (
+    <Dialog {...props}>
+      <AddContactPage contactId={contactId} />
+    </Dialog>
+  );
+};
diff --git a/client/src/components/ConversationList.tsx b/client/src/components/ConversationList.tsx
index 08553b7..3f7603f 100644
--- a/client/src/components/ConversationList.tsx
+++ b/client/src/components/ConversationList.tsx
@@ -19,23 +19,19 @@
 import List from '@mui/material/List';
 import ListSubheader from '@mui/material/ListSubheader';
 import Typography from '@mui/material/Typography';
-import { useContext, useEffect } from 'react';
+import { useContext } from 'react';
 
 import { MessengerContext } from '../contexts/MessengerProvider';
-import { Conversation } from '../models/conversation';
-import { useAppSelector } from '../redux/hooks';
+import ContactSearchResultList from './ContactSearchResultList';
 import ConversationListItem from './ConversationListItem';
+import LoadingPage from './Loading';
 
-type ConversationListProps = {
-  conversations: Conversation[];
-};
-export default function ConversationList({ conversations }: ConversationListProps) {
-  const { searchResult } = useContext(MessengerContext);
-  const { refresh } = useAppSelector((state) => state.userInfo);
+export default function ConversationList() {
+  const { searchResult, conversationsSummaries } = useContext(MessengerContext);
 
-  useEffect(() => {
-    console.log('refresh list');
-  }, [refresh]);
+  if (!conversationsSummaries) {
+    return <LoadingPage />;
+  }
 
   return (
     <div className="rooms-list">
@@ -43,14 +39,14 @@
         {searchResult && (
           <div>
             <ListSubheader>Public directory</ListSubheader>
-            <ConversationListItem conversation={searchResult} />
+            <ContactSearchResultList contacts={searchResult} />
             <ListSubheader>Conversations</ListSubheader>
           </div>
         )}
-        {conversations.map((conversation) => (
-          <ConversationListItem key={conversation.id} conversation={conversation} />
+        {conversationsSummaries.map((conversationSummary) => (
+          <ConversationListItem key={conversationSummary.id} conversationSummary={conversationSummary} />
         ))}
-        {conversations.length === 0 && (
+        {conversationsSummaries.length === 0 && (
           <div className="list-placeholder">
             <GroupIcon color="disabled" fontSize="large" />
             <Typography className="subtitle" variant="subtitle2">
diff --git a/client/src/components/ConversationListItem.tsx b/client/src/components/ConversationListItem.tsx
index e4be263..f71705f 100644
--- a/client/src/components/ConversationListItem.tsx
+++ b/client/src/components/ConversationListItem.tsx
@@ -16,7 +16,7 @@
  * <https://www.gnu.org/licenses/>.
  */
 import { Box, ListItem, ListItemAvatar, ListItemText } from '@mui/material';
-import { ContactDetails } from 'jami-web-common';
+import { IConversationSummary } from 'jami-web-common';
 import { QRCodeCanvas } from 'qrcode.react';
 import { useCallback, useContext, useMemo, useState } from 'react';
 import { useTranslation } from 'react-i18next';
@@ -25,62 +25,42 @@
 import { useAuthContext } from '../contexts/AuthProvider';
 import { CallManagerContext } from '../contexts/CallManagerProvider';
 import { CallStatus, useCallContext } from '../contexts/CallProvider';
-import { useConversationContext } from '../contexts/ConversationProvider';
-import { MessengerContext } from '../contexts/MessengerProvider';
-import { Conversation } from '../models/conversation';
+import { useUrlParams } from '../hooks/useUrlParams';
 import { setRefreshFromSlice } from '../redux/appSlice';
 import { useAppDispatch } from '../redux/hooks';
+import { ConversationRouteParams } from '../router';
 import ContextMenu, { ContextMenuHandler, useContextMenuHandler } from './ContextMenu';
 import ConversationAvatar from './ConversationAvatar';
 import { ConfirmationDialog, DialogContentList, InfosDialog, useDialogHandler } from './Dialog';
 import { PopoverListItemData } from './PopoverList';
-import {
-  AudioCallIcon,
-  BlockContactIcon,
-  CancelIcon,
-  ContactDetailsIcon,
-  MessageIcon,
-  RemoveContactIcon,
-  VideoCallIcon,
-} from './SvgIcon';
+import { AudioCallIcon, CancelIcon, MessageIcon, PersonIcon, VideoCallIcon } from './SvgIcon';
 
 type ConversationListItemProps = {
-  conversation: Conversation;
+  conversationSummary: IConversationSummary;
 };
 
-export default function ConversationListItem({ conversation }: ConversationListItemProps) {
-  const conversationContext = useConversationContext(true);
-  const conversationId = conversationContext?.conversationId;
+export default function ConversationListItem({ conversationSummary }: ConversationListItemProps) {
+  const {
+    urlParams: { conversationId: selectedConversationId },
+  } = useUrlParams<ConversationRouteParams>();
   const contextMenuHandler = useContextMenuHandler();
-  const { newContactId, setNewContactId } = useContext(MessengerContext);
   const callContext = useCallContext(true);
   const { callData } = useContext(CallManagerContext);
   const { t } = useTranslation();
-
-  const pathId = conversationId ?? newContactId;
-  const isSelected = conversation.getDisplayUri() === pathId;
-
   const navigate = useNavigate();
-  const userId = conversation?.getFirstMember()?.contact.uri;
+
+  const conversationId = conversationSummary.id;
+  const isSelected = conversationId === selectedConversationId;
 
   const onClick = useCallback(() => {
-    const newConversationId = conversation.id;
-    if (newConversationId) {
-      navigate(`/conversation/${newConversationId}`);
-    } else {
-      setNewContactId(userId);
+    if (conversationId) {
+      navigate(`/conversation/${conversationId}`);
     }
-  }, [navigate, conversation, userId, setNewContactId]);
+  }, [navigate, conversationId]);
 
-  const getSecondaryText = () => {
-    const propsConversationId = conversation.id;
-
-    if (!propsConversationId) {
-      return '';
-    }
-
-    if (!callContext || !callData || callData.conversationId !== propsConversationId) {
-      return conversation.getDisplayUri();
+  const secondaryText = useMemo(() => {
+    if (!callContext || !callData || callData.conversationId !== conversationSummary.id) {
+      return conversationSummary.lastMessage.body;
     }
 
     if (callContext.callStatus === CallStatus.InCall) {
@@ -92,67 +72,56 @@
     }
 
     return callContext.callRole === 'caller' ? t('outgoing_call') : t('incoming_call');
-  };
+  }, [conversationSummary, callContext, callData, t]);
+
+  const conversationName = useMemo(
+    () => conversationSummary.title ?? conversationSummary.membersNames.join(', '),
+    [conversationSummary]
+  );
 
   return (
     <Box onContextMenu={contextMenuHandler.handleAnchorPosition}>
       <ConversationMenu
-        userId={userId}
-        conversation={conversation}
+        conversationId={conversationId}
+        conversationName={conversationName}
         onMessageClick={onClick}
         isSelected={isSelected}
         contextMenuProps={contextMenuHandler.props}
       />
       <ListItem button alignItems="flex-start" selected={isSelected} onClick={onClick}>
         <ListItemAvatar>
-          <ConversationAvatar displayName={conversation.getDisplayNameNoFallback()} />
+          <ConversationAvatar displayName={conversationName} />
         </ListItemAvatar>
-        <ListItemText primary={conversation.getDisplayName()} secondary={getSecondaryText()} />
+        <ListItemText primary={conversationName} secondary={secondaryText} />
       </ListItem>
     </Box>
   );
 }
 
 interface ConversationMenuProps {
-  userId: string;
-  conversation: Conversation;
+  conversationId: string;
+  conversationName: string;
   onMessageClick: () => void;
   isSelected: boolean;
   contextMenuProps: ContextMenuHandler['props'];
 }
 
 const ConversationMenu = ({
-  userId,
-  conversation,
+  conversationId,
+  conversationName,
   onMessageClick,
   isSelected,
   contextMenuProps,
 }: ConversationMenuProps) => {
   const { t } = useTranslation();
-  const { axiosInstance } = useAuthContext();
   const { startCall } = useContext(CallManagerContext);
   const [isSwarm] = useState(true);
 
   const detailsDialogHandler = useDialogHandler();
-  const blockContactDialogHandler = useDialogHandler();
-  const removeContactDialogHandler = useDialogHandler();
+  const RemoveConversationDialogHandler = useDialogHandler();
 
   const navigate = useNavigate();
 
-  const getContactDetails = useCallback(async () => {
-    const controller = new AbortController();
-    try {
-      const { data } = await axiosInstance.get<ContactDetails>(`/contacts/${userId}`, {
-        signal: controller.signal,
-      });
-      console.log('CONTACT LIST - DETAILS: ', data);
-    } catch (e) {
-      console.log('ERROR GET CONTACT DETAILS: ', e);
-    }
-  }, [axiosInstance, userId]);
-
-  const conversationId = conversation.id;
-
   const menuOptions: PopoverListItemData[] = useMemo(
     () => [
       {
@@ -198,24 +167,16 @@
         : []),
       {
         label: t('conversation_details'),
-        Icon: ContactDetailsIcon,
+        Icon: PersonIcon,
         onClick: () => {
           detailsDialogHandler.openDialog();
-          getContactDetails();
-        },
-      },
-      {
-        label: t('conversation_block'),
-        Icon: BlockContactIcon,
-        onClick: () => {
-          blockContactDialogHandler.openDialog();
         },
       },
       {
         label: t('conversation_delete'),
-        Icon: RemoveContactIcon,
+        Icon: CancelIcon,
         onClick: () => {
-          removeContactDialogHandler.openDialog();
+          RemoveConversationDialogHandler.openDialog();
         },
       },
     ],
@@ -223,10 +184,8 @@
       navigate,
       onMessageClick,
       isSelected,
-      getContactDetails,
       detailsDialogHandler,
-      blockContactDialogHandler,
-      removeContactDialogHandler,
+      RemoveConversationDialogHandler,
       t,
       startCall,
       conversationId,
@@ -237,108 +196,67 @@
     <>
       <ContextMenu {...contextMenuProps} items={menuOptions} />
 
-      <DetailsDialog {...detailsDialogHandler.props} userId={userId} conversation={conversation} isSwarm={isSwarm} />
+      <DetailsDialog
+        {...detailsDialogHandler.props}
+        conversationId={conversationId}
+        conversationName={conversationName}
+        isSwarm={isSwarm}
+      />
 
-      <RemoveContactDialog {...removeContactDialogHandler.props} userId={userId} conversation={conversation} />
-
-      <BlockContactDialog {...blockContactDialogHandler.props} userId={userId} conversation={conversation} />
+      <RemoveConversationDialog {...RemoveConversationDialogHandler.props} conversationId={conversationId} />
     </>
   );
 };
 
 interface DetailsDialogProps {
-  userId: string;
-  conversation: Conversation;
+  conversationId: string;
+  conversationName: string;
   open: boolean;
   onClose: () => void;
   isSwarm: boolean;
 }
 
-const DetailsDialog = ({ userId, conversation, open, onClose, isSwarm }: DetailsDialogProps) => {
+const DetailsDialog = ({ conversationId, conversationName, open, onClose, isSwarm }: DetailsDialogProps) => {
   const { t } = useTranslation();
   const items = useMemo(
     () => [
       {
-        label: t('conversation_details_username'),
-        value: conversation.getDisplayNameNoFallback(),
+        label: t('conversation_details_name'),
+        value: conversationName,
       },
       {
         label: t('conversation_details_identifier'),
-        value: userId,
+        value: conversationId,
       },
       {
         label: t('conversation_details_qr_code'),
-        value: <QRCodeCanvas size={80} value={`${userId}`} />,
+        value: <QRCodeCanvas size={80} value={`${conversationId}`} />,
       },
       {
         label: t('conversation_details_is_swarm'),
         value: isSwarm ? t('conversation_details_is_swarm_true') : t('conversation_details_is_swarm_false'),
       },
     ],
-    [userId, conversation, isSwarm, t]
+    [conversationId, conversationName, isSwarm, t]
   );
   return (
     <InfosDialog
       open={open}
       onClose={onClose}
-      icon={
-        <ConversationAvatar
-          sx={{ width: 'inherit', height: 'inherit' }}
-          displayName={conversation.getDisplayNameNoFallback()}
-        />
-      }
-      title={conversation.getDisplayNameNoFallback() || ''}
+      icon={<ConversationAvatar sx={{ width: 'inherit', height: 'inherit' }} displayName={conversationName} />}
+      title={conversationName}
       content={<DialogContentList title={t('conversation_details_informations')} items={items} />}
     />
   );
 };
 
-interface BlockContactDialogProps {
-  userId: string;
-  conversation: Conversation;
+interface RemoveConversationDialogProps {
+  conversationId: string;
   open: boolean;
   onClose: () => void;
 }
 
-const BlockContactDialog = ({ userId, open, onClose }: BlockContactDialogProps) => {
-  const { axiosInstance } = useAuthContext();
-  const { t } = useTranslation();
-  const dispatch = useAppDispatch();
-
-  const block = async () => {
-    const controller = new AbortController();
-    try {
-      await axiosInstance.post(`/contacts/${userId}/block`, {
-        signal: controller.signal,
-      });
-      dispatch(setRefreshFromSlice());
-    } catch (e) {
-      console.error(`Error $block contact : `, e);
-      dispatch(setRefreshFromSlice());
-    }
-    onClose();
-  };
-
-  return (
-    <ConfirmationDialog
-      open={open}
-      onClose={onClose}
-      title={t('dialog_confirm_title_default')}
-      content={t('conversation_ask_confirm_block')}
-      onConfirm={block}
-      confirmButtonText={t('conversation_confirm_block')}
-    />
-  );
-};
-
-interface RemoveContactDialogProps {
-  userId: string;
-  conversation: Conversation;
-  open: boolean;
-  onClose: () => void;
-}
-
-const RemoveContactDialog = ({ userId, open, onClose }: RemoveContactDialogProps) => {
+const RemoveConversationDialog = ({ conversationId, open, onClose }: RemoveConversationDialogProps) => {
   const { axiosInstance } = useAuthContext();
   const { t } = useTranslation();
   const dispatch = useAppDispatch();
@@ -346,12 +264,12 @@
   const remove = async () => {
     const controller = new AbortController();
     try {
-      await axiosInstance.delete(`/contacts/${userId}`, {
+      await axiosInstance.delete(`/conversations/${conversationId}`, {
         signal: controller.signal,
       });
       dispatch(setRefreshFromSlice());
     } catch (e) {
-      console.error(`Error removing contact : `, e);
+      console.error(`Error removing conversation : `, e);
       dispatch(setRefreshFromSlice());
     }
     onClose();
diff --git a/client/src/components/ConversationView.tsx b/client/src/components/ConversationView.tsx
index 51c0899..9216249 100644
--- a/client/src/components/ConversationView.tsx
+++ b/client/src/components/ConversationView.tsx
@@ -16,18 +16,14 @@
  * <https://www.gnu.org/licenses/>.
  */
 import { Divider, Stack, Typography } from '@mui/material';
-import { useContext, useMemo } from 'react';
-import { useTranslation } from 'react-i18next';
+import { useContext } from 'react';
 
-import { useAuthContext } from '../contexts/AuthProvider';
 import { CallManagerContext } from '../contexts/CallManagerProvider';
 import { useCallContext } from '../contexts/CallProvider';
 import { useConversationContext } from '../contexts/ConversationProvider';
 import { useWebRtcContext } from '../contexts/WebRtcProvider';
-import { ConversationMember } from '../models/conversation';
 import CallInterface from '../pages/CallInterface';
 import ChatInterface from '../pages/ChatInterface';
-import { translateEnumeration, TranslateEnumerationOptions } from '../utils/translations';
 import { AddParticipantButton, ShowOptionsMenuButton, StartAudioCallButton, StartVideoCallButton } from './Button';
 
 const ConversationView = () => {
@@ -54,42 +50,14 @@
 };
 
 const ConversationHeader = () => {
-  const { account } = useAuthContext();
-  const { conversation, conversationId } = useConversationContext();
+  const { conversationId, conversationDisplayName } = useConversationContext();
   const { startCall } = useContext(CallManagerContext);
-  const { t } = useTranslation();
-
-  const members = conversation.members;
-  const adminTitle = conversation.infos.title as string;
-
-  const title = useMemo(() => {
-    if (adminTitle) {
-      return adminTitle;
-    }
-
-    const options: TranslateEnumerationOptions<ConversationMember> = {
-      elementPartialKey: 'member',
-      getElementValue: (member) => getMemberName(member),
-      translaters: [
-        () =>
-          // The user is chatting with themself
-          t('conversation_title_one', { member0: account?.getDisplayName() }),
-        (interpolations) => t('conversation_title_one', interpolations),
-        (interpolations) => t('conversation_title_two', interpolations),
-        (interpolations) => t('conversation_title_three', interpolations),
-        (interpolations) => t('conversation_title_four', interpolations),
-        (interpolations) => t('conversation_title_more', interpolations),
-      ],
-    };
-
-    return translateEnumeration<ConversationMember>(members, options);
-  }, [account, members, adminTitle, t]);
 
   return (
     <Stack direction="row" padding="16px" overflow="hidden">
       <Stack flex={1} justifyContent="center" whiteSpace="nowrap" overflow="hidden">
         <Typography variant="h3" textOverflow="ellipsis">
-          {title}
+          {conversationDisplayName}
         </Typography>
       </Stack>
       <Stack direction="row" spacing="20px">
@@ -102,9 +70,4 @@
   );
 };
 
-const getMemberName = (member: ConversationMember) => {
-  const contact = member.contact;
-  return contact.getDisplayName();
-};
-
 export default ConversationView;
diff --git a/client/src/components/ConversationsOverviewCard.tsx b/client/src/components/ConversationsOverviewCard.tsx
index 67b4b68..b9dafc9 100644
--- a/client/src/components/ConversationsOverviewCard.tsx
+++ b/client/src/components/ConversationsOverviewCard.tsx
@@ -16,32 +16,14 @@
  * <https://www.gnu.org/licenses/>.
  */
 import { Card, CardActionArea, CardContent, CircularProgress, Typography } from '@mui/material';
-import { IConversation } from 'jami-web-common';
-import { useEffect, useState } from 'react';
 import { useNavigate } from 'react-router';
 
-import { useAuthContext } from '../contexts/AuthProvider';
+import { useConversationsSummariesQuery } from '../services/conversationQueries';
 
 export default function ConversationsOverviewCard() {
-  const { axiosInstance, account } = useAuthContext();
   const navigate = useNavigate();
 
-  const [conversationCount, setConversationCount] = useState<number | undefined>();
-
-  const accountId = account.id;
-
-  useEffect(() => {
-    const controller = new AbortController();
-    axiosInstance
-      .get<IConversation[]>('/conversations', {
-        signal: controller.signal,
-      })
-      .then(({ data }) => {
-        console.log(data);
-        setConversationCount(data.length);
-      });
-    return () => controller.abort(); // crash on React18
-  }, [axiosInstance, accountId]);
+  const conversationSummariesQuery = useConversationsSummariesQuery();
 
   return (
     <Card onClick={() => navigate(`/`)}>
@@ -51,7 +33,7 @@
             Conversations
           </Typography>
           <Typography gutterBottom variant="h5" component="h2">
-            {conversationCount != null ? conversationCount : <CircularProgress size={24} />}
+            {conversationSummariesQuery?.data?.length ?? <CircularProgress size={24} />}
           </Typography>
         </CardContent>
       </CardActionArea>
diff --git a/client/src/components/MessageList.tsx b/client/src/components/MessageList.tsx
index 356d3e6..66915ab 100644
--- a/client/src/components/MessageList.tsx
+++ b/client/src/components/MessageList.tsx
@@ -23,17 +23,17 @@
 import { Waypoint } from 'react-waypoint';
 
 import { useAuthContext } from '../contexts/AuthProvider';
-import { ConversationMember } from '../models/conversation';
+import { useConversationContext } from '../contexts/ConversationProvider';
 import { MessageRow } from './Message';
 import { ArrowDownIcon } from './SvgIcon';
 
 interface MessageListProps {
-  members: ConversationMember[];
   messages: Message[];
 }
 
-export default function MessageList({ members, messages }: MessageListProps) {
+export default function MessageList({ messages }: MessageListProps) {
   const { account } = useAuthContext();
+  const { members } = useConversationContext();
   const [showScrollButton, setShowScrollButton] = useState(false);
   const listBottomRef = useRef<HTMLElement>();
 
diff --git a/client/src/components/SendMessageForm.tsx b/client/src/components/SendMessageForm.tsx
index 7bb7a7f..a94ab56 100644
--- a/client/src/components/SendMessageForm.tsx
+++ b/client/src/components/SendMessageForm.tsx
@@ -21,7 +21,8 @@
 import { useTranslation } from 'react-i18next';
 
 import { useAuthContext } from '../contexts/AuthProvider';
-import { ConversationMember } from '../models/conversation';
+import { useConversationContext } from '../contexts/ConversationProvider';
+import { ConversationMember } from '../models/conversation-member';
 import { translateEnumeration, TranslateEnumerationOptions } from '../utils/translations';
 import {
   RecordVideoMessageButton,
@@ -32,12 +33,12 @@
 } from './Button';
 
 type SendMessageFormProps = {
-  members: ConversationMember[];
   onSend: (message: string) => void;
   openFilePicker: () => void;
 };
 
-export default function SendMessageForm({ members, onSend, openFilePicker }: SendMessageFormProps) {
+export default function SendMessageForm({ onSend, openFilePicker }: SendMessageFormProps) {
+  const { members } = useConversationContext();
   const [currentMessage, setCurrentMessage] = useState('');
   const placeholder = usePlaceholder(members);
 
@@ -97,7 +98,7 @@
   return useMemo(() => {
     const options: TranslateEnumerationOptions<ConversationMember> = {
       elementPartialKey: 'member',
-      getElementValue: (member) => getMemberName(member),
+      getElementValue: (member) => member.getDisplayName(),
       translaters: [
         () =>
           // The user is chatting with themself
@@ -113,8 +114,3 @@
     return translateEnumeration<ConversationMember>(members, options);
   }, [account, members, t]);
 };
-
-const getMemberName = (member: ConversationMember) => {
-  const contact = member.contact;
-  return contact.getDisplayName();
-};
diff --git a/client/src/components/SvgIcon.tsx b/client/src/components/SvgIcon.tsx
index 1ef5f9d..bfce146 100644
--- a/client/src/components/SvgIcon.tsx
+++ b/client/src/components/SvgIcon.tsx
@@ -223,28 +223,6 @@
   );
 };
 
-export const ContactDetailsIcon = (props: SvgIconProps) => {
-  return (
-    <SvgIcon {...props} viewBox="0 0 14.647 16">
-      {/* <svg
-        xmlns="http://www.w3.org/2000/svg"
-        width="14.647"
-        height="16"
-        viewBox="0 0 14.647 16"
-      >
-        <defs>
-          <style>.a{"fill:#005699;"}</style>
-        </defs> */}
-      <path
-        className="a"
-        d="M11.258,9.562A3.774,3.774,0,0,0,13.965,5.9,3.79,3.79,0,0,0,10.144,2,3.871,3.871,0,0,0,8.95,9.562,7.806,7.806,0,0,0,2.9,17.443a.557.557,0,1,0,1.114,0c0-3.821,2.786-6.925,6.209-6.925s6.209,3.1,6.209,6.925a.557.557,0,0,0,1.114,0C17.388,13.463,14.681,10.119,11.258,9.562ZM7.278,5.9a2.866,2.866,0,1,1,5.731,0,2.787,2.787,0,0,1-2.866,2.786A2.838,2.838,0,0,1,7.278,5.9Z"
-        transform="translate(-2.9 -2)"
-      />
-      {/* </svg> */}
-    </SvgIcon>
-  );
-};
-
 export const CrossedEyeIcon = (props: SvgIconProps) => {
   return (
     <SvgIcon {...props} viewBox="0 0 15.931 12.145">
@@ -544,10 +522,8 @@
 
 export const PersonIcon = (props: SvgIconProps) => {
   return (
-    <SvgIcon {...props} viewBox="0 0 24 24">
-      <g stroke="#03B9E9" strokeWidth="1.75" fill="none" fillRule="evenodd" strokeLinejoin="round">
-        <path d="M17 6.5c0 2.48522308-2.0147769 4.5-4.5 4.5C10.01477692 11 8 8.98522308 8 6.5 8 4.0147769 10.01477692 2 12.5 2 14.9852231 2 17 4.0147769 17 6.5ZM3 22c0-5.5228267 4.02947764-10 9.00005436-10C16.9705224 12 21 16.4771733 21 22" />
-      </g>
+    <SvgIcon {...props} viewBox="8.8 2 3 16">
+      <path d="M11.258,9.562A3.774,3.774,0,0,0,13.965,5.9,3.79,3.79,0,0,0,10.144,2,3.871,3.871,0,0,0,8.95,9.562,7.806,7.806,0,0,0,2.9,17.443a.557.557,0,1,0,1.114,0c0-3.821,2.786-6.925,6.209-6.925s6.209,3.1,6.209,6.925a.557.557,0,0,0,1.114,0C17.388,13.463,14.681,10.119,11.258,9.562ZM7.278,5.9a2.866,2.866,0,1,1,5.731,0,2.787,2.787,0,0,1-2.866,2.786A2.838,2.838,0,0,1,7.278,5.9Z" />
     </SvgIcon>
   );
 };
@@ -580,31 +556,6 @@
   );
 };
 
-export const RemoveContactIcon = (props: SvgIconProps) => {
-  return (
-    <SvgIcon {...props} viewBox="0 0 16 16">
-      {/* <svg
-        xmlns="http://www.w3.org/2000/svg"
-        width="16"
-        height="16"
-        viewBox="0 0 16 16"
-      >
-        <defs>
-          <style>.a{"fill:#005699;"}</style>
-        </defs> */}
-      <g transform="translate(-2 -2)">
-        <g transform="translate(2 2)">
-          <path
-            className="a"
-            d="M8,0a8,8,0,1,0,8,8A8.024,8.024,0,0,0,8,0ZM8,1.04a6.5,6.5,0,0,1,4.48,1.68L2.72,12.48A6.9,6.9,0,0,1,1.68,5.12,7.081,7.081,0,0,1,8,1.04ZM8,14.96a7.274,7.274,0,0,1-4.56-1.68l9.84-9.76a6.9,6.9,0,0,1,1.04,7.36A7.032,7.032,0,0,1,8,14.96Z"
-          />
-        </g>
-      </g>
-      {/* </svg> */}
-    </SvgIcon>
-  );
-};
-
 export const RoundCloseIcon = (props: SvgIconProps) => {
   return (
     <SvgIcon {...props} viewBox="0 0 24 24">
diff --git a/client/src/contexts/CallManagerProvider.tsx b/client/src/contexts/CallManagerProvider.tsx
index c04010f..b99df69 100644
--- a/client/src/contexts/CallManagerProvider.tsx
+++ b/client/src/contexts/CallManagerProvider.tsx
@@ -15,7 +15,7 @@
  * License along with this program.  If not, see
  * <https://www.gnu.org/licenses/>.
  */
-import { CallBegin, WebSocketMessageType } from 'jami-web-common';
+import { CallBegin, ConversationInfos, WebSocketMessageType } from 'jami-web-common';
 import { createContext, useCallback, useContext, useEffect, useMemo, useState } from 'react';
 import { useTranslation } from 'react-i18next';
 import { useNavigate } from 'react-router-dom';
@@ -23,9 +23,9 @@
 import { AlertSnackbar } from '../components/AlertSnackbar';
 import { RemoteVideoOverlay } from '../components/VideoOverlay';
 import { useUrlParams } from '../hooks/useUrlParams';
-import { Conversation } from '../models/conversation';
+import { ConversationMember } from '../models/conversation-member';
 import { ConversationRouteParams } from '../router';
-import { useConversationQuery } from '../services/conversationQueries';
+import { useConversationInfosQuery, useMembersQuery } from '../services/conversationQueries';
 import { SetState, WithChildren } from '../utils/utils';
 import CallProvider, { CallRole } from './CallProvider';
 import WebRtcProvider from './WebRtcProvider';
@@ -39,16 +39,16 @@
 
 type ICallManagerContext = {
   callData: CallData | undefined;
-  callConversation: Conversation | undefined;
-
+  callConversationInfos: ConversationInfos | undefined;
+  callMembers: ConversationMember[] | undefined;
   startCall: SetState<CallData | undefined>;
   exitCall: () => void;
 };
 
 const defaultCallManagerContext: ICallManagerContext = {
   callData: undefined,
-  callConversation: undefined,
-
+  callConversationInfos: undefined,
+  callMembers: undefined,
   startCall: () => {},
   exitCall: () => {},
 };
@@ -60,7 +60,8 @@
   const [callData, setCallData] = useState<CallData>();
   const webSocket = useContext(WebSocketContext);
   const navigate = useNavigate();
-  const { conversation } = useConversationQuery(callData?.conversationId);
+  const { data: conversationInfos } = useConversationInfosQuery(callData?.conversationId);
+  const { data: members } = useMembersQuery(callData?.conversationId);
   const { urlParams } = useUrlParams<ConversationRouteParams>();
   const [missedCallConversationId, setMissedCallConversationId] = useState<string>();
   const { t } = useTranslation();
@@ -108,10 +109,11 @@
     () => ({
       startCall,
       callData,
-      callConversation: conversation,
+      callConversationInfos: conversationInfos,
+      callMembers: members,
       exitCall,
     }),
-    [startCall, callData, conversation, exitCall]
+    [startCall, callData, conversationInfos, members, exitCall]
   );
 
   return (
diff --git a/client/src/contexts/CallProvider.tsx b/client/src/contexts/CallProvider.tsx
index 7517133..9570ee5 100644
--- a/client/src/contexts/CallProvider.tsx
+++ b/client/src/contexts/CallProvider.tsx
@@ -19,7 +19,7 @@
 import { useCallback, useContext, useEffect, useMemo, useState } from 'react';
 
 import { createOptionalContext } from '../hooks/createOptionalContext';
-import { Conversation } from '../models/conversation';
+import { ConversationMember } from '../models/conversation-member';
 import { callTimeoutMs } from '../utils/constants';
 import { AsyncSetState, SetState, WithChildren } from '../utils/utils';
 import { CallData, CallManagerContext } from './CallManagerProvider';
@@ -75,19 +75,19 @@
 
 export default ({ children }: WithChildren) => {
   const webSocket = useContext(WebSocketContext);
-  const { callConversation, callData, exitCall } = useContext(CallManagerContext);
+  const { callMembers, callData, exitCall } = useContext(CallManagerContext);
   const webRtcContext = useWebRtcContext(true);
 
   const dependencies = useMemo(
     () => ({
       webSocket,
       webRtcContext,
-      callConversation,
+      callMembers,
       callData,
       exitCall,
       conversationId: callData?.conversationId,
     }),
-    [webSocket, webRtcContext, callConversation, callData, exitCall]
+    [webSocket, webRtcContext, callMembers, callData, exitCall]
   );
 
   return (
@@ -104,7 +104,7 @@
 
 const CallProvider = ({
   webRtcContext,
-  callConversation,
+  callMembers,
   callData,
   exitCall,
   conversationId,
@@ -112,7 +112,7 @@
 }: {
   webSocket: IWebSocketContext;
   webRtcContext: IWebRtcContext;
-  callConversation: Conversation;
+  callMembers: ConversationMember[];
   callData: CallData;
   exitCall: () => void;
   conversationId: string;
@@ -147,7 +147,7 @@
   // TODO: This logic will have to change to support multiple people in a call. Could we move this logic to the server?
   //       The client could make a single request with the conversationId, and the server would be tasked with sending
   //       all the individual requests to the members of the conversation.
-  const contactUri = useMemo(() => callConversation.getFirstMember().contact.uri, [callConversation]);
+  const contactUri = useMemo(() => callMembers[0].contact.uri, [callMembers]);
 
   useEffect(() => {
     if (callStatus !== CallStatus.InCall) {
diff --git a/client/src/contexts/ConversationProvider.tsx b/client/src/contexts/ConversationProvider.tsx
index 8488feb..b6d2f4a 100644
--- a/client/src/contexts/ConversationProvider.tsx
+++ b/client/src/contexts/ConversationProvider.tsx
@@ -15,22 +15,25 @@
  * License along with this program.  If not, see
  * <https://www.gnu.org/licenses/>.
  */
-import { ConversationView, WebSocketMessageType } from 'jami-web-common';
+import { ConversationInfos, ConversationView, WebSocketMessageType } from 'jami-web-common';
 import { useContext, useEffect, useMemo } from 'react';
 
 import LoadingPage from '../components/Loading';
 import { createOptionalContext } from '../hooks/createOptionalContext';
+import { useConversationDisplayName } from '../hooks/useConversationDisplayName';
 import { useUrlParams } from '../hooks/useUrlParams';
-import { Conversation } from '../models/conversation';
+import { ConversationMember } from '../models/conversation-member';
 import { ConversationRouteParams } from '../router';
-import { useConversationQuery } from '../services/conversationQueries';
+import { useConversationInfosQuery, useMembersQuery } from '../services/conversationQueries';
 import { WithChildren } from '../utils/utils';
 import { useAuthContext } from './AuthProvider';
 import { WebSocketContext } from './WebSocketProvider';
 
 interface IConversationContext {
   conversationId: string;
-  conversation: Conversation;
+  conversationDisplayName: string;
+  conversationInfos: ConversationInfos;
+  members: ConversationMember[];
 }
 
 const optionalConversationContext = createOptionalContext<IConversationContext>('ConversationContext');
@@ -41,13 +44,29 @@
   const {
     urlParams: { conversationId },
   } = useUrlParams<ConversationRouteParams>();
-  const { accountId } = useAuthContext();
+  const { accountId, account } = useAuthContext();
   const webSocket = useContext(WebSocketContext);
 
-  const { conversation, isLoading, isError } = useConversationQuery(conversationId!);
+  const conversationInfosQuery = useConversationInfosQuery(conversationId!);
+  const membersQuery = useMembersQuery(conversationId!);
+
+  const isError = useMemo(
+    () => conversationInfosQuery.isError || membersQuery.isError,
+    [conversationInfosQuery.isError, membersQuery.isError]
+  );
+
+  const isLoading = useMemo(
+    () => conversationInfosQuery.isLoading || membersQuery.isLoading,
+    [conversationInfosQuery.isLoading, membersQuery.isLoading]
+  );
+
+  const conversationInfos = conversationInfosQuery.data;
+  const members = membersQuery.data;
+
+  const conversationDisplayName = useConversationDisplayName(account, conversationId, conversationInfos, members);
 
   useEffect(() => {
-    if (!conversation || !conversationId || !webSocket) {
+    if (!conversationInfos || !conversationId || !webSocket) {
       return;
     }
 
@@ -56,18 +75,20 @@
     };
 
     webSocket.send(WebSocketMessageType.ConversationView, conversationView);
-  }, [accountId, conversation, conversationId, webSocket]);
+  }, [accountId, conversationInfos, conversationId, webSocket]);
 
   const value = useMemo(() => {
-    if (!conversation || !conversationId) {
+    if (!conversationId || !conversationDisplayName || !conversationInfos || !members) {
       return;
     }
 
     return {
       conversationId,
-      conversation,
+      conversationDisplayName,
+      conversationInfos,
+      members,
     };
-  }, [conversationId, conversation]);
+  }, [conversationId, conversationDisplayName, conversationInfos, members]);
 
   if (isLoading) {
     return <LoadingPage />;
diff --git a/client/src/contexts/MessengerProvider.tsx b/client/src/contexts/MessengerProvider.tsx
index c02a1b3..8a405bb 100644
--- a/client/src/contexts/MessengerProvider.tsx
+++ b/client/src/contexts/MessengerProvider.tsx
@@ -15,32 +15,25 @@
  * License along with this program.  If not, see
  * <https://www.gnu.org/licenses/>.
  */
-import { ConversationMessage, IConversation, LookupResult, WebSocketMessageType } from 'jami-web-common';
+import { ConversationMessage, IConversationSummary, LookupResult, WebSocketMessageType } from 'jami-web-common';
 import { createContext, ReactNode, useContext, useEffect, useMemo, useState } from 'react';
 
 import { Contact } from '../models/contact';
-import { Conversation } from '../models/conversation';
-import { setRefreshFromSlice } from '../redux/appSlice';
-import { useAppDispatch, useAppSelector } from '../redux/hooks';
+import { useConversationsSummariesQuery, useRefreshConversationsSummaries } from '../services/conversationQueries';
 import { SetState } from '../utils/utils';
 import { useAuthContext } from './AuthProvider';
 import { WebSocketContext } from './WebSocketProvider';
 
 export interface IMessengerContext {
-  conversations: Conversation[] | undefined;
+  conversationsSummaries: IConversationSummary[] | undefined;
 
   setSearchQuery: SetState<string | undefined>;
 
-  searchResult: Conversation | undefined;
-
-  newContactId: string | undefined;
-  setNewContactId: SetState<string | undefined>;
+  searchResult: Contact[] | undefined;
 }
 
 const defaultMessengerContext: IMessengerContext = {
-  conversations: undefined,
-  newContactId: undefined,
-  setNewContactId: () => {},
+  conversationsSummaries: undefined,
   setSearchQuery: () => {},
   searchResult: undefined,
 };
@@ -48,29 +41,15 @@
 export const MessengerContext = createContext<IMessengerContext>(defaultMessengerContext);
 
 export default ({ children }: { children: ReactNode }) => {
-  const { refresh } = useAppSelector((state) => state.userInfo);
-  const dispatch = useAppDispatch();
   const { accountId, axiosInstance } = useAuthContext();
   const webSocket = useContext(WebSocketContext);
 
-  const [conversations, setConversations] = useState<Conversation[] | undefined>(undefined);
   const [searchQuery, setSearchQuery] = useState<string>();
-  const [searchResult, setSearchResults] = useState<Conversation | undefined>(undefined);
-  const [newContactId, setNewContactId] = useState<string>();
+  const [searchResult, setSearchResults] = useState<Contact[] | undefined>(undefined);
 
-  useEffect(() => {
-    const controller = new AbortController();
-    axiosInstance
-      .get<IConversation[]>('/conversations', {
-        signal: controller.signal,
-      })
-      .then(({ data }) => {
-        setConversations(
-          Object.values(data).map((conversationInterface) => Conversation.fromInterface(conversationInterface))
-        );
-      });
-    // return () => controller.abort()
-  }, [axiosInstance, accountId, refresh]);
+  const conversationsSummariesQuery = useConversationsSummariesQuery();
+  const conversationsSummaries = conversationsSummariesQuery.data;
+  const refreshConversationsSummaries = useRefreshConversationsSummaries();
 
   useEffect(() => {
     if (!webSocket) {
@@ -78,7 +57,7 @@
     }
 
     const conversationMessageListener = (_data: ConversationMessage) => {
-      dispatch(setRefreshFromSlice());
+      refreshConversationsSummaries();
     };
 
     webSocket.bind(WebSocketMessageType.ConversationMessage, conversationMessageListener);
@@ -86,7 +65,7 @@
     return () => {
       webSocket.unbind(WebSocketMessageType.ConversationMessage, conversationMessageListener);
     };
-  }, [webSocket, dispatch]);
+  }, [refreshConversationsSummaries, webSocket]);
 
   useEffect(() => {
     if (!searchQuery) return;
@@ -97,7 +76,7 @@
       })
       .then(({ data }) => {
         const contact = new Contact(data.address, data.username);
-        setSearchResults(contact ? Conversation.fromSingleContact(contact) : undefined);
+        setSearchResults([contact]);
       })
       .catch(() => {
         setSearchResults(undefined);
@@ -105,15 +84,13 @@
     // return () => controller.abort() // crash on React18
   }, [accountId, searchQuery, axiosInstance]);
 
-  const value = useMemo(
+  const value = useMemo<IMessengerContext>(
     () => ({
-      conversations,
+      conversationsSummaries,
       setSearchQuery,
       searchResult,
-      newContactId,
-      setNewContactId,
     }),
-    [conversations, setSearchQuery, searchResult, newContactId, setNewContactId]
+    [conversationsSummaries, setSearchQuery, searchResult]
   );
 
   return <MessengerContext.Provider value={value}>{children}</MessengerContext.Provider>;
diff --git a/client/src/contexts/WebRtcProvider.tsx b/client/src/contexts/WebRtcProvider.tsx
index 55a3a1d..e35df0c 100644
--- a/client/src/contexts/WebRtcProvider.tsx
+++ b/client/src/contexts/WebRtcProvider.tsx
@@ -20,7 +20,7 @@
 import { useCallback, useContext, useEffect, useMemo, useState } from 'react';
 
 import { createOptionalContext } from '../hooks/createOptionalContext';
-import { Conversation } from '../models/conversation';
+import { ConversationMember } from '../models/conversation-member';
 import { WithChildren } from '../utils/utils';
 import { useAuthContext } from './AuthProvider';
 import { CallManagerContext } from './CallManagerProvider';
@@ -52,7 +52,7 @@
   const { account } = useAuthContext();
   const [webRtcConnection, setWebRtcConnection] = useState<RTCPeerConnection | undefined>();
   const webSocket = useContext(WebSocketContext);
-  const { callConversation, callData } = useContext(CallManagerContext);
+  const { callConversationInfos, callMembers, callData } = useContext(CallManagerContext);
 
   useEffect(() => {
     if (webRtcConnection && !callData) {
@@ -85,10 +85,11 @@
     () => ({
       webRtcConnection,
       webSocket,
-      conversation: callConversation,
+      conversationInfos: callConversationInfos,
+      members: callMembers,
       conversationId: callData?.conversationId,
     }),
-    [webRtcConnection, webSocket, callConversation, callData?.conversationId]
+    [webRtcConnection, webSocket, callConversationInfos, callMembers, callData?.conversationId]
   );
 
   return (
@@ -104,14 +105,14 @@
 };
 
 const useWebRtcContextValue = ({
-  conversation,
+  members,
   conversationId,
   webRtcConnection,
   webSocket,
 }: {
   webRtcConnection: RTCPeerConnection;
   webSocket: IWebSocketContext;
-  conversation: Conversation;
+  members: ConversationMember[];
   conversationId: string;
 }) => {
   const [localStream, setLocalStream] = useState<MediaStream>();
@@ -133,7 +134,7 @@
   const [iceCandidateQueue, setIceCandidateQueue] = useState<RTCIceCandidate[]>([]);
 
   // TODO: This logic will have to change to support multiple people in a call
-  const contactUri = useMemo(() => conversation.getFirstMember().contact.uri, [conversation]);
+  const contactUri = useMemo(() => members[0]?.contact.uri, [members]);
 
   const getMediaDevices = useCallback(async (): Promise<MediaDevicesInfo> => {
     try {
diff --git a/client/src/hooks/useConversationDisplayName.ts b/client/src/hooks/useConversationDisplayName.ts
new file mode 100644
index 0000000..c8aabae
--- /dev/null
+++ b/client/src/hooks/useConversationDisplayName.ts
@@ -0,0 +1,62 @@
+/*
+ * 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 { ConversationInfos } from 'jami-web-common';
+import { useMemo } from 'react';
+import { useTranslation } from 'react-i18next';
+
+import { Account } from '../models/account';
+import { ConversationMember } from '../models/conversation-member';
+import { translateEnumeration, TranslateEnumerationOptions } from '../utils/translations';
+
+export const useConversationDisplayName = (
+  account: Account,
+  conversationId: string | undefined,
+  conversationInfos: ConversationInfos | undefined,
+  members: ConversationMember[] | undefined
+) => {
+  const { t } = useTranslation();
+
+  const adminTitle = conversationInfos?.title as string;
+
+  return useMemo(() => {
+    if (adminTitle) {
+      return adminTitle;
+    }
+
+    if (!members) {
+      return conversationId;
+    }
+
+    const options: TranslateEnumerationOptions<ConversationMember> = {
+      elementPartialKey: 'member',
+      getElementValue: (member) => member.getDisplayName(),
+      translaters: [
+        () =>
+          // The user is chatting with themself
+          t('conversation_title_one', { member0: account?.getDisplayName() }),
+        (interpolations) => t('conversation_title_one', interpolations),
+        (interpolations) => t('conversation_title_two', interpolations),
+        (interpolations) => t('conversation_title_three', interpolations),
+        (interpolations) => t('conversation_title_four', interpolations),
+        (interpolations) => t('conversation_title_more', interpolations),
+      ],
+    };
+
+    return translateEnumeration<ConversationMember>(members, options);
+  }, [account, adminTitle, conversationId, members, t]);
+};
diff --git a/client/src/locale/en/translation.json b/client/src/locale/en/translation.json
index 21fff51..4ed3515 100644
--- a/client/src/locale/en/translation.json
+++ b/client/src/locale/en/translation.json
@@ -12,7 +12,7 @@
   "conversation_details": "Conversation details",
   "conversation_block": "Block conversation",
   "conversation_delete": "Remove conversation",
-  "conversation_details_username": "Username",
+  "conversation_details_name": "Title",
   "conversation_details_identifier": "Identifier",
   "conversation_details_qr_code": "QR code",
   "conversation_details_is_swarm": "Is swarm",
diff --git a/client/src/locale/fr/translation.json b/client/src/locale/fr/translation.json
index c397c0f..c11d93b 100644
--- a/client/src/locale/fr/translation.json
+++ b/client/src/locale/fr/translation.json
@@ -12,7 +12,7 @@
   "conversation_details": "Détails de la conversation",
   "conversation_block": "Bloquer la conversation",
   "conversation_delete": "Supprimer la conversation",
-  "conversation_details_username": "Nom d'utilisateur",
+  "conversation_details_name": "Titre",
   "conversation_details_identifier": "Identifiant",
   "conversation_details_qr_code": "Code QR",
   "conversation_details_is_swarm": "Est un swarm",
diff --git a/client/src/models/account.ts b/client/src/models/account.ts
index 60ac1d8..ae2299e 100644
--- a/client/src/models/account.ts
+++ b/client/src/models/account.ts
@@ -18,7 +18,6 @@
 import { AccountDetails, Devices, IAccount, VolatileDetails } from 'jami-web-common';
 
 import { Contact } from './contact';
-import { Conversation } from './conversation';
 
 export type AccountType = 'RING' | 'SIP';
 
@@ -30,8 +29,6 @@
   devices: Devices = {};
   contacts: Contact[] = [];
 
-  private _conversations: Record<string, Conversation> = {};
-
   constructor(id: string, details: AccountDetails, volatileDetails: VolatileDetails) {
     this.id = id;
     this.details = details;
@@ -79,19 +76,4 @@
   getDisplayNameNoFallback() {
     return this.details['Account.displayName'] ?? this.getRegisteredName();
   }
-
-  get conversations() {
-    return this._conversations;
-  }
-
-  addConversation(conversation: Conversation) {
-    if (conversation.id === undefined) {
-      throw new Error('Conversation ID cannot be undefined');
-    }
-    this._conversations[conversation.id] = conversation;
-  }
-
-  removeConversation(conversationId: string) {
-    delete this.conversations[conversationId];
-  }
 }
diff --git a/client/src/models/conversation-member.ts b/client/src/models/conversation-member.ts
new file mode 100644
index 0000000..b1b213d
--- /dev/null
+++ b/client/src/models/conversation-member.ts
@@ -0,0 +1,41 @@
+/*
+ * 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 { ConversationMemberRole, IConversationMember } from 'jami-web-common';
+
+import { Contact } from './contact';
+
+export class ConversationMember implements IConversationMember {
+  readonly role;
+  readonly contact;
+
+  constructor(role: ConversationMemberRole | undefined, contact: Contact) {
+    this.role = role;
+    this.contact = contact;
+  }
+
+  static fromInterface(conversationMemberIterface: IConversationMember) {
+    return new ConversationMember(
+      conversationMemberIterface.role,
+      Contact.fromInterface(conversationMemberIterface.contact)
+    );
+  }
+
+  getDisplayName = () => {
+    return this.contact.getDisplayName();
+  };
+}
diff --git a/client/src/models/conversation.ts b/client/src/models/conversation.ts
deleted file mode 100644
index 484f166..0000000
--- a/client/src/models/conversation.ts
+++ /dev/null
@@ -1,94 +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 { ConversationInfos, IConversation, IConversationMember, Message } from 'jami-web-common';
-
-import { Contact } from './contact';
-
-export interface ConversationMember extends IConversationMember {
-  contact: Contact;
-}
-
-export class Conversation implements IConversation {
-  readonly id: string;
-  members: ConversationMember[];
-  messages: Message[] = [];
-  infos: ConversationInfos = {};
-
-  constructor(id: string, members?: ConversationMember[]) {
-    this.id = id;
-    this.members = members ?? [];
-  }
-
-  static fromInterface(conversationInterface: IConversation) {
-    const conversation = new Conversation(
-      conversationInterface.id,
-      conversationInterface.members.map((member) => {
-        const contact = Contact.fromInterface(member.contact);
-        return { contact } as ConversationMember;
-      })
-    );
-
-    conversation.messages = conversationInterface.messages;
-    conversation.infos = conversationInterface.infos;
-
-    return conversation;
-  }
-
-  static fromSingleContact(contact: Contact) {
-    return new Conversation('', [{ contact } as ConversationMember]);
-  }
-
-  getDisplayUri() {
-    return this.id ?? this.getFirstMember().contact.uri;
-  }
-
-  getDisplayName() {
-    if (this.members.length !== 0) {
-      return this.getFirstMember().contact.registeredName;
-    }
-    return this.getDisplayUri();
-  }
-
-  getDisplayNameNoFallback() {
-    if (this.members.length !== 0) {
-      return this.getFirstMember().contact.registeredName;
-    }
-  }
-
-  getFirstMember() {
-    return this.members[0];
-  }
-
-  addMessage(message: Message) {
-    if (this.messages.length === 0) {
-      this.messages.push(message);
-    } else if (message.id === this.messages[this.messages.length - 1].linearizedParent) {
-      this.messages.push(message);
-    } else if (message.linearizedParent === this.messages[0].id) {
-      this.messages.unshift(message);
-    } else {
-      console.log('Could not insert message', message.id);
-    }
-  }
-
-  addMessages(messages: Message[]) {
-    for (const message of messages) {
-      this.addMessage(message);
-    }
-  }
-}
diff --git a/client/src/pages/CallInterface.tsx b/client/src/pages/CallInterface.tsx
index 52b970f..b535b25 100644
--- a/client/src/pages/CallInterface.tsx
+++ b/client/src/pages/CallInterface.tsx
@@ -170,9 +170,8 @@
 
 const CallInterfaceInformation = () => {
   const { callStartTime } = useCallContext();
-  const { conversation } = useConversationContext();
+  const { conversationDisplayName } = useConversationContext();
   const [elapsedTime, setElapsedTime] = useState(callStartTime ? (Date.now() - callStartTime) / 1000 : 0);
-  const memberName = useMemo(() => conversation.getFirstMember().contact.registeredName, [conversation]);
 
   useEffect(() => {
     if (callStartTime) {
@@ -188,7 +187,7 @@
   return (
     <Stack direction="row" justifyContent="space-between" alignItems="center">
       <Typography color="white" component="p">
-        {memberName}
+        {conversationDisplayName}
       </Typography>
       <Typography color="white" component="p">
         {elapsedTimerString}
diff --git a/client/src/pages/CallPending.tsx b/client/src/pages/CallPending.tsx
index 2ef27f2..a7566db 100644
--- a/client/src/pages/CallPending.tsx
+++ b/client/src/pages/CallPending.tsx
@@ -35,7 +35,7 @@
 
 export const CallPending = () => {
   const { localStream } = useWebRtcContext();
-  const { conversation } = useConversationContext();
+  const { conversationDisplayName } = useConversationContext();
   const { callRole } = useCallContext();
   const localVideoRef = useRef<VideoElementWithSinkId | null>(null);
 
@@ -99,7 +99,7 @@
           />
           <ConversationAvatar
             alt="contact profile picture"
-            displayName={conversation.getDisplayNameNoFallback()}
+            displayName={conversationDisplayName}
             style={{
               width: '100%',
               height: '100%',
@@ -148,8 +148,8 @@
 export const CallPendingCallerInterface = () => {
   const { callStatus } = useCallContext();
   const { t } = useTranslation();
-  const { conversation } = useConversationContext();
-  const memberName = useMemo(() => conversation.getFirstMember().contact.registeredName, [conversation]);
+  const { members } = useConversationContext();
+  const memberName = useMemo(() => members[0].getDisplayName(), [members]);
 
   let title = t('loading');
 
@@ -182,8 +182,8 @@
   const { callStatus } = useCallContext();
 
   const { t } = useTranslation();
-  const { conversation } = useConversationContext();
-  const memberName = useMemo(() => conversation.getFirstMember().contact.registeredName, [conversation]);
+  const { members } = useConversationContext();
+  const memberName = useMemo(() => members[0].getDisplayName(), [members]);
 
   let title = t('loading');
 
diff --git a/client/src/pages/ChatInterface.tsx b/client/src/pages/ChatInterface.tsx
index 00aeb3e..0db7171 100644
--- a/client/src/pages/ChatInterface.tsx
+++ b/client/src/pages/ChatInterface.tsx
@@ -31,7 +31,7 @@
 
 const ChatInterface = () => {
   const webSocket = useContext(WebSocketContext);
-  const { conversationId, conversation } = useConversationContext();
+  const { conversationId } = useConversationContext();
   const [messages, setMessages] = useState<Message[]>([]);
   const [isLoading, setIsLoading] = useState(true);
   const [error, setError] = useState(false);
@@ -105,8 +105,6 @@
     return <div>Error loading {conversationId}</div>;
   }
 
-  const members = conversation.members;
-
   return (
     <Stack flex={1} overflow="hidden" {...getRootProps()} paddingBottom="16px">
       {isDragActive && (
@@ -123,14 +121,14 @@
         />
       )}
       <input {...getInputProps()} />
-      <MessageList members={members} messages={messages} />
+      <MessageList messages={messages} />
       <Divider
         sx={{
           margin: '30px 16px 0px 16px',
           borderTop: '1px solid #E5E5E5',
         }}
       />
-      <SendMessageForm members={members} onSend={sendMessage} openFilePicker={openFilePicker} />
+      <SendMessageForm onSend={sendMessage} openFilePicker={openFilePicker} />
       {fileHandlers.length > 0 && <FilePreviewsList fileHandlers={fileHandlers} removeFile={removeFile} />}
     </Stack>
   );
diff --git a/client/src/pages/Messenger.tsx b/client/src/pages/Messenger.tsx
index 294e069..f498625 100644
--- a/client/src/pages/Messenger.tsx
+++ b/client/src/pages/Messenger.tsx
@@ -16,32 +16,19 @@
  * <https://www.gnu.org/licenses/>.
  */
 import { Box, Stack } from '@mui/material';
-import { ReactNode, useContext } from 'react';
+import { ReactNode } from 'react';
 
-//import Sound from 'react-sound';
 import ConversationList from '../components/ConversationList';
 import Header from '../components/Header';
-import LoadingPage from '../components/Loading';
 import NewContactForm from '../components/NewContactForm';
-import { MessengerContext } from '../contexts/MessengerProvider';
-import AddContactPage from './AddContactPage';
 
 const Messenger = ({ children }: { children?: ReactNode }) => {
-  const { newContactId, conversations } = useContext(MessengerContext);
-
   return (
     <Box display="flex" height="100%">
       <Stack flexGrow={0} flexShrink={0} overflow="auto">
         <Header />
         <NewContactForm />
-        {newContactId && <AddContactPage contactId={newContactId} />}
-        {conversations ? (
-          <ConversationList conversations={conversations} />
-        ) : (
-          <div className="rooms-list">
-            <LoadingPage />
-          </div>
-        )}
+        <ConversationList />
       </Stack>
       <Box flexGrow={1} display="flex" position="relative">
         {children}
diff --git a/client/src/services/conversationQueries.ts b/client/src/services/conversationQueries.ts
index 94946ad..538d0e0 100644
--- a/client/src/services/conversationQueries.ts
+++ b/client/src/services/conversationQueries.ts
@@ -16,41 +16,59 @@
  * <https://www.gnu.org/licenses/>.
  */
 import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
-import { IConversation, Message } from 'jami-web-common';
-import { useMemo } from 'react';
+import { ConversationInfos, IConversationMember, IConversationSummary, Message } from 'jami-web-common';
+import { useCallback } from 'react';
 
 import { useAuthContext } from '../contexts/AuthProvider';
-import { Conversation } from '../models/conversation';
+import { ConversationMember } from '../models/conversation-member';
 
-export const useConversationQuery = (conversationId?: string) => {
+export const useConversationInfosQuery = (conversationId?: string) => {
   const { axiosInstance } = useAuthContext();
-  const conversationQuery = useQuery(
-    ['conversation', conversationId],
+  return useQuery(
+    ['conversations', conversationId],
     async () => {
-      const { data } = await axiosInstance.get<IConversation>(`/conversations/${conversationId}`);
+      const { data } = await axiosInstance.get<ConversationInfos>(`/conversations/${conversationId}/infos`);
       return data;
     },
     {
       enabled: !!conversationId,
     }
   );
+};
 
-  const conversation = useMemo(() => {
-    if (conversationQuery.isSuccess) {
-      return Conversation.fromInterface(conversationQuery.data);
+export const useConversationsSummariesQuery = () => {
+  const { axiosInstance } = useAuthContext();
+  return useQuery(['conversations', 'summaries'], async () => {
+    const { data } = await axiosInstance.get<IConversationSummary[]>(`/conversations`);
+    return data;
+  });
+};
+
+export const useRefreshConversationsSummaries = () => {
+  const queryClient = useQueryClient();
+  return useCallback(() => {
+    queryClient.invalidateQueries(['conversations', 'summaries']);
+  }, [queryClient]);
+};
+
+export const useMembersQuery = (conversationId?: string) => {
+  const { axiosInstance } = useAuthContext();
+  return useQuery(
+    ['conversations', conversationId, 'members'],
+    async () => {
+      const { data } = await axiosInstance.get<IConversationMember[]>(`/conversations/${conversationId}/members`);
+      return data.map((item) => ConversationMember.fromInterface(item));
+    },
+    {
+      enabled: !!conversationId,
     }
-  }, [conversationQuery.isSuccess, conversationQuery.data]);
-
-  return {
-    conversation,
-    ...conversationQuery,
-  };
+  );
 };
 
 export const useMessagesQuery = (conversationId: string) => {
   const { axiosInstance } = useAuthContext();
   return useQuery(
-    ['messages', conversationId],
+    ['conversations', conversationId, 'messages'],
     async () => {
       const { data } = await axiosInstance.get<Message[]>(`/conversations/${conversationId}/messages`);
       return data;