From 77f906e136485da586cc9ad911853faf7fa6afa1 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?S=C3=A9bastien=20Blin?=
 <sebastien.blin@savoirfairelinux.com>
Date: Thu, 5 Mar 2020 10:42:09 -0500
Subject: [PATCH] conversationmodel: add composingStatus indicator support

Change-Id: I06daf46ed6f59f43c4d7f5ca653e7162a27ed7c9
Gitlab: #425
---
 src/api/conversationmodel.h               | 13 +++++
 src/conversationmodel.cpp                 | 34 +++++++++++
 src/qtwrapper/configurationmanager_wrap.h |  9 +++
 src/web-chatview/chatview.css             | 70 +++++++++++++++++++++++
 src/web-chatview/chatview.html            |  1 +
 src/web-chatview/chatview.js              | 64 +++++++++++++++++++--
 6 files changed, 187 insertions(+), 4 deletions(-)

diff --git a/src/api/conversationmodel.h b/src/api/conversationmodel.h
index 410e3748..e9bde275 100644
--- a/src/api/conversationmodel.h
+++ b/src/api/conversationmodel.h
@@ -221,6 +221,12 @@ public:
      * @return the number of unread messages for the conversation
      */
     int getNumberOfUnreadMessagesFor(const QString& convUid);
+    /**
+     * Send a composing status
+     * @param uid           conversation's id
+     * @param isComposing   if is composing
+     */
+    void setIsComposing(const QString& uid, bool isComposing);
 
 Q_SIGNALS:
     /**
@@ -285,6 +291,13 @@ Q_SIGNALS:
      * @param uid
      */
     void conversationReady(QString uid) const;
+    /**
+     * Emitted when a contact in a conversation is composing a message
+     * @param uid           conversation's id
+     * @param contactUri    contact's uri
+     * @param isComposing   if contact is composing a message
+     */
+    void composingStatusChanged(const QString& uid, const QString& contactUri, bool isComposing) const;
 
 private:
     std::unique_ptr<ConversationModelPimpl> pimpl_;
diff --git a/src/conversationmodel.cpp b/src/conversationmodel.cpp
index 60711e49..3e2b2db3 100644
--- a/src/conversationmodel.cpp
+++ b/src/conversationmodel.cpp
@@ -244,6 +244,13 @@ public Q_SLOTS:
      * @param confId
      */
     void slotConferenceRemoved(const QString& confId);
+    /**
+     * Listen for when a contact is composing
+     * @param accountId
+     * @param contactUri
+     * @param isComposing
+     */
+    void slotComposingStatusChanged(const QString& accountId, const QString& contactUri, bool isComposing);
 
     void slotTransferStatusCreated(long long dringId, api::datatransfer::Info info);
     void slotTransferStatusCanceled(long long dringId, api::datatransfer::Info info);
@@ -1204,6 +1211,10 @@ ConversationModelPimpl::ConversationModelPimpl(const ConversationModel& linked,
             &CallbacksHandler::conferenceRemoved,
             this,
             &ConversationModelPimpl::slotConferenceRemoved);
+    connect(&ConfigurationManager::instance(),
+            &ConfigurationManagerInterface::composingStatusChanged,
+            this,
+            &ConversationModelPimpl::slotComposingStatusChanged);
 
     // data transfer
     connect(&*linked.owner.contactModel, &ContactModel::newAccountTransfer,
@@ -1278,6 +1289,8 @@ ConversationModelPimpl::~ConversationModelPimpl()
                this, &ConversationModelPimpl::slotCallAddedToConference);
     disconnect(&callbacksHandler, &CallbacksHandler::conferenceRemoved,
                this, &ConversationModelPimpl::slotConferenceRemoved);
+    disconnect(&ConfigurationManager::instance(), &ConfigurationManagerInterface::composingStatusChanged,
+               this, &ConversationModelPimpl::slotComposingStatusChanged);
 
     // data transfer
     disconnect(&*linked.owner.contactModel, &ContactModel::newAccountTransfer,
@@ -1927,12 +1940,33 @@ ConversationModelPimpl::slotConferenceRemoved(const QString& confId)
     }
 }
 
+void
+ConversationModelPimpl::slotComposingStatusChanged(const QString& accountId, const QString& contactUri, bool isComposing)
+{
+    if (accountId != linked.owner.id) return;
+    // Check conversation's validity
+    auto convIds = storage::getConversationsWithPeer(db, contactUri);
+    if (convIds.empty()) return;
+    emit linked.composingStatusChanged(convIds.front(), contactUri, isComposing);
+}
+
 int
 ConversationModelPimpl::getNumberOfUnreadMessagesFor(const QString& uid)
 {
     return storage::countUnreadFromInteractions(db, uid);
 }
 
