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/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 {