diff --git a/qml.qrc b/qml.qrc
index 135bfadb4..5abf8aece 100644
--- a/qml.qrc
+++ b/qml.qrc
@@ -169,5 +169,6 @@
src/commoncomponents/GeneratedMessageDelegate.qml
src/commoncomponents/DataTransferMessageDelegate.qml
src/mainview/components/ScrollToBottomButton.qml
+ src/commoncomponents/TypingDots.qml
diff --git a/src/commoncomponents/TypingDots.qml b/src/commoncomponents/TypingDots.qml
new file mode 100644
index 000000000..939f4193b
--- /dev/null
+++ b/src/commoncomponents/TypingDots.qml
@@ -0,0 +1,107 @@
+/*
+ * Copyright (C) 2021 by Savoir-faire Linux
+ * Author: Mingrui Zhang
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU 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 General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ */
+
+import QtQuick 2.15
+import QtQuick.Controls 2.15
+import QtGraphicalEffects 1.15
+
+import net.jami.Constants 1.1
+
+Row {
+ id: root
+
+ property int currentRect: 0
+
+ spacing: 5
+
+ Timer {
+ repeat: true
+ running: true
+ interval: JamiTheme.typingDotsAnimationInterval
+
+ onTriggered: {
+ if (root.currentRect < 2)
+ root.currentRect ++
+ else
+ root.currentRect = 0
+ }
+ }
+
+ Repeater {
+ model: 3
+
+ Rectangle {
+ id: circleRect
+
+ radius: JamiTheme.typingDotsRadius
+
+ width: JamiTheme.typingDotsSize
+ height: JamiTheme.typingDotsSize
+ color: JamiTheme.typingDotsNormalColor
+
+ states: State {
+ id: enlargeState
+
+ name: "enlarge"
+ when: root.currentRect === index
+ }
+
+ transitions: [
+ Transition {
+ to: "enlarge"
+ ParallelAnimation {
+ NumberAnimation {
+ from: 1.0
+ to: 1.3
+ target: circleRect
+ duration: JamiTheme.typingDotsAnimationInterval
+ property: "scale"
+ }
+
+ ColorAnimation {
+ from: JamiTheme.typingDotsNormalColor
+ to: JamiTheme.typingDotsEnlargeColor
+ target: circleRect
+ property: "color"
+ duration: JamiTheme.typingDotsAnimationInterval
+ }
+ }
+ },
+ Transition {
+ from: "enlarge"
+ ParallelAnimation {
+ NumberAnimation {
+ from: 1.3
+ to: 1.0
+ target: circleRect
+ duration: JamiTheme.typingDotsAnimationInterval
+ property: "scale"
+ }
+ ColorAnimation {
+ from: JamiTheme.typingDotsEnlargeColor
+ to: JamiTheme.typingDotsNormalColor
+ target: circleRect
+ property: "color"
+ duration: JamiTheme.typingDotsAnimationInterval
+ }
+ }
+ }
+ ]
+ }
+ }
+}
diff --git a/src/constant/JamiQmlUtils.qml b/src/constant/JamiQmlUtils.qml
index 0a82a6e37..d0c02d5b7 100644
--- a/src/constant/JamiQmlUtils.qml
+++ b/src/constant/JamiQmlUtils.qml
@@ -79,7 +79,7 @@ Item {
}
}
- TextMetrics {
+ Text {
id: globalTextMetrics
}
@@ -87,6 +87,6 @@ Item {
globalTextMetrics.font = font
globalTextMetrics.text = text
- return globalTextMetrics.boundingRect
+ return Qt.size(globalTextMetrics.contentWidth, globalTextMetrics.contentHeight)
}
}
diff --git a/src/constant/JamiStrings.qml b/src/constant/JamiStrings.qml
index cefd63286..5725172a9 100644
--- a/src/constant/JamiStrings.qml
+++ b/src/constant/JamiStrings.qml
@@ -257,6 +257,10 @@ Item {
// Chatview footer
property string jumpToLatest: qsTr("Jump to latest")
+ property string typeIndicatorSingle: qsTr("{} is typing…")
+ property string typeIndicatorPlural: qsTr("{} are typing…")
+ property string typeIndicatorMax: qsTr("Several people are typing…")
+ property string typeIndicatorAnd: qsTr(" and ")
// ConnectToAccountManager
property string enterJAMSURL: qsTr("Enter Jami Account Management Server (JAMS) URL")
diff --git a/src/constant/JamiTheme.qml b/src/constant/JamiTheme.qml
index 0f2b08147..79ff8055d 100644
--- a/src/constant/JamiTheme.qml
+++ b/src/constant/JamiTheme.qml
@@ -172,6 +172,10 @@ Item {
// Files To Send Container
property color removeFileButtonColor: Qt.rgba(96, 95, 97, 0.5)
+ // TypingDots
+ property color typingDotsNormalColor: darkTheme ? "#686b72" : "lightgrey"
+ property color typingDotsEnlargeColor: darkTheme ? "white" : Qt.darker("lightgrey", 3.0)
+
// Font.
property color faddedFontColor: darkTheme? "#c0c0c0" : "#a0a0a0"
property color faddedLastInteractionFontColor: darkTheme ? "#c0c0c0" : "#505050"
@@ -284,6 +288,11 @@ Item {
property real chatViewFooterTextAreaMaximumHeight: 130
property real chatViewScrollToBottomButtonBottomMargin: 8
+ // TypingDots
+ property real typingDotsAnimationInterval: 500
+ property real typingDotsRadius: 30
+ property real typingDotsSize: 8
+
// MessageWebView File Transfer Container
property real filesToSendContainerSpacing: 5
property real filesToSendContainerPadding: 10
diff --git a/src/mainview/components/MessageBar.qml b/src/mainview/components/MessageBar.qml
index b9a8c8628..cf5aed3b8 100644
--- a/src/mainview/components/MessageBar.qml
+++ b/src/mainview/components/MessageBar.qml
@@ -151,6 +151,7 @@ ColumnLayout {
- marginSize / 2
onSendMessagesRequired: root.sendMessageButtonClicked()
+ onTextChanged: MessagesAdapter.userIsComposing(text ? true : false)
}
PushButton {
diff --git a/src/mainview/components/MessageListView.qml b/src/mainview/components/MessageListView.qml
index cd7ff3c3e..5d515994d 100644
--- a/src/mainview/components/MessageListView.qml
+++ b/src/mainview/components/MessageListView.qml
@@ -273,4 +273,84 @@ ListView {
onClicked: root.ScrollBar.vertical.position =
1.0 - root.ScrollBar.vertical.size
}
+
+ header: Control {
+ id: typeIndicatorContainer
+
+ topPadding: 3
+
+ width: root.width
+ height: typeIndicatorNameText.contentHeight + topPadding
+
+ visible: MessagesAdapter.currentConvComposingList.length
+
+ TypingDots {
+ id: typingDots
+
+ anchors.left: typeIndicatorContainer.left
+ anchors.leftMargin: 5
+ anchors.verticalCenter: typeIndicatorContainer.verticalCenter
+ }
+
+ Text {
+ id: typeIndicatorNameText
+
+ anchors.left: typingDots.right
+ anchors.leftMargin: 5
+ anchors.verticalCenter: typeIndicatorContainer.verticalCenter
+
+ width: {
+ var textSize = text ? JamiQmlUtils.getTextBoundingRect(font, text).width : 0
+ var typingContentWidth = typingDots.width + typingDots.anchors.leftMargin
+ + typeIndicatorNameText.anchors.leftMargin
+ + typeIndicatorEndingText.contentWidth
+ return Math.min(typeIndicatorContainer.width - 5 - typingContentWidth, textSize)
+ }
+
+ font.pointSize: 8
+ font.bold: Font.DemiBold
+ elide: Text.ElideRight
+ color: JamiTheme.textColor
+ text: {
+ var finalText = ""
+ var nameList = MessagesAdapter.currentConvComposingList
+
+ if (nameList.length > 4)
+ return ""
+ if (nameList.length === 1)
+ return nameList[0]
+
+ for (var i = 0; i < nameList.length; i++) {
+ finalText += nameList[i]
+
+ if (i === nameList.length - 2)
+ finalText += JamiStrings.typeIndicatorAnd
+ else if (i !== nameList.length - 1)
+ finalText += ", "
+ }
+
+ return finalText
+ }
+ }
+
+ Text {
+ id: typeIndicatorEndingText
+
+ anchors.left: typeIndicatorNameText.right
+ anchors.verticalCenter: typeIndicatorContainer.verticalCenter
+
+ font.pointSize: 8
+ color: JamiTheme.textColor
+ text: {
+ var nameList = MessagesAdapter.currentConvComposingList
+
+ if (nameList.length > 4)
+ return JamiStrings.typeIndicatorMax
+ if (nameList.length === 1)
+ return JamiStrings.typeIndicatorSingle.replace("{}", "")
+
+ return JamiStrings.typeIndicatorPlural.replace("{}", "")
+ }
+ }
+ }
}
diff --git a/src/mainview/components/ScrollToBottomButton.qml b/src/mainview/components/ScrollToBottomButton.qml
index cb9b10892..7eae256bb 100644
--- a/src/mainview/components/ScrollToBottomButton.qml
+++ b/src/mainview/components/ScrollToBottomButton.qml
@@ -111,7 +111,8 @@ Control {
MouseArea {
anchors.fill: parent
- cursorShape: Qt.PointingHandCursor
+ cursorShape: root.opacity ? Qt.PointingHandCursor :
+ Qt.ArrowCursor
onClicked: root.clicked()
}
diff --git a/src/messagesadapter.cpp b/src/messagesadapter.cpp
index 15e33daa7..2d0875c5a 100644
--- a/src/messagesadapter.cpp
+++ b/src/messagesadapter.cpp
@@ -54,6 +54,7 @@ MessagesAdapter::MessagesAdapter(AppSettingsManager* settingsManager,
const auto& conversation = lrcInstance_->getConversationFromConvUid(convId);
filteredMsgListModel_->setSourceModel(conversation.interactions.get());
set_messageListModel(QVariant::fromValue(filteredMsgListModel_));
+ set_currentConvComposingList({});
});
connect(previewEngine_, &PreviewEngine::infoReady, this, &MessagesAdapter::onPreviewInfoReady);
@@ -113,6 +114,12 @@ MessagesAdapter::connectConversationModel()
this,
&MessagesAdapter::onConversationMessagesLoaded,
Qt::UniqueConnection);
+
+ QObject::connect(currentConversationModel,
+ &ConversationModel::composingStatusChanged,
+ this,
+ &MessagesAdapter::onComposingStatusChanged,
+ Qt::UniqueConnection);
}
void
@@ -414,6 +421,22 @@ MessagesAdapter::onMessageLinkified(const QString& messageId, const QString& lin
conversation.interactions->linkifyMessage(messageId, linkified);
}
+void
+MessagesAdapter::onComposingStatusChanged(const QString& convId,
+ const QString& contactUri,
+ bool isComposing)
+{
+ if (lrcInstance_->get_selectedConvUid() == convId) {
+ auto name = lrcInstance_->getCurrentContactModel()->bestNameForContact(contactUri);
+ if (isComposing)
+ currentConvComposingList_.append(name);
+ else
+ currentConvComposingList_.removeOne(name);
+
+ Q_EMIT currentConvComposingListChanged();
+ }
+}
+
bool
MessagesAdapter::isLocalImage(const QString& msg)
{
diff --git a/src/messagesadapter.h b/src/messagesadapter.h
index 062743332..aec4f65d9 100644
--- a/src/messagesadapter.h
+++ b/src/messagesadapter.h
@@ -60,6 +60,7 @@ class MessagesAdapter final : public QmlAdapterBase
{
Q_OBJECT
QML_RO_PROPERTY(QVariant, messageListModel)
+ QML_RO_PROPERTY(QList, currentConvComposingList)
public:
explicit MessagesAdapter(AppSettingsManager* settingsManager,
@@ -121,6 +122,9 @@ private Q_SLOTS:
void onPreviewInfoReady(QString messageIndex, QVariantMap urlInMessage);
void onConversationMessagesLoaded(uint32_t requestId, const QString& convId);
void onMessageLinkified(const QString& messageId, const QString& linkified);
+ void onComposingStatusChanged(const QString& convId,
+ const QString& contactUri,
+ bool isComposing);
private:
AppSettingsManager* settingsManager_;