Separate common interfaces from client-specific classes

Changes:
- Move client-specific classes to client/src/models
- Extract common interface to standalone files in common/interfaces
- Remove unused features from client-specific classes
    - These are features which were once used on the old server, but are now no longer needed
    - Remove getObject() method for Account, Contact, and Conversation
    - Remove lookups, registrationState and registeringName from Account
    - Remove resolving logic from Contact
    - Remove requests and listeners from Conversation (once used for Socket.IO and promises on server)
- Rename services/Conversation.ts to services/conversationQueries.ts
- Update imports

Future work:
- Create interface versions of Account, Contact, and Conversation
- Create new interfaces to replace Records on server

GitLab: #94
Change-Id: Ia51fe6ebeda44a30887d851a5564569dc290e5ed
diff --git a/client/src/components/AccountPreferences.tsx b/client/src/components/AccountPreferences.tsx
index 2086f14..3a63ee7 100644
--- a/client/src/components/AccountPreferences.tsx
+++ b/client/src/components/AccountPreferences.tsx
@@ -35,10 +35,11 @@
   Typography,
 } from '@mui/material';
 import { motion } from 'framer-motion';
-import { Account, AccountDetails } from 'jami-web-common';
+import { AccountDetails } from 'jami-web-common';
 import { useState } from 'react';
 
 import { useAuthContext } from '../contexts/AuthProvider';
+import { Account } from '../models/Account';
 import ConversationAvatar from './ConversationAvatar';
 import ConversationsOverviewCard from './ConversationsOverviewCard';
 import JamiIdCard from './JamiIdCard';
diff --git a/client/src/components/ConversationList.tsx b/client/src/components/ConversationList.tsx
index 684c7e5..6b03224 100644
--- a/client/src/components/ConversationList.tsx
+++ b/client/src/components/ConversationList.tsx
@@ -19,10 +19,10 @@
 import List from '@mui/material/List';
 import ListSubheader from '@mui/material/ListSubheader';
 import Typography from '@mui/material/Typography';
-import { Conversation } from 'jami-web-common';
 import { useContext, useEffect } from 'react';
 
 import { MessengerContext } from '../contexts/MessengerProvider';
+import { Conversation } from '../models/Conversation';
 import { useAppSelector } from '../redux/hooks';
 import ConversationListItem from './ConversationListItem';
 
diff --git a/client/src/components/ConversationListItem.tsx b/client/src/components/ConversationListItem.tsx
index 03685c3..ebf0c93 100644
--- a/client/src/components/ConversationListItem.tsx
+++ b/client/src/components/ConversationListItem.tsx
@@ -16,7 +16,6 @@
  * <https://www.gnu.org/licenses/>.
  */
 import { Box, ListItem, ListItemAvatar, ListItemText } from '@mui/material';
-import { Conversation } from 'jami-web-common';
 import { QRCodeCanvas } from 'qrcode.react';
 import { useCallback, useContext, useMemo, useState } from 'react';
 import { useTranslation } from 'react-i18next';
@@ -26,6 +25,7 @@
 import { MessengerContext } from '../contexts/MessengerProvider';
 import { useStartCall } from '../hooks/useStartCall';
 import { useUrlParams } from '../hooks/useUrlParams';
+import { Conversation } from '../models/Conversation';
 import { setRefreshFromSlice } from '../redux/appSlice';
 import { useAppDispatch } from '../redux/hooks';
 import { ConversationRouteParams } from '../router';
diff --git a/client/src/components/ConversationView.tsx b/client/src/components/ConversationView.tsx
index e412cca..0319574 100644
--- a/client/src/components/ConversationView.tsx
+++ b/client/src/components/ConversationView.tsx
@@ -16,13 +16,13 @@
  * <https://www.gnu.org/licenses/>.
  */
 import { Divider, Stack, Typography } from '@mui/material';
