Add composing notification

Change-Id: I2c052c4395a56ba6acf882cea3be4b82e2fde761
diff --git a/server/src/app.ts b/server/src/app.ts
index ff94a59..0e1b055 100644
--- a/server/src/app.ts
+++ b/server/src/app.ts
@@ -32,6 +32,7 @@
 import { linkPreviewRouter } from './routers/link-preview-router.js';
 import { nameserverRouter } from './routers/nameserver-router.js';
 import { setupRouter } from './routers/setup-router.js';
+import { bindChatCallbacks } from './websocket/chat-handler.js';
 import { bindWebRtcCallbacks } from './websocket/webrtc-handler.js';
 
 @Service()
@@ -59,6 +60,7 @@
     this.app.use('/ns', nameserverRouter);
 
     // Setup WebSocket callbacks
+    bindChatCallbacks();
     bindWebRtcCallbacks();
 
     // Setup 404 error handling
diff --git a/server/src/jamid/jami-signal-interfaces.ts b/server/src/jamid/jami-signal-interfaces.ts
index 851239b..188dfaf 100644
--- a/server/src/jamid/jami-signal-interfaces.ts
+++ b/server/src/jamid/jami-signal-interfaces.ts
@@ -133,3 +133,10 @@
   conversationId: string;
   message: Message;
 }
+
+export interface ComposingStatusChanged {
+  accountId: string;
+  conversationId: string;
+  from: string;
+  status: number;
+}
diff --git a/server/src/jamid/jami-signal.ts b/server/src/jamid/jami-signal.ts
index 6a9b15b..15bef48 100644
--- a/server/src/jamid/jami-signal.ts
+++ b/server/src/jamid/jami-signal.ts
@@ -25,6 +25,7 @@
   AccountsChanged = 'AccountsChanged',
   AccountDetailsChanged = 'AccountDetailsChanged',
   RegistrationStateChanged = 'RegistrationStateChanged',
+  ComposingStatusChanged = 'ComposingStatusChanged',
   IncomingTrustRequest = 'IncomingTrustRequest',
   ContactAdded = 'ContactAdded',
   ContactRemoved = 'ContactRemoved',
diff --git a/server/src/jamid/jami-swig.ts b/server/src/jamid/jami-swig.ts
index cd6a95c..07fa2dd 100644
--- a/server/src/jamid/jami-swig.ts
+++ b/server/src/jamid/jami-swig.ts
@@ -123,6 +123,7 @@
 
   sendMessage(accountId: string, conversationId: string, message: string, replyTo: string, flag: number): void;
   loadConversationMessages(accountId: string, conversationId: string, fromMessage: string, n: number): number;
+  setIsComposing(accountId: string, conversationId: string, isWriting: boolean): void;
 
   getCallList(accountId: string): StringVect;
   getCallDetails(accountId: string, callId: string): StringMap;
diff --git a/server/src/jamid/jamid.ts b/server/src/jamid/jamid.ts
index a868afe..ab4d8a8 100644
--- a/server/src/jamid/jamid.ts
+++ b/server/src/jamid/jamid.ts
@@ -19,6 +19,7 @@
 
 import {
   AccountDetails,
+  ComposingStatus,
   ContactDetails,
   ConversationInfos,
   ConversationMessage,
@@ -41,6 +42,7 @@
 import {
   AccountDetailsChanged,
   AccountMessageStatusChanged,
+  ComposingStatusChanged,
   ContactAdded,
   ContactRemoved,
   ConversationLoaded,
@@ -167,6 +169,10 @@
     handlers.MessageReceived = (accountId: string, conversationId: string, message: Message) =>
       onMessageReceived.next({ accountId, conversationId, message });
 
+    const onComposingStatusChanged = new Subject<ComposingStatusChanged>();
+    handlers.ComposingStatusChanged = (accountId: string, conversationId: string, from: string, status: number) =>
+      onComposingStatusChanged.next({ accountId, conversationId, from, status });
+
     // Expose all signals in an events object to allow other handlers to subscribe after jamiSwig.init()
     this.events = {
       onAccountsChanged: onAccountsChanged.asObservable(),
@@ -186,6 +192,7 @@
       onConversationLoaded: onConversationLoaded.asObservable(),
       onConversationMemberEvent: onConversationMemberEvent.asObservable(),
       onMessageReceived: onMessageReceived.asObservable(),
+      onComposingStatusChanged: onComposingStatusChanged.asObservable(),
     };
 
     this.setupSignalHandlers();
@@ -378,6 +385,10 @@
     this.jamiSwig.sendMessage(accountId, conversationId, message, replyTo ?? '', flag ?? 0);
   }
 
+  setIsComposing(accountId: string, conversationId: string, isWriting: boolean) {
+    this.jamiSwig.setIsComposing(accountId, conversationId, isWriting);
+  }
+
   getCallIds(accountId: string): string[] {
     return stringVectToArray(this.jamiSwig.getCallList(accountId));
   }
@@ -503,5 +514,16 @@
       };
       this.webSocketServer.send(signal.accountId, WebSocketMessageType.ConversationMessage, data);
     });
+
+    this.events.onComposingStatusChanged.subscribe((signal) => {
+      log.debug('Received ComposingStatusChanged:', JSON.stringify(signal));
+
+      const data: ComposingStatus = {
+        contactId: signal.from,
+        conversationId: signal.conversationId,
+        isWriting: signal.status === 1,
+      };
+      this.webSocketServer.send(signal.accountId, WebSocketMessageType.OnComposingStatusChanged, data);
+    });
   }
 }
diff --git a/server/src/websocket/chat-handler.ts b/server/src/websocket/chat-handler.ts
new file mode 100644
index 0000000..161203b
--- /dev/null
+++ b/server/src/websocket/chat-handler.ts
@@ -0,0 +1,40 @@
+/*
+ * 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 { ComposingStatus, WebSocketMessageType } from 'jami-web-common';
+import log from 'loglevel';
+import { Container } from 'typedi';
+
+import { Jamid } from '../jamid/jamid.js';
+import { WebSocketServer } from './websocket-server.js';
+
+const jamid = Container.get(Jamid);
+const webSocketServer = Container.get(WebSocketServer);
+
+export function bindChatCallbacks() {
+  webSocketServer.bind(
+    WebSocketMessageType.SetIsComposing,
+    (accountId, { contactId, conversationId, isWriting }: ComposingStatus) => {
+      if (contactId !== undefined) {
+        log.warn('SetIsComposing expects contactId to be undefined');
+        return;
+      }
+
+      jamid.setIsComposing(accountId, conversationId, isWriting);
+    }
+  );
+}