+void
+ConversationModel::setIsComposing(const QString& uid, bool isComposing)
+{
+    auto conversationIdx = pimpl_->indexOf(uid);
+    if (conversationIdx == -1 || !owner.enabled)
+        return;
+
+    const auto peerUri = pimpl_->conversations[conversationIdx].participants.front();
+    ConfigurationManager::instance().setIsComposing(owner.id, peerUri, isComposing);
+}
+
 void
 ConversationModel::sendFile(const QString& convUid,
                             const QString& path,
diff --git a/src/qtwrapper/configurationmanager_wrap.h b/src/qtwrapper/configurationmanager_wrap.h
index a2795e12..009aab1e 100644
--- a/src/qtwrapper/configurationmanager_wrap.h
+++ b/src/qtwrapper/configurationmanager_wrap.h
@@ -172,6 +172,10 @@ public:
                 [this](const std::string& message) {
                     Q_EMIT this->debugMessageReceived(QString(message.c_str()));
                 }),
+            exportable_callback<ConfigurationSignal::ComposingStatusChanged>(
+                [this](const std::string& account_id, const std::string& from, int status) {
+                    Q_EMIT this->composingStatusChanged(QString(account_id.c_str()), QString(from.c_str()), status > 0 ? true : false);
+                }),
         };
 
         dataXferHandlers = {
@@ -705,6 +709,10 @@ public Q_SLOTS: // METHODS
         DRing::pushNotificationReceived(from.toStdString(), convertMap(data));
     }
 
+    void setIsComposing(const QString& accountId, const QString& contactId, bool isComposing) {
+        DRing::setIsComposing(accountId.toStdString(), contactId.toStdString(), isComposing);
+    }
+
 Q_SIGNALS: // SIGNALS
     void volumeChanged(const QString& device, double value);
     void accountsChanged();
@@ -735,6 +743,7 @@ Q_SIGNALS: // SIGNALS
     void dataTransferEvent(qulonglong transfer_id, uint code);
     void deviceRevocationEnded(const QString& accountId, const QString& deviceId, int status);
     void debugMessageReceived(const QString& message);