-import { ConversationMember } from 'jami-web-common';
 import { useContext, useMemo } from 'react';
 import { useTranslation } from 'react-i18next';
 
 import { useAuthContext } from '../contexts/AuthProvider';
 import { ConversationContext } from '../contexts/ConversationProvider';
 import { useStartCall } from '../hooks/useStartCall';
+import { ConversationMember } from '../models/Conversation';
 import ChatInterface from '../pages/ChatInterface';
 import { translateEnumeration, TranslateEnumerationOptions } from '../utils/translations';
 import { AddParticipantButton, ShowOptionsMenuButton, StartAudioCallButton, StartVideoCallButton } from './Button';
diff --git a/client/src/components/ConversationsOverviewCard.tsx b/client/src/components/ConversationsOverviewCard.tsx
index 84d022d..cdc1c79 100644
--- a/client/src/components/ConversationsOverviewCard.tsx
+++ b/client/src/components/ConversationsOverviewCard.tsx
@@ -16,11 +16,11 @@
  * <https://www.gnu.org/licenses/>.
  */
 import { Card, CardActionArea, CardContent, CircularProgress, Typography } from '@mui/material';
-import { Conversation } from 'jami-web-common/dist/Conversation';
 import { useEffect, useState } from 'react';
 import { useNavigate } from 'react-router';
 
 import { useAuthContext } from '../contexts/AuthProvider';
