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/server/src/services/ContactService.ts b/server/src/services/ContactService.ts
new file mode 100644
index 0000000..1ec626f
--- /dev/null
+++ b/server/src/services/ContactService.ts
@@ -0,0 +1,36 @@
+/*
+ * 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';
+import { Container, Service } from 'typedi';
+
+import { Jamid } from '../jamid/jamid.js';
+
+const jamid = Container.get(Jamid);
+
+@Service()
+export class ContactService {
+ async getContactFromUri(accountId: string, contactUri: string): Promise<IContact> {
+ const { username } = await jamid.lookupAddress(contactUri, accountId);
+
+ return {
+ uri: contactUri,
+ registeredName: username,
+ };
+ }
+}
diff --git a/server/src/services/ConversationService.ts b/server/src/services/ConversationService.ts
new file mode 100644
index 0000000..5265c16
--- /dev/null
+++ b/server/src/services/ConversationService.ts
@@ -0,0 +1,119 @@
+/*
+ * 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 { IConversationRequest, IConversationSummary, Message, WebSocketMessageType } from 'jami-web-common';
+import { Container, Service } from 'typedi';
+
+import { ConversationRequestMetadata } from '../jamid/conversation-request-metadata.js';
+import { Jamid } from '../jamid/jamid.js';
+import { WebSocketServer } from '../websocket/websocket-server.js';
+import { ContactService } from './ContactService.js';
+
+const jamid = Container.get(Jamid);
+const webSocketServer = Container.get(WebSocketServer);
+const contactService = Container.get(ContactService);
+
+@Service()
+export class ConversationService {
+ constructor() {
+ jamid.events.onConversationRequestReceived.subscribe(async (signal) => {
+ const conversationRequest = await this.createConversationRequest(signal.accountId, signal.metadata);
+ webSocketServer.send(signal.accountId, WebSocketMessageType.ConversationRequest, conversationRequest);
+ });
+ }
+
+ async createConversationRequest(
+ accountId: string,
+ jamidRequest: ConversationRequestMetadata
+ ): Promise<IConversationRequest> {
+ const contact = await contactService.getContactFromUri(accountId, jamidRequest.from);
+ const membersNames = await this.getMembersNames(accountId, jamidRequest.id);
+
+ // Currently, this does not actually works.
+ // It seems the members can't be accessed yet. Same on jami-android
+ if (membersNames.length === 0) {
+ membersNames.push(contact.registeredName || contact.uri);
+ }
+
+ return {
+ conversationId: jamidRequest.id,
+ received: jamidRequest.received,
+ from: contact,
+ infos: {
+ // Build a dataUrl from the avatar
+ // TODO: Host the image and use a "normal" url instead
+ avatar: `data:image/jpeg;base64,${jamidRequest.avatar}`,
+ description: jamidRequest.description,
+ mode: jamidRequest.mode,
+ title: jamidRequest.title,
+ },
+ membersNames,
+ };
+ }
+
+ async createConversationSummary(
+ accountId: string,
+ conversationId: string
+ ): Promise<IConversationSummary | undefined> {
+ const infos = jamid.getConversationInfos(accountId, conversationId);
+ if (Object.keys(infos).length === 0) {
+ return undefined;
+ }
+ // Build a dataUrl from the avatar
+ // TODO: Host the image and use a "normal" url instead
+ infos.avatar = `data:image/jpeg;base64,${infos.avatar}`;
+
+ const membersNames = await this.getMembersNames(accountId, conversationId);
+ let lastMessage: Message | undefined;
+ // Skip "merge" type since they are of no interest for the user
+ // Should we add some protection to prevent infinite loop?
+ while (!lastMessage || lastMessage.type === 'merge') {
+ lastMessage = (
+ await jamid.getConversationMessages(accountId, conversationId, lastMessage?.linearizedParent || '', 1)
+ )[0];
+ }
+
+ return {
+ id: conversationId,
+ avatar: infos.avatar,
+ title: infos.title,
+ membersNames,
+ lastMessage,
+ };
+ }
+
+ async getMembersNames(accountId: string, conversationId: string): Promise<string[]> {
+ // Retrieve the URI of the current account (Account.username actually stores the URI rather than the username)
+ const accountUri = jamid.getAccountDetails(accountId)['Account.username'];
+
+ const members = jamid.getConversationMembers(accountId, conversationId);
+ const membersNames: string[] = [];
+ for (const member of members) {
+ // Exclude current user from returned conversation members
+ if (member.uri === accountUri) {
+ continue;
+ }
+
+ // Add usernames for conversation members
+ const { username } = await jamid.lookupAddress(member.uri, accountId);
+ membersNames.push(username || member.uri);
+ }
+
+ return membersNames;
+ }
+}