Create new interfaces for objects transmitted using the REST API
Changes:
- Create new IContact, IAccount, and IConversation interfaces in common/
- These interfaces represent the serialized versions of the models which are transferred
- The client models are classes which implement these interfaces
- Create new LookupResult interface for nameserver lookup results
- Create new IConversationMember interface for conversation members
- The client interface ConversationMember extends this interface to have a Contact field rather than IContact
- Create new ConversationInfos interface for conversation infos
- Create new ContactDetails interface for contact details (used by contacts routes)
- Move request and response body interfaces into common/
- Merge AccountConfig into AccountDetails interface
- Create interfaces for server-only objects:
- ConversationMemberInfos
- ConversationRequestMetadata
- Ensure interfaces in jami-signal-interfaces.ts do not contain fields with JamiSwig types
- Rename models/ filenames to camelCase as they are not components
- Rewrite client models to have proper TypeScript accessors and remove unused getters
- Rewrite how client models are initialized from the serialized interface using .fromInterface static methods
- Make client models implement the interfaces in common/ for consistency
- Remove unneeded _next parameter for Express.js route handlers
- Use Partial<T> for all Express.js request body types on server
- Type all Axios response body types with interfaces
GitLab: #92
Change-Id: I4b2c75ac632ec5d9bf12a874a5ba04467c76fa6d
diff --git a/client/src/App.tsx b/client/src/App.tsx
index f113e25..afd9817 100644
--- a/client/src/App.tsx
+++ b/client/src/App.tsx
@@ -25,7 +25,7 @@
export async function checkSetupStatus(): Promise<boolean> {
try {
- const { data } = await axios.get('/setup/check', { baseURL: apiUrl });
+ const { data } = await axios.get<{ isSetupComplete: boolean }>('/setup/check', { baseURL: apiUrl });
return data.isSetupComplete;
} catch (e) {
throw new Error('Cannot connect to server', { cause: e });
diff --git a/client/src/components/AccountPreferences.tsx b/client/src/components/AccountPreferences.tsx
index 6788535..da926d0 100644
--- a/client/src/components/AccountPreferences.tsx
+++ b/client/src/components/AccountPreferences.tsx
@@ -39,7 +39,6 @@
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';
@@ -60,17 +59,17 @@
const { account, axiosInstance } = useAuthContext();
const devices: string[][] = [];
- const accountDevices = account.getDevices();
+ const accountDevices = account.devices;
for (const i in accountDevices) devices.push([i, accountDevices[i]]);
console.log(devices);
- const isJamiAccount = account.getType() === Account.TYPE_JAMI;
+ const isJamiAccount = account.getType() === 'RING';
const alias = isJamiAccount ? 'Jami account' : 'SIP account';
- const moderators = account.getDefaultModerators();
+ const moderators = account.defaultModerators;
const [defaultModeratorUri, setDefaultModeratorUri] = useState('');
- const [details, setDetails] = useState(account.getDetails());
+ const [details, setDetails] = useState(account.details);
const addModerator = async () => {
if (defaultModeratorUri) {
@@ -87,7 +86,8 @@
newDetails[key] = value ? 'true' : 'false';
console.log(newDetails);
await axiosInstance.patch('/account', newDetails);
- setDetails({ ...account.updateDetails(newDetails) });
+ account.updateDetails(newDetails);
+ setDetails(account.details);
};
return (
@@ -219,19 +219,19 @@
</IconButton>
</ListItemSecondaryAction>
</ListItem>
- {!moderators || moderators.length === 0 ? (
+ {moderators.length === 0 ? (
<ListItem key="placeholder">
<ListItemText primary="No default moderator" />
</ListItem>
) : (
moderators.map((moderator) => (
- <ListItem key={moderator.getUri()}>
+ <ListItem key={moderator.uri}>
<ListItemAvatar>
<ConversationAvatar displayName={moderator.getDisplayName()} />
</ListItemAvatar>
<ListItemText primary={moderator.getDisplayName()} />
<ListItemSecondaryAction>
- <IconButton onClick={() => removeModerator(moderator.getUri())} size="large">
+ <IconButton onClick={() => removeModerator(moderator.uri)} size="large">
<DeleteRounded />
</IconButton>
</ListItemSecondaryAction>
diff --git a/client/src/components/ConversationList.tsx b/client/src/components/ConversationList.tsx
index 6b03224..08553b7 100644
--- a/client/src/components/ConversationList.tsx
+++ b/client/src/components/ConversationList.tsx
@@ -22,7 +22,7 @@
import { useContext, useEffect } from 'react';
import { MessengerContext } from '../contexts/MessengerProvider';
-import { Conversation } from '../models/Conversation';
+import { Conversation } from '../models/conversation';
import { useAppSelector } from '../redux/hooks';
import ConversationListItem from './ConversationListItem';
@@ -48,7 +48,7 @@
</div>
)}
{conversations.map((conversation) => (
- <ConversationListItem key={conversation.getId()} conversation={conversation} />
+ <ConversationListItem key={conversation.id} conversation={conversation} />
))}
{conversations.length === 0 && (
<div className="list-placeholder">
diff --git a/client/src/components/ConversationListItem.tsx b/client/src/components/ConversationListItem.tsx
index 41cfbe4..d94733d 100644
--- a/client/src/components/ConversationListItem.tsx
+++ b/client/src/components/ConversationListItem.tsx
@@ -16,6 +16,7 @@
* <https://www.gnu.org/licenses/>.
*/
import { Box, ListItem, ListItemAvatar, ListItemText } from '@mui/material';
+import { ContactDetails } from 'jami-web-common';
import { QRCodeCanvas } from 'qrcode.react';
import { useCallback, useContext, useMemo, useState } from 'react';
import { useTranslation } from 'react-i18next';
@@ -25,7 +26,7 @@
import { useConversationContext } from '../contexts/ConversationProvider';
import { MessengerContext } from '../contexts/MessengerProvider';
import { useStartCall } from '../hooks/useStartCall';
-import { Conversation } from '../models/Conversation';
+import { Conversation } from '../models/conversation';
import { setRefreshFromSlice } from '../redux/appSlice';
import { useAppDispatch } from '../redux/hooks';
import ContextMenu, { ContextMenuHandler, useContextMenuHandler } from './ContextMenu';
@@ -56,10 +57,10 @@
const isSelected = conversation.getDisplayUri() === pathId;
const navigate = useNavigate();
- const userId = conversation?.getFirstMember()?.contact.getUri();
+ const userId = conversation?.getFirstMember()?.contact.uri;
const onClick = useCallback(() => {
- const newConversationId = conversation.getId();
+ const newConversationId = conversation.id;
if (newConversationId) {
navigate(`/conversation/${newConversationId}`);
} else {
@@ -116,7 +117,7 @@
const getContactDetails = useCallback(async () => {
const controller = new AbortController();
try {
- const { data } = await axiosInstance.get(`/contacts/${userId}`, {
+ const { data } = await axiosInstance.get<ContactDetails>(`/contacts/${userId}`, {
signal: controller.signal,
});
console.log('CONTACT LIST - DETAILS: ', data);
@@ -125,7 +126,7 @@
}
}, [axiosInstance, userId]);
- const conversationId = conversation.getId();
+ const conversationId = conversation.id;
const menuOptions: PopoverListItemData[] = useMemo(
() => [
diff --git a/client/src/components/ConversationView.tsx b/client/src/components/ConversationView.tsx
index 075ca6e..c9a19a2 100644
--- a/client/src/components/ConversationView.tsx
+++ b/client/src/components/ConversationView.tsx
@@ -22,7 +22,7 @@
import { useAuthContext } from '../contexts/AuthProvider';
import { useConversationContext } from '../contexts/ConversationProvider';
import { useStartCall } from '../hooks/useStartCall';
-import { ConversationMember } from '../models/Conversation';
+import { ConversationMember } from '../models/conversation';
import ChatInterface from '../pages/ChatInterface';
import { translateEnumeration, TranslateEnumerationOptions } from '../utils/translations';
import { AddParticipantButton, ShowOptionsMenuButton, StartAudioCallButton, StartVideoCallButton } from './Button';
@@ -46,7 +46,7 @@
const { conversation, conversationId } = useConversationContext();
const { t } = useTranslation();
- const members = conversation.getMembers();
+ const members = conversation.members;
const adminTitle = conversation.infos.title as string;
const title = useMemo(() => {
diff --git a/client/src/components/ConversationsOverviewCard.tsx b/client/src/components/ConversationsOverviewCard.tsx
index cdc1c79..67b4b68 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 { IConversation } from 'jami-web-common';
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();
@@ -28,12 +28,12 @@
const [conversationCount, setConversationCount] = useState<number | undefined>();
- const accountId = account.getId();
+ const accountId = account.id;
useEffect(() => {
const controller = new AbortController();
axiosInstance
- .get<Conversation[]>('/conversations', {
+ .get<IConversation[]>('/conversations', {
signal: controller.signal,
})
.then(({ data }) => {
diff --git a/client/src/components/JamiIdCard.tsx b/client/src/components/JamiIdCard.tsx
index 73c038c..bfa8045 100644
--- a/client/src/components/JamiIdCard.tsx
+++ b/client/src/components/JamiIdCard.tsx
@@ -17,7 +17,7 @@
*/
import { Box, Card, CardContent, Typography } from '@mui/material';
-import { Account } from '../models/Account';
+import { Account } from '../models/account';
type JamiIdCardProps = {
account: Account;
diff --git a/client/src/components/Message.tsx b/client/src/components/Message.tsx
index 669f041..8807295 100644
--- a/client/src/components/Message.tsx
+++ b/client/src/components/Message.tsx
@@ -23,8 +23,8 @@
import { useTranslation } from 'react-i18next';
import dayjs from '../dayjsInitializer';
-import { Account } from '../models/Account';
-import { Contact } from '../models/Contact';
+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 538acc7..356d3e6 100644
--- a/client/src/components/MessageList.tsx
+++ b/client/src/components/MessageList.tsx
@@ -23,7 +23,7 @@
import { Waypoint } from 'react-waypoint';
import { useAuthContext } from '../contexts/AuthProvider';
-import { ConversationMember } from '../models/Conversation';
+import { ConversationMember } from '../models/conversation';
import { MessageRow } from './Message';
import { ArrowDownIcon } from './SvgIcon';
@@ -56,7 +56,7 @@
if (isAccountMessage) {
author = account;
} else {
- const member = members.find((member) => message.author === member.contact.getUri());
+ const member = members.find((member) => message.author === member.contact.uri);
author = member?.contact;
}
if (!author) {
diff --git a/client/src/components/SendMessageForm.tsx b/client/src/components/SendMessageForm.tsx
index d1267aa..7bb7a7f 100644
--- a/client/src/components/SendMessageForm.tsx
+++ b/client/src/components/SendMessageForm.tsx
@@ -21,7 +21,7 @@
import { useTranslation } from 'react-i18next';
import { useAuthContext } from '../contexts/AuthProvider';
-import { ConversationMember } from '../models/Conversation';
+import { ConversationMember } from '../models/conversation';
import { translateEnumeration, TranslateEnumerationOptions } from '../utils/translations';
import {
RecordVideoMessageButton,
diff --git a/client/src/components/UsernameChooser.jsx b/client/src/components/UsernameChooser.jsx
index 19b8380..b098d1d 100644
--- a/client/src/components/UsernameChooser.jsx
+++ b/client/src/components/UsernameChooser.jsx
@@ -20,7 +20,7 @@
import axios from 'axios';
import { useEffect, useState } from 'react';
-import { apiUrl } from '../utils/constants.js';
+import { apiUrl } from '../utils/constants';
const isInputValid = (input) => input && input.length > 2;
diff --git a/client/src/contexts/AuthProvider.tsx b/client/src/contexts/AuthProvider.tsx
index d0d88cc..94f3dcf 100644
--- a/client/src/contexts/AuthProvider.tsx
+++ b/client/src/contexts/AuthProvider.tsx
@@ -16,13 +16,13 @@
* <https://www.gnu.org/licenses/>.
*/
import axios, { AxiosInstance } from 'axios';
-import { HttpStatusCode } from 'jami-web-common';
+import { HttpStatusCode, IAccount } from 'jami-web-common';
import { useCallback, useEffect, useMemo, useState } from 'react';
import { useNavigate } from 'react-router-dom';
import ProcessingRequest from '../components/ProcessingRequest';
import { createOptionalContext } from '../hooks/createOptionalContext';
-import { Account } from '../models/Account';
+import { Account } from '../models/account';
import { apiUrl } from '../utils/constants';
import { WithChildren } from '../utils/utils';
@@ -91,7 +91,7 @@
return;
}
- axiosInstance.get('/account').then(({ data }) => setAccount(Account.from(data)));
+ axiosInstance.get<IAccount>('/account').then(({ data }) => setAccount(Account.fromInterface(data)));
}, [axiosInstance, logout]);
if (!token || !account || !axiosInstance) {
@@ -104,7 +104,7 @@
token,
logout,
account,
- accountId: account.getId(),
+ accountId: account.id,
axiosInstance,
}}
>
diff --git a/client/src/contexts/CallProvider.tsx b/client/src/contexts/CallProvider.tsx
index 5464228..88951a9 100644
--- a/client/src/contexts/CallProvider.tsx
+++ b/client/src/contexts/CallProvider.tsx
@@ -156,7 +156,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(() => conversation.getFirstMember().contact.getUri(), [conversation]);
+ const contactUri = useMemo(() => conversation.getFirstMember().contact.uri, [conversation]);
useEffect(() => {
if (callStatus !== CallStatus.InCall) {
diff --git a/client/src/contexts/ConversationProvider.tsx b/client/src/contexts/ConversationProvider.tsx
index b927278..8d5f7b5 100644
--- a/client/src/contexts/ConversationProvider.tsx
+++ b/client/src/contexts/ConversationProvider.tsx
@@ -21,7 +21,7 @@
import LoadingPage from '../components/Loading';
import { createOptionalContext } from '../hooks/createOptionalContext';
import { useUrlParams } from '../hooks/useUrlParams';
-import { Conversation } from '../models/Conversation';
+import { Conversation } from '../models/conversation';
import { ConversationRouteParams } from '../router';
import { useConversationQuery } from '../services/conversationQueries';
import { WithChildren } from '../utils/utils';
@@ -51,7 +51,7 @@
useEffect(() => {
if (conversationQuery.isSuccess) {
- const conversation = Conversation.from(accountId, conversationQuery.data);
+ const conversation = Conversation.fromInterface(conversationQuery.data);
setConversation(conversation);
}
}, [accountId, conversationQuery.isSuccess, conversationQuery.data]);
diff --git a/client/src/contexts/MessengerProvider.tsx b/client/src/contexts/MessengerProvider.tsx
index 3ec6164..51a19fb 100644
--- a/client/src/contexts/MessengerProvider.tsx
+++ b/client/src/contexts/MessengerProvider.tsx
@@ -15,11 +15,11 @@
* License along with this program. If not, see
* <https://www.gnu.org/licenses/>.
*/
-import { ConversationMessage, WebSocketMessageType } from 'jami-web-common';
+import { ConversationMessage, IConversation, LookupResult, WebSocketMessageType } from 'jami-web-common';
import { createContext, ReactNode, useContext, useEffect, useState } from 'react';
-import { Contact } from '../models/Contact';
-import { Conversation } from '../models/Conversation';
+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';
@@ -61,11 +61,13 @@
useEffect(() => {
const controller = new AbortController();
axiosInstance
- .get<Conversation[]>('/conversations', {
+ .get<IConversation[]>('/conversations', {
signal: controller.signal,
})
.then(({ data }) => {
- setConversations(Object.values(data).map((c) => Conversation.from(accountId, c)));
+ setConversations(
+ Object.values(data).map((conversationInterface) => Conversation.fromInterface(conversationInterface))
+ );
});
// return () => controller.abort()
}, [axiosInstance, accountId, refresh]);
@@ -89,15 +91,13 @@
useEffect(() => {
if (!searchQuery) return;
const controller = new AbortController();
- // TODO: Type properly https://git.jami.net/savoirfairelinux/jami-web/-/issues/92
axiosInstance
- .get<{ state: number; address: string; username: string }>(`/ns/username/${searchQuery}`, {
+ .get<LookupResult>(`/ns/username/${searchQuery}`, {
signal: controller.signal,
})
.then(({ data }) => {
- const contact = new Contact(data.address);
- contact.setRegisteredName(data.username);
- setSearchResults(contact ? Conversation.fromSingleContact(accountId, contact) : undefined);
+ const contact = new Contact(data.address, data.username);
+ setSearchResults(contact ? Conversation.fromSingleContact(contact) : undefined);
})
.catch(() => {
setSearchResults(undefined);
diff --git a/client/src/contexts/WebRtcProvider.tsx b/client/src/contexts/WebRtcProvider.tsx
index 1ce3ff6..b7733bc 100644
--- a/client/src/contexts/WebRtcProvider.tsx
+++ b/client/src/contexts/WebRtcProvider.tsx
@@ -66,17 +66,17 @@
if (!webRtcConnection && account) {
const iceServers: RTCIceServer[] = [];
- if (account.getDetails()['TURN.enable'] === 'true') {
+ if (account.details['TURN.enable'] === 'true') {
iceServers.push({
- urls: 'turn:' + account.getDetails()['TURN.server'],
- username: account.getDetails()['TURN.username'],
- credential: account.getDetails()['TURN.password'],
+ urls: 'turn:' + account.details['TURN.server'],
+ username: account.details['TURN.username'],
+ credential: account.details['TURN.password'],
});
}
- if (account.getDetails()['STUN.enable'] === 'true') {
+ if (account.details['STUN.enable'] === 'true') {
iceServers.push({
- urls: 'stun:' + account.getDetails()['STUN.server'],
+ urls: 'stun:' + account.details['STUN.server'],
});
}
@@ -123,7 +123,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.getUri(), [conversation]);
+ const contactUri = useMemo(() => conversation.getFirstMember().contact.uri, [conversation]);
const getMediaDevices = useCallback(async (): Promise<MediaDevicesInfo> => {
try {
diff --git a/client/src/models/Account.ts b/client/src/models/Account.ts
deleted file mode 100644
index 7471e5e..0000000
--- a/client/src/models/Account.ts
+++ /dev/null
@@ -1,151 +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 { 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
deleted file mode 100644
index bd526ae..0000000
--- a/client/src/models/Contact.ts
+++ /dev/null
@@ -1,54 +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/>.
- */
-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
deleted file mode 100644
index fa72af9..0000000
--- a/client/src/models/Conversation.ts
+++ /dev/null
@@ -1,121 +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 { 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/models/account.ts b/client/src/models/account.ts
new file mode 100644
index 0000000..60ac1d8
--- /dev/null
+++ b/client/src/models/account.ts
@@ -0,0 +1,97 @@
+/*
+ * 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, Devices, IAccount, VolatileDetails } from 'jami-web-common';
+
+import { Contact } from './contact';
+import { Conversation } from './conversation';
+
+export type AccountType = 'RING' | 'SIP';
+
+export class Account implements IAccount {
+ readonly id: string;
+ details: AccountDetails;
+ volatileDetails: VolatileDetails;
+ defaultModerators: Contact[] = [];
+ devices: Devices = {};
+ contacts: Contact[] = [];
+
+ private _conversations: Record<string, Conversation> = {};
+
+ constructor(id: string, details: AccountDetails, volatileDetails: VolatileDetails) {
+ this.id = id;
+ this.details = details;
+ this.volatileDetails = volatileDetails;
+ }
+
+ static fromInterface(accountInterface: IAccount) {
+ const account = new Account(accountInterface.id, accountInterface.details, accountInterface.volatileDetails);
+ account.defaultModerators = accountInterface.defaultModerators.map(Contact.fromInterface);
+ return account;
+ }
+
+ getType(): AccountType {
+ return this.details['Account.type'] as AccountType;
+ }
+
+ 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';
+ }
+
+ updateDetails(details: Partial<AccountDetails>) {
+ this.details = { ...this.details, ...details };
+ }
+
+ getDisplayUri() {
+ return this.getRegisteredName() ?? this.getUri();
+ }
+
+ getDisplayName() {
+ return this.details['Account.displayName'] ?? this.getDisplayUri();
+ }
+
+ 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/contact.ts b/client/src/models/contact.ts
new file mode 100644
index 0000000..e696db0
--- /dev/null
+++ b/client/src/models/contact.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 { IContact } from 'jami-web-common';
+
+export class Contact implements IContact {
+ readonly uri: string;
+ registeredName?: string;
+ displayName?: string;
+
+ constructor(uri: string, registeredName?: string) {
+ this.uri = uri;
+ this.registeredName = registeredName;
+ }
+
+ static fromInterface(contactInterface: IContact) {
+ return new Contact(contactInterface.uri, contactInterface.registeredName);
+ }
+
+ getDisplayName() {
+ return this.getDisplayNameNoFallback() ?? this.uri;
+ }
+
+ getDisplayNameNoFallback() {
+ return this.displayName ?? this.registeredName;
+ }
+}
diff --git a/client/src/models/conversation.ts b/client/src/models/conversation.ts
new file mode 100644
index 0000000..484f166
--- /dev/null
+++ b/client/src/models/conversation.ts
@@ -0,0 +1,94 @@
+/*
+ * 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/AddContactPage.tsx b/client/src/pages/AddContactPage.tsx
index 5e3c375..fb7e4bb 100644
--- a/client/src/pages/AddContactPage.tsx
+++ b/client/src/pages/AddContactPage.tsx
@@ -17,6 +17,7 @@
*/
import GroupAddRounded from '@mui/icons-material/GroupAddRounded';
import { Box, Card, CardContent, Container, Fab, Typography } from '@mui/material';
+import { ContactDetails } from 'jami-web-common';
import { useTranslation } from 'react-i18next';
import { useNavigate } from 'react-router-dom';
@@ -36,7 +37,7 @@
const dispatch = useAppDispatch();
const handleClick = async () => {
- const { data } = await axiosInstance.put(`/contacts/${contactId}`);
+ const { data } = await axiosInstance.put<ContactDetails>(`/contacts/${contactId}`);
dispatch(setRefreshFromSlice());
if (data.conversationId) {
diff --git a/client/src/pages/CallInterface.tsx b/client/src/pages/CallInterface.tsx
index 10b1d3c..3d5b366 100644
--- a/client/src/pages/CallInterface.tsx
+++ b/client/src/pages/CallInterface.tsx
@@ -214,7 +214,7 @@
const { callStartTime } = useContext(CallContext);
const { conversation } = useConversationContext();
const [elapsedTime, setElapsedTime] = useState(0);
- const memberName = useMemo(() => conversation.getFirstMember().contact.getRegisteredName(), [conversation]);
+ const memberName = useMemo(() => conversation.getFirstMember().contact.registeredName, [conversation]);
useEffect(() => {
if (callStartTime) {
diff --git a/client/src/pages/CallPending.tsx b/client/src/pages/CallPending.tsx
index 2cd57c6..54fb916 100644
--- a/client/src/pages/CallPending.tsx
+++ b/client/src/pages/CallPending.tsx
@@ -149,7 +149,7 @@
const { callStatus } = useContext(CallContext);
const { t } = useTranslation();
const { conversation } = useConversationContext();
- const memberName = useMemo(() => conversation.getFirstMember().contact.getRegisteredName(), [conversation]);
+ const memberName = useMemo(() => conversation.getFirstMember().contact.registeredName, [conversation]);
let title = t('loading');
@@ -183,7 +183,7 @@
const { t } = useTranslation();
const { conversation } = useConversationContext();
- const memberName = useMemo(() => conversation.getFirstMember().contact.getRegisteredName(), [conversation]);
+ const memberName = useMemo(() => conversation.getFirstMember().contact.registeredName, [conversation]);
let title = t('loading');
diff --git a/client/src/pages/ChatInterface.tsx b/client/src/pages/ChatInterface.tsx
index 4ae9ce6..00aeb3e 100644
--- a/client/src/pages/ChatInterface.tsx
+++ b/client/src/pages/ChatInterface.tsx
@@ -105,7 +105,7 @@
return <div>Error loading {conversationId}</div>;
}
- const members = conversation.getMembers();
+ const members = conversation.members;
return (
<Stack flex={1} overflow="hidden" {...getRootProps()} paddingBottom="16px">
diff --git a/client/src/pages/SetupLogin.tsx b/client/src/pages/SetupLogin.tsx
index d34d366..2ae33b9 100644
--- a/client/src/pages/SetupLogin.tsx
+++ b/client/src/pages/SetupLogin.tsx
@@ -18,7 +18,7 @@
import GroupAddRounded from '@mui/icons-material/GroupAddRounded';
import { Box, Card, CardContent, Container, Fab, Input, Typography } from '@mui/material';
import axios from 'axios';
-import { HttpStatusCode } from 'jami-web-common';
+import { AccessToken, HttpStatusCode } from 'jami-web-common';
import { FormEvent, useEffect, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { useNavigate } from 'react-router-dom';
@@ -45,7 +45,7 @@
const loginAdmin = async (password: string) => {
try {
- const { data } = await axios.post('/setup/admin/login', { password }, { baseURL: apiUrl });
+ const { data } = await axios.post<AccessToken>('/setup/admin/login', { password }, { baseURL: apiUrl });
localStorage.setItem('adminAccessToken', data.accessToken);
} catch (e: any) {
if (e.response?.status === HttpStatusCode.Forbidden) {
diff --git a/client/src/services/conversationQueries.ts b/client/src/services/conversationQueries.ts
index 92cc80d..44c0aae 100644
--- a/client/src/services/conversationQueries.ts
+++ b/client/src/services/conversationQueries.ts
@@ -16,6 +16,7 @@
* <https://www.gnu.org/licenses/>.
*/
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
+import { IConversation, Message } from 'jami-web-common';
import { useAuthContext } from '../contexts/AuthProvider';
@@ -24,7 +25,7 @@
return useQuery(
['conversation', conversationId],
async () => {
- const { data } = await axiosInstance.get(`/conversations/${conversationId}`);
+ const { data } = await axiosInstance.get<IConversation>(`/conversations/${conversationId}`);
return data;
},
{
@@ -38,7 +39,7 @@
return useQuery(
['messages', conversationId],
async () => {
- const { data } = await axiosInstance.get(`/conversations/${conversationId}/messages`);
+ const { data } = await axiosInstance.get<Message[]>(`/conversations/${conversationId}/messages`);
return data;
},
{
diff --git a/client/src/utils/auth.ts b/client/src/utils/auth.ts
index 7b3a088..07797a4 100644
--- a/client/src/utils/auth.ts
+++ b/client/src/utils/auth.ts
@@ -17,7 +17,7 @@
*/
import axios from 'axios';
import { passwordStrength } from 'check-password-strength';
-import { HttpStatusCode } from 'jami-web-common';
+import { AccessToken, HttpStatusCode } from 'jami-web-common';
import { PasswordStrength } from '../enums/passwordStrength';
import { apiUrl } from './constants';
@@ -75,7 +75,7 @@
export async function loginUser(username: string, password: string, isJams: boolean): Promise<string> {
try {
- const { data } = await axios.post('/auth/login', { username, password, isJams }, { baseURL: apiUrl });
+ const { data } = await axios.post<AccessToken>('/auth/login', { username, password, isJams }, { baseURL: apiUrl });
return data.accessToken;
} catch (e: any) {
switch (e.response?.status) {