Add conversation requests list

- Add routes to REST API for conversation requests
- Add websocket notification on new conversation requests. This is unreliable.
- Rename 'ColoredCallButton' as 'ColoredRoundButton' and move it to Buttons file for reuse
- Review logic to show conversation tabs
- Add useConversationDisplayNameShort for conversations' names in lists. Will need more work.
- Add hooks to help managing React Query's cache
- Use React Query to remove conversations and update the cache doing so.
- Add ContactService and ConversationService as a way to group reusable functions for the server. This is inspired by jami-android

Known bug: The server often freezes on getContactFromUri (in ContactService) when a new conversation request is received.

Change-Id: I46a60a401f09c3941c864afcdb2625b5fcfe054a
diff --git a/client/src/services/conversationQueries.ts b/client/src/services/conversationQueries.ts
index 538d0e0..e33a9cc 100644
--- a/client/src/services/conversationQueries.ts
+++ b/client/src/services/conversationQueries.ts
@@ -16,11 +16,19 @@
  * <https://www.gnu.org/licenses/>.
  */
 import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
-import { ConversationInfos, IConversationMember, IConversationSummary, Message } from 'jami-web-common';
+import { AxiosResponse } from 'axios';
+import {
+  ConversationInfos,
+  IConversationMember,
+  IConversationRequest,
+  IConversationSummary,
+  Message,
+} from 'jami-web-common';
 import { useCallback } from 'react';
 
 import { useAuthContext } from '../contexts/AuthProvider';
 import { ConversationMember } from '../models/conversation-member';
+import { useAddToCache, useRemoveFromCache } from '../utils/reactquery';
 
 export const useConversationInfosQuery = (conversationId?: string) => {
   const { axiosInstance } = useAuthContext();
@@ -44,6 +52,31 @@
   });
 };
 
+const checkConversationSummariesAreEqual = (
+  conversationSummary1: IConversationSummary,
+  conversationSummary2: IConversationSummary
+) => conversationSummary1.id === conversationSummary2.id;
+const checkIsConversationSummaryFn = (conversationSummary: IConversationSummary, conversationId: string) =>
+  conversationSummary.id === conversationId;
+
+export const useAddConversationSummaryToCache = () =>
+  useAddToCache(['conversations', 'summaries'], checkConversationSummariesAreEqual);
+export const useRemoveConversationSummaryFromCache = () =>
+  useRemoveFromCache(['conversations', 'summaries'], checkIsConversationSummaryFn);
+
+export const useRemoveConversationMutation = () => {
+  const { axiosInstance } = useAuthContext();
+  const removeConversationSummaryFromCache = useRemoveConversationSummaryFromCache();
+  return useMutation(
+    ({ conversationId }: { conversationId: string }) => axiosInstance.delete(`/conversations/${conversationId}`),
+    {
+      onSuccess: (_data, { conversationId }) => {
+        removeConversationSummaryFromCache(conversationId);
+      },
+    }
+  );
+};
+
 export const useRefreshConversationsSummaries = () => {
   const queryClient = useQueryClient();
   return useCallback(() => {
@@ -89,3 +122,74 @@
     }
   );
 };
+
+const CheckConversationRequestsAreEqual = (
+  conversationRequest1: IConversationRequest,
+  conversationRequest2: IConversationRequest
+) => conversationRequest1.conversationId === conversationRequest2.conversationId;
+const checkIsConversationRequestFn = (conversationRequest: IConversationRequest, conversationId: string) =>
+  conversationRequest.conversationId === conversationId;
+
+export const useAddConversationRequestToCache = () =>
+  useAddToCache(['conversationsRequests'], CheckConversationRequestsAreEqual);
+export const useRemoveConversationRequestFromCache = () =>
+  useRemoveFromCache(['conversationsRequests'], checkIsConversationRequestFn);
+
+export const useConversationRequestsQuery = () => {
+  const { axiosInstance } = useAuthContext();
+  return useQuery({
+    queryKey: ['conversationRequests'],
+    queryFn: async () => {
+      const { data } = await axiosInstance.get<IConversationRequest[]>('/conversation-requests/');
+      return data;
+    },
+  });
+};
+
+export const useAcceptConversationRequestMutation = () => {
+  const { axiosInstance } = useAuthContext();
+  const addConversationSummaryToCache = useAddConversationSummaryToCache();
+  const removeConversationRequestFromCache = useRemoveConversationRequestFromCache();
+  return useMutation(
+    async (variables: { conversationId: string }) => {
+      const { data } = await axiosInstance.post<undefined, AxiosResponse<IConversationSummary>>(
+        `/conversation-requests/${variables.conversationId}`
+      );
+      return data;
+    },
+    {
+      onSuccess: (data, { conversationId }) => {
+        addConversationSummaryToCache(data);
+        removeConversationRequestFromCache(conversationId);
+      },
+    }
+  );
+};
+
+export const useBlockConversationRequestMutation = () => {
+  const { axiosInstance } = useAuthContext();
+  const removeConversationRequestFromCache = useRemoveConversationRequestFromCache();
+  return useMutation(
+    ({ conversationId }: { conversationId: string }) =>
+      axiosInstance.post(`/conversation-requests/${conversationId}/block`),
+    {
+      onSuccess: (_data, { conversationId }) => {
+        removeConversationRequestFromCache(conversationId);
+      },
+    }
+  );
+};
+
+export const useDeclineConversationRequestMutation = () => {
+  const { axiosInstance } = useAuthContext();
+  const removeConversationRequestFromCache = useRemoveConversationRequestFromCache();
+  return useMutation(
+    ({ conversationId }: { conversationId: string }) =>
+      axiosInstance.delete(`/conversation-requests/${conversationId}`),
+    {
+      onSuccess: (_data, { conversationId }) => {
+        removeConversationRequestFromCache(conversationId);
+      },
+    }
+  );
+};