+    void composingStatusChanged(const QString& accountId, const QString& contactId, bool isComposing);
 };
 
 namespace org { namespace ring { namespace Ring {
diff --git a/src/web-chatview/chatview.css b/src/web-chatview/chatview.css
index dfc7c5a8..3c6ed5ee 100644
--- a/src/web-chatview/chatview.css
+++ b/src/web-chatview/chatview.css
@@ -1206,4 +1206,74 @@ video {
 
 .oneEntry #nav-contactid-bestId {
     display: none;
+}
+
+.typing_message {
+    display: flex;
+    justify-content: flex-start;
+}
+
+.typing_message .message_wrapper {
+    border-top-left-radius: 0;
+    transform-origin: top left;
+    background-color: var(--message-out-bg);
+    color: var(--message-out-txt);
+    margin-top: auto;
+    margin-bottom: auto;
+}
+
+.typing-indicator {
+  width: auto;
+  border-radius: 50px;
+  padding: 0px;
+  display: table;
+  position: relative;
+  -webkit-animation: 2s bulge infinite ease-out;
+          animation: 2s bulge infinite ease-out;
+}
+.typing-indicator span {
+  height: 8px;
+  width: 8px;
+  float: left;
+  margin: 0 1px;
+  background-color: #003b4e;
+  display: block;
+  border-radius: 50%;
+  opacity: 0.4;
+}
+.typing-indicator span:nth-of-type(1) {
+  -webkit-animation: 1s blink infinite 0.3333s;
+          animation: 1s blink infinite 0.3333s;
+}
+.typing-indicator span:nth-of-type(2) {
+  -webkit-animation: 1s blink infinite 0.6666s;
+          animation: 1s blink infinite 0.6666s;
+}
+.typing-indicator span:nth-of-type(3) {
+  -webkit-animation: 1s blink infinite 0.9999s;
+          animation: 1s blink infinite 0.9999s;
+}
+
+@-webkit-keyframes blink {
+  50% {
+    opacity: 1;
+  }
+}
+
+@keyframes blink {
+  50% {
+    opacity: 1;
+  }
+}
+@-webkit-keyframes bulge {
+  50% {
+    -webkit-transform: scale(1.05);
+            transform: scale(1.05);
+  }
+}
+@keyframes bulge {
+  50% {
+    -webkit-transform: scale(1.05);
+            transform: scale(1.05);
+  }
 }
\ No newline at end of file
diff --git a/src/web-chatview/chatview.html b/src/web-chatview/chatview.html
index b44acbd8..05c9f1db 100644
--- a/src/web-chatview/chatview.html
+++ b/src/web-chatview/chatview.html
@@ -86,6 +86,7 @@
     </div>
     <div id="container">
         <div id="messages" onscroll="onScrolled()"></div>
+
         <div id="back_to_bottom_button_container">
             <div id="back_to_bottom_button" onclick="back_to_bottom()">Jump to latest &#9660;</div>
         </div>
diff --git a/src/web-chatview/chatview.js b/src/web-chatview/chatview.js
index 1b342580..5cff5658 100644
--- a/src/web-chatview/chatview.js
+++ b/src/web-chatview/chatview.js
@@ -376,6 +376,12 @@ function grow_text_area() {
         var total_size = parseInt(msgbar_size) + parseInt(new_height) - parseInt(old_height)
 
         document.body.style.setProperty("--messagebar-size", total_size.toString() + "px")
+
+        if (use_qt) {
+            window.jsbridge.onComposing(messageBarInput.value.length !== 0)
+        } else {
+            window.prompt(`ON_COMPOSING:${messageBarInput.value.length !== 0}`)
+        }
     }, [])
 }
 
@@ -389,6 +395,7 @@ function grow_text_area() {
 function process_messagebar_keydown(key) {
     key = key || event
     var map = {}
+
     map[key.keyCode] = key.type == "keydown"
     if (key.ctrlKey && map[13]) {
         messageBarInput.value += "\n"
@@ -1728,8 +1735,15 @@ function addOrUpdateMessage(message_object, new_message, insert_after = true, me
             computeSequencing(previousMessage, message_div, null, insert_after)
             if (previousMessage) {
                 previousMessage.classList.remove("last_message")
+                console.log(previousMessage)
+                if (previousMessage.id === "message_typing") {
+                    previousMessage.parentNode.removeChild(previousMessage)
+                    console.log(previousMessage)
+                    message_div.parentNode.appendChild(previousMessage)
+                } else {
+                    message_div.classList.add("last_message")
+                }
             }
-            message_div.classList.add("last_message")
 
             /* When inserting at the bottom we should also check that the
                previously sent message does not have the same timestamp.
@@ -2135,7 +2149,7 @@ function setSenderImage(set_sender_image_object)
     if (use_qt) {
         var sender_contact_method = set_sender_image_object["sender_contact_method"].replace(/@/g, "_").replace(/\./g, "_"),
             sender_image = set_sender_image_object["sender_image"],
-            sender_image_id = "sender_image_" + sender_contact_method,
+            contactUri = "sender_image_" + sender_contact_method,
             invite_sender_image_id = "invite_sender_image_" + sender_contact_method,
             currentSenderImage = document.getElementById(sender_image_id), // Remove the currently set sender image
             style, invite_style
@@ -2158,17 +2172,59 @@ function setSenderImage(set_sender_image_object)
 
     style.type = "text/css"
     style.id = sender_image_id
-    style.innerHTML = "." + sender_image_id + " {content: url(data:image/png;base64," + sender_image + ");height: 2.25em;width: 2.25em;}"
+    style.innerHTML = "." + sender_image_id + " {content: url(data:image/png;base64," + sender_image + ");height: 32px; width: 32px;}"
     document.head.appendChild(style)
 
     invite_style = document.createElement("style")
 
     invite_style.type = "text/css"
     invite_style.id = invite_sender_image_id
-    invite_style.innerHTML = "." + invite_sender_image_id + " {content: url(data:image/png;base64," + sender_image + ");height: 48px;width: 48px;}"
+    invite_style.innerHTML = "." + invite_sender_image_id + " {content: url(data:image/png;base64," + sender_image + ");height: 48px; width: 48px;}"
     document.head.appendChild(invite_style)
 }
 
+/**
+ * Show Typing indicator
+ */
+/* exported showTypingIndicator */
+function showTypingIndicator(contactUri, isTyping) {
+
+    var message_div = messages.lastChild.querySelector("#message_typing")
+    if (!isTyping) {
+        if (message_div) {
+            message_div.style.display = 'none'
+        }
+    } else {
+        if (message_div) {
+            message_div.parentNode.removeChild(message_div)
+        }
+        message_div = buildNewMessage({
+            "id":"typing",
+            "type":"text",
+            "text":"",
+            "direction":"in",
+            "delivery_status":"",
+            "sender_contact_method": contactUri
+        })
+
+        var previousMessage = messages.lastChild.lastChild
+        messages.lastChild.appendChild(message_div)
+        computeSequencing(previousMessage, message_div, null, true)
+        if (previousMessage) {
+            previousMessage.classList.remove("last_message")
+        }
+        message_div.classList.add("last_message")
+        let msg_text = message_div.querySelector(".message_text")
+        msg_text.innerHTML = " \
+                        <div class=\"typing-indicator\"> \
+                            <span></span> \
+                            <span></span> \
+                            <span></span> \
+                        </div>"
+    }
+    updateMesPos()
+}
+
 /**
  * Copy Mouse Selected Text and return it
  */
-- 
GitLab