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/utils/reactquery.ts b/client/src/utils/reactquery.ts
new file mode 100644
index 0000000..6ae5f71
--- /dev/null
+++ b/client/src/utils/reactquery.ts
@@ -0,0 +1,67 @@
+/*
+ * 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 { QueryKey, useQueryClient } from '@tanstack/react-query';
+import { useCallback } from 'react';
+
+// Test whether two elements are equals or not
+// Return 'true' if they are equal, 'false' otherwise
+export type CheckElementsAreEqual<ElementType> = (element1: ElementType, element2: ElementType) => boolean;
+
+type AddToCacheFn<ElementType> = (element: ElementType) => void;
+
+export const useAddToCache = <ElementType>(
+  queryKey: QueryKey,
+  CheckElementsAreEqual: CheckElementsAreEqual<ElementType>
+): AddToCacheFn<ElementType> => {
+  const queryClient = useQueryClient();
+  return useCallback(
+    (newElement: ElementType) => {
+      // Make sure the element is not already in the cache
+      // This is expected to happen all the time on strict mode
+      const data = queryClient.getQueryData<ElementType[]>(queryKey);
+      if (data?.find((element) => CheckElementsAreEqual(element, newElement))) {
+        return;
+      }
+
+      // Add the element
+      queryClient.setQueryData<ElementType[]>(queryKey, (elements) => [...(elements || []), newElement]);
+    },
+    [CheckElementsAreEqual, queryClient, queryKey]
+  );
+};
+
+// Check whether the passed element is the one we are looking for or not
+type CheckIsElementFn<ElementType, IdentifierType> = (element: ElementType, identifier: IdentifierType) => boolean;
+
+type RemoveFromCacheFn<IdentifierType> = (element: IdentifierType) => void;
+
+export const useRemoveFromCache = <ElementType, IdentifierType>(
+  queryKey: QueryKey,
+  checkIsElementFn: CheckIsElementFn<ElementType, IdentifierType>
+): RemoveFromCacheFn<IdentifierType> => {
+  const queryClient = useQueryClient();
+  return useCallback(
+    (identifier: IdentifierType) => {
+      queryClient.setQueryData<ElementType[]>(queryKey, (elements) =>
+        elements?.filter((element) => !checkIsElementFn(element, identifier))
+      );
+    },
+    [checkIsElementFn, queryClient, queryKey]
+  );
+};