+import { Conversation } from '../models/Conversation';
 
 export default function ConversationsOverviewCard() {
   const { axiosInstance, account } = useAuthContext();
diff --git a/client/src/components/JamiIdCard.tsx b/client/src/components/JamiIdCard.tsx
index 6a3be3c..73c038c 100644
--- a/client/src/components/JamiIdCard.tsx
+++ b/client/src/components/JamiIdCard.tsx
@@ -16,7 +16,8 @@
  * <https://www.gnu.org/licenses/>.
  */
 import { Box, Card, CardContent, Typography } from '@mui/material';
-import { Account } from 'jami-web-common';
+
+import { Account } from '../models/Account';
 
 type JamiIdCardProps = {
   account: Account;
diff --git a/client/src/components/Message.tsx b/client/src/components/Message.tsx
index 7cf7225..669f041 100644
--- a/client/src/components/Message.tsx
+++ b/client/src/components/Message.tsx
@@ -18,11 +18,13 @@
 import { Box, Chip, Divider, Stack, Tooltip, Typography } from '@mui/material';
 import { styled } from '@mui/material/styles';
 import { Dayjs } from 'dayjs';
-import { Account, Contact, Message } from 'jami-web-common';
+import { Message } from 'jami-web-common';
 import { ReactElement, ReactNode, useCallback, useMemo, useState } from 'react';
 import { useTranslation } from 'react-i18next';
 
 import dayjs from '../dayjsInitializer';
+import { Account } from '../models/Account';
+import { Contact } from '../models/Contact';
 import { EmojiButton, MoreButton, ReplyMessageButton } from './Button';
 import ConversationAvatar from './ConversationAvatar';
 import PopoverList, { PopoverListItemData } from './PopoverList';
diff --git a/client/src/components/MessageList.tsx b/client/src/components/MessageList.tsx
index 0ae04d2..538acc7 100644
--- a/client/src/components/MessageList.tsx
+++ b/client/src/components/MessageList.tsx
@@ -17,12 +17,13 @@
  */
 import { Typography } from '@mui/material';
 import { Box, Stack } from '@mui/system';
-import { ConversationMember, Message } from 'jami-web-common';
+import { Message } from 'jami-web-common';
 import { MutableRefObject, useRef, useState } from 'react';
 import { useTranslation } from 'react-i18next';
 import { Waypoint } from 'react-waypoint';
 
 import { useAuthContext } from '../contexts/AuthProvider';
+import { ConversationMember } from '../models/Conversation';
 import { MessageRow } from './Message';
 import { ArrowDownIcon } from './SvgIcon';
 
diff --git a/client/src/components/SendMessageForm.tsx b/client/src/components/SendMessageForm.tsx
index dfb5f3d..d1267aa 100644
--- a/client/src/components/SendMessageForm.tsx
+++ b/client/src/components/SendMessageForm.tsx
@@ -17,11 +17,11 @@
  */
 import { InputBase } from '@mui/material';
 import { Stack } from '@mui/system';
-import { ConversationMember } from 'jami-web-common';
 import { ChangeEvent, FormEvent, useCallback, useMemo, useState } from 'react';
 import { useTranslation } from 'react-i18next';
 
 import { useAuthContext } from '../contexts/AuthProvider';
+import { ConversationMember } from '../models/Conversation';
 import { translateEnumeration, TranslateEnumerationOptions } from '../utils/translations';
 import {
   RecordVideoMessageButton,
diff --git a/client/src/contexts/AuthProvider.tsx b/client/src/contexts/AuthProvider.tsx
index ced9a85..06bf1bb 100644
--- a/client/src/contexts/AuthProvider.tsx
+++ b/client/src/contexts/AuthProvider.tsx
@@ -16,12 +16,12 @@
  * <https://www.gnu.org/licenses/>.
  */
 import axios, { AxiosInstance } from 'axios';
-import { Account } from 'jami-web-common/dist/Account';
-import { HttpStatusCode } from 'jami-web-common/dist/enums/http-status-code';
+import { HttpStatusCode } from 'jami-web-common';
 import { createContext, useCallback, useContext, useEffect, useMemo, useState } from 'react';
 import { useNavigate } from 'react-router-dom';
 
 import ProcessingRequest from '../components/ProcessingRequest';
+import { Account } from '../models/Account';
 import { apiUrl } from '../utils/constants';
 import { WithChildren } from '../utils/utils';
 
diff --git a/client/src/contexts/ConversationProvider.tsx b/client/src/contexts/ConversationProvider.tsx
index 7d09881..25137c0 100644
--- a/client/src/contexts/ConversationProvider.tsx
+++ b/client/src/contexts/ConversationProvider.tsx
@@ -15,13 +15,14 @@
  * License along with this program.  If not, see
  * <https://www.gnu.org/licenses/>.
  */
-import { Conversation, ConversationView, WebSocketMessageType } from 'jami-web-common';
+import { ConversationView, WebSocketMessageType } from 'jami-web-common';
 import { createContext, useContext, useEffect, useState } from 'react';
 
 import LoadingPage from '../components/Loading';
 import { useUrlParams } from '../hooks/useUrlParams';
+import { Conversation } from '../models/Conversation';
 import { ConversationRouteParams } from '../router';
-import { useConversationQuery } from '../services/Conversation';
+import { useConversationQuery } from '../services/conversationQueries';
 import { WithChildren } from '../utils/utils';
 import { useAuthContext } from './AuthProvider';
 import { WebSocketContext } from './WebSocketProvider';
diff --git a/client/src/contexts/MessengerProvider.tsx b/client/src/contexts/MessengerProvider.tsx
index f92a6d7..3ec6164 100644
--- a/client/src/contexts/MessengerProvider.tsx
+++ b/client/src/contexts/MessengerProvider.tsx
@@ -15,9 +15,11 @@
  * License along with this program.  If not, see
  * <https://www.gnu.org/licenses/>.
  */
-import { Contact, Conversation, ConversationMessage, WebSocketMessageType } from 'jami-web-common';
-import React, { createContext, ReactNode, useContext, useEffect, useState } from 'react';
+import { ConversationMessage, WebSocketMessageType } from 'jami-web-common';
+import { createContext, ReactNode, useContext, useEffect, 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 { SetState } from '../utils/utils';
diff --git a/client/src/models/Account.ts b/client/src/models/Account.ts
new file mode 100644
index 0000000..7471e5e
--- /dev/null
+++ b/client/src/models/Account.ts
@@ -0,0 +1,151 @@
+/*
+ * 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 { AccountDetails, VolatileDetails } from 'jami-web-common';
+
+import { Contact } from './Contact.js';
+import { Conversation } from './Conversation.js';
+
+export class Account {
+  private readonly id: string;
+  private _details: AccountDetails;
+  private _volatileDetails: VolatileDetails;
+  private _contacts: Contact[];
+  private readonly conversations: Record<string, Conversation>;
+  private defaultModerators: Contact[];
+  private devices: Record<string, string>;
+
+  static readonly TYPE_JAMI: string = 'RING';
+  static readonly TYPE_SIP: string = 'SIP';
+
+  constructor(id: string, details: AccountDetails, volatileDetails: VolatileDetails) {
+    this.id = id;
+    this._details = details || {};
+    this._volatileDetails = volatileDetails || {};
+    this._contacts = [];
+    this.conversations = {};
+    this.defaultModerators = [];
+    this.devices = {};
+  }
+
+  static from(object: any) {
+    const account = new Account(object.id, object.details, object.volatileDetails);
+    if (object.defaultModerators) account.defaultModerators = object.defaultModerators.map((m: any) => Contact.from(m));
+    return account;
+  }
+
+  getId() {
+    return this.id;
+  }
+
+  getType() {
+    return this._details['Account.type'];
+  }
+
+  getUri() {
+    return this._details['Account.username'];
+  }
+
+  getRegisteredName() {
+    return this._volatileDetails['Account.registeredName'];
+  }
+
+  isRendezVous() {
+    return this._details['Account.rendezVous'] === 'true';
+  }
+
+  isPublicIn() {
+    return this._details['DHT.PublicInCalls'] === 'true';
+  }
+
+  setDetail(detail: keyof AccountDetails, value: string) {
+    this._details[detail] = value;
+  }
+
+  updateDetails(details: Partial<AccountDetails>) {
+    return Object.assign(this._details, details);
+  }
+
+  getDetails() {
+    return this._details;
+  }
+
+  getDisplayName() {
+    return this._details['Account.displayName'] || this.getDisplayUri();
+  }
+
+  getDisplayUri() {
+    return this.getRegisteredName() || this.getUri();
+  }
+
+  getDisplayNameNoFallback() {
+    return this._details['Account.displayName'] || this.getRegisteredName();
+  }
+
+  getConversationIds() {
+    return Object.keys(this.conversations);
+  }
+
+  getConversations() {
+    return this.conversations;
+  }
+
+  getConversation(conversationId: string) {
+    return this.conversations[conversationId];
+  }
+
+  addConversation(conversation: Conversation) {
+    const conversationId = conversation.getId();
+    if (conversationId != null) {
+      this.conversations[conversationId] = conversation;
+    } else {
+      throw new Error('Conversation ID cannot be undefined');
+    }
+  }
+
+  removeConversation(conversationId: string) {
+    delete this.conversations[conversationId];
+  }
+
+  getContacts() {
+    return this._contacts;
+  }
+
+  set contacts(contacts: Contact[]) {
+    this._contacts = contacts;
+  }
+
+  getDefaultModerators() {
+    return this.defaultModerators;
+  }
+
+  set details(value: AccountDetails) {
+    this._details = value;
+  }
+
+  set volatileDetails(value: VolatileDetails) {
+    this._volatileDetails = value;
+  }
+
+  setDevices(devices: Record<string, string>) {
+    this.devices = { ...devices };
+  }
+
+  getDevices() {
+    return this.devices;
+  }
+}
diff --git a/client/src/models/Contact.ts b/client/src/models/Contact.ts
new file mode 100644
index 0000000..bd526ae
--- /dev/null
+++ b/client/src/models/Contact.ts
@@ -0,0 +1,54 @@
+/*
+ * 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/>.
+ */
+export class Contact {
+  private readonly uri: string;
+  private readonly displayName: string | undefined;
+  private registeredName: string | undefined;
+
+  constructor(uri: string) {
+    this.uri = uri;
+    this.displayName = undefined;
+    this.registeredName = undefined;
+  }
+
+  static from(object: any) {
+    const contact = new Contact(object.uri);
+    if (object.registeredName) contact.setRegisteredName(object.registeredName);
+    return contact;
+  }
+
+  getUri() {
+    return this.uri;
+  }
+
+  getRegisteredName() {
+    return this.registeredName;
+  }
+
+  setRegisteredName(name: string | undefined) {
+    this.registeredName = name;
+  }
+
+  getDisplayName() {
+    return this.getDisplayNameNoFallback() || this.getUri();
+  }
+
+  getDisplayNameNoFallback() {
+    return this.displayName || this.getRegisteredName();
+  }
+}
diff --git a/client/src/models/Conversation.ts b/client/src/models/Conversation.ts
new file mode 100644
index 0000000..fa72af9
--- /dev/null
+++ b/client/src/models/Conversation.ts
@@ -0,0 +1,121 @@
+/*
+ * 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 { Message } from 'jami-web-common';
+
+import { Contact } from './Contact';
+
+export interface ConversationMember {
+  contact: Contact;
+  role?: 'admin' | 'member' | 'invited' | 'banned' | 'left';
+}
+
+type ConversationInfos = Record<string, unknown>;
+
+export class Conversation {
+  private readonly id: string | undefined;
+  private readonly accountId: string;
+  private readonly members: ConversationMember[];
+  private messages: Message[];
+  private _infos: ConversationInfos;
+
+  constructor(id: string | undefined, accountId: string, members?: ConversationMember[]) {
+    this.id = id;
+    this.accountId = accountId;
+    this.members = members || [];
+
+    this.messages = [];
+    this._infos = {};
+  }
+
+  static from(accountId: string, object: any) {
+    const conversation = new Conversation(
+      object.id,
+      accountId,
+      object.members.map((member: any) => {
+        member.contact = Contact.from(member.contact);
+        return member;
+      })
+    );
+    conversation.messages = object.messages;
+    conversation.infos = object.infos;
+    return conversation;
+  }
+  static fromSingleContact(accountId: string, contact: Contact) {
+    return new Conversation(undefined, accountId, [{ contact }]);
+  }
+
+  getId() {
+    return this.id;
+  }
+
+  getAccountId() {
+    return this.accountId;
+  }
+
+  getDisplayName() {
+    if (this.members.length !== 0) {
+      return this.members[0].contact.getDisplayName();
+    }
+    return this.getDisplayUri();
+  }
+
+  getDisplayNameNoFallback() {
+    if (this.members.length !== 0) {
+      return this.members[0].contact.getDisplayNameNoFallback();
+    }
+  }
+
+  getDisplayUri() {
+    return this.getId() || this.getFirstMember().contact.getUri();
+  }
+
+  getFirstMember() {
+    return this.members[0];
+  }
+
+  getMembers() {
+    return this.members;
+  }
+
+  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("Can't insert message " + message.id);
+    }
+  }
+
+  addLoadedMessages(messages: Message[]) {
+    messages.forEach((message) => this.addMessage(message));
+  }
+
+  getMessages() {
+    return this.messages;
+  }
+
+  get infos() {
+    return this._infos;
+  }
+
+  set infos(infos: ConversationInfos) {
+    this._infos = infos;
+  }
+}
diff --git a/client/src/pages/ChatInterface.tsx b/client/src/pages/ChatInterface.tsx
index 5bcd8a2..f6a637a 100644
--- a/client/src/pages/ChatInterface.tsx
+++ b/client/src/pages/ChatInterface.tsx
@@ -26,7 +26,7 @@
 import SendMessageForm from '../components/SendMessageForm';
 import { ConversationContext } from '../contexts/ConversationProvider';
 import { WebSocketContext } from '../contexts/WebSocketProvider';
-import { useMessagesQuery, useSendMessageMutation } from '../services/Conversation';
+import { useMessagesQuery, useSendMessageMutation } from '../services/conversationQueries';
 import { FileHandler } from '../utils/files';
 
 const ChatInterface = () => {
diff --git a/client/src/services/Conversation.ts b/client/src/services/conversationQueries.ts
similarity index 100%
rename from client/src/services/Conversation.ts
rename to client/src/services/conversationQueries.ts