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 ▼</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