diff --git a/assets/languages/en.ts b/assets/languages/en.ts
index 1317cbdb2..0d7db03b3 100644
--- a/assets/languages/en.ts
+++ b/assets/languages/en.ts
@@ -414,6 +414,10 @@
Unable to send file.
Server url not configured.
+
+ isComposing
+ %1 is typing...
+
Cli
diff --git a/assets/languages/fr.ts b/assets/languages/fr.ts
index e2f22d817..0e277fafc 100644
--- a/assets/languages/fr.ts
+++ b/assets/languages/fr.ts
@@ -414,6 +414,10 @@
Impossible d'envoyer un fichier.
Url du serveur non configurée.
+
+ isComposing
+ %1 est en train d'écrire...
+
Cli
diff --git a/src/components/chat/ChatModel.cpp b/src/components/chat/ChatModel.cpp
index 6336fb425..d8cc4b869 100644
--- a/src/components/chat/ChatModel.cpp
+++ b/src/components/chat/ChatModel.cpp
@@ -199,6 +199,20 @@ ChatModel::ChatModel (QObject *parent) : QAbstractListModel(parent) {
QObject::connect(mCoreHandlers.get(), &CoreHandlers::messageReceived, this, &ChatModel::handleMessageReceived);
QObject::connect(mCoreHandlers.get(), &CoreHandlers::callStateChanged, this, &ChatModel::handleCallStateChanged);
+
+ // Deal with remote composing.
+ QTimer *timer = new QTimer(this);
+ timer->setInterval(500);
+
+ QObject::connect(timer, &QTimer::timeout, this, [this] {
+ bool isRemoteComposing = mChatRoom->isRemoteComposing();
+ if (isRemoteComposing != mIsRemoteComposing) {
+ mIsRemoteComposing = isRemoteComposing;
+ emit isRemoteComposingChanged(mIsRemoteComposing);
+ }
+ });
+
+ timer->start();
}
ChatModel::~ChatModel () {
@@ -259,7 +273,7 @@ bool ChatModel::removeRows (int row, int count, const QModelIndex &parent) {
QString ChatModel::getSipAddress () const {
if (!mChatRoom)
- return "";
+ return QString("");
return ::Utils::coreStringToAppString(
mChatRoom->getPeerAddress()->asStringUriOnly()
@@ -305,6 +319,10 @@ void ChatModel::setSipAddress (const QString &sipAddress) {
emit sipAddressChanged(sipAddress);
}
+bool ChatModel::getIsRemoteComposing () const {
+ return mIsRemoteComposing;
+}
+
// -----------------------------------------------------------------------------
void ChatModel::removeEntry (int id) {
@@ -473,6 +491,10 @@ bool ChatModel::fileWasDownloaded (int id) {
return entry.second && ::fileWasDownloaded(static_pointer_cast(entry.second));
}
+void ChatModel::compose () {
+ return mChatRoom->compose();
+}
+
// -----------------------------------------------------------------------------
const ChatModel::ChatEntryData ChatModel::getFileMessageEntry (int id) {
diff --git a/src/components/chat/ChatModel.hpp b/src/components/chat/ChatModel.hpp
index ce18ec315..e4132242b 100644
--- a/src/components/chat/ChatModel.hpp
+++ b/src/components/chat/ChatModel.hpp
@@ -38,6 +38,7 @@ class ChatModel : public QAbstractListModel {
Q_OBJECT;
Q_PROPERTY(QString sipAddress READ getSipAddress WRITE setSipAddress NOTIFY sipAddressChanged);
+ Q_PROPERTY(bool isRemoteComposing READ getIsRemoteComposing NOTIFY isRemoteComposingChanged);
public:
enum Roles {
@@ -88,6 +89,8 @@ public:
QString getSipAddress () const;
void setSipAddress (const QString &sipAddress);
+ bool getIsRemoteComposing () const;
+
void removeEntry (int id);
void removeAllEntries ();
@@ -105,8 +108,12 @@ public:
bool fileWasDownloaded (int id);
+ void compose ();
+
signals:
void sipAddressChanged (const QString &sipAddress);
+ bool isRemoteComposingChanged (bool status);
+
void allEntriesRemoved ();
void messageSent (const std::shared_ptr &message);
@@ -133,6 +140,8 @@ private:
void handleCallStateChanged (const std::shared_ptr &call, linphone::CallState state);
void handleMessageReceived (const std::shared_ptr &message);
+ bool mIsRemoteComposing = false;
+
QList mEntries;
std::shared_ptr mChatRoom;
diff --git a/src/components/chat/ChatProxyModel.cpp b/src/components/chat/ChatProxyModel.cpp
index b4fc64a2c..024d58cce 100644
--- a/src/components/chat/ChatProxyModel.cpp
+++ b/src/components/chat/ChatProxyModel.cpp
@@ -69,6 +69,14 @@ ChatProxyModel::ChatProxyModel (QObject *parent) : QSortFilterProxyModel(parent)
ChatModel *chat = static_cast(mChatModelFilter->sourceModel());
+ QObject::connect(chat, &ChatModel::sipAddressChanged, this, [this](const QString &sipAddress) {
+ emit sipAddressChanged(sipAddress);
+ });
+
+ QObject::connect(chat, &ChatModel::isRemoteComposingChanged, this, [this](bool status) {
+ emit isRemoteComposingChanged(status);
+ });
+
QObject::connect(chat, &ChatModel::messageReceived, this, [this](const shared_ptr &) {
mMaxDisplayedEntries++;
});
@@ -88,13 +96,21 @@ ChatProxyModel::ChatProxyModel (QObject *parent) : QSortFilterProxyModel(parent)
); \
}
-#define CREATE_PARENT_MODEL_FUNCTION_PARAM(METHOD, ARG_TYPE) \
+#define CREATE_PARENT_MODEL_FUNCTION(METHOD) \
+ void ChatProxyModel::METHOD() { \
+ static_cast(mChatModelFilter->sourceModel())->METHOD(); \
+ }
+
+#define CREATE_PARENT_MODEL_FUNCTION_WITH_PARAM(METHOD, ARG_TYPE) \
void ChatProxyModel::METHOD(ARG_TYPE value) { \
static_cast(mChatModelFilter->sourceModel())->METHOD(value); \
}
-CREATE_PARENT_MODEL_FUNCTION_PARAM(sendFileMessage, const QString &);
-CREATE_PARENT_MODEL_FUNCTION_PARAM(sendMessage, const QString &);
+CREATE_PARENT_MODEL_FUNCTION(compose);
+CREATE_PARENT_MODEL_FUNCTION(removeAllEntries);
+
+CREATE_PARENT_MODEL_FUNCTION_WITH_PARAM(sendFileMessage, const QString &);
+CREATE_PARENT_MODEL_FUNCTION_WITH_PARAM(sendMessage, const QString &);
CREATE_PARENT_MODEL_FUNCTION_WITH_ID(downloadFile);
CREATE_PARENT_MODEL_FUNCTION_WITH_ID(openFile);
@@ -103,14 +119,11 @@ CREATE_PARENT_MODEL_FUNCTION_WITH_ID(removeEntry);
CREATE_PARENT_MODEL_FUNCTION_WITH_ID(resendMessage);
#undef CREATE_PARENT_MODEL_FUNCTION
+#undef CREATE_PARENT_MODEL_FUNCTION_WITH_PARAM
#undef CREATE_PARENT_MODEL_FUNCTION_WITH_ID
// -----------------------------------------------------------------------------
-void ChatProxyModel::removeAllEntries () {
- static_cast(mChatModelFilter->sourceModel())->removeAllEntries();
-}
-
QString ChatProxyModel::getSipAddress () const {
return static_cast(mChatModelFilter->sourceModel())->getSipAddress();
}
@@ -121,6 +134,10 @@ void ChatProxyModel::setSipAddress (const QString &sipAddress) {
);
}
+bool ChatProxyModel::getIsRemoteComposing () const {
+ return static_cast(mChatModelFilter->sourceModel())->getIsRemoteComposing();
+}
+
// -----------------------------------------------------------------------------
void ChatProxyModel::loadMoreEntries () {
diff --git a/src/components/chat/ChatProxyModel.hpp b/src/components/chat/ChatProxyModel.hpp
index 042babaab..1385f96c6 100644
--- a/src/components/chat/ChatProxyModel.hpp
+++ b/src/components/chat/ChatProxyModel.hpp
@@ -35,6 +35,7 @@ class ChatProxyModel : public QSortFilterProxyModel {
Q_OBJECT;
Q_PROPERTY(QString sipAddress READ getSipAddress WRITE setSipAddress NOTIFY sipAddressChanged);
+ Q_PROPERTY(bool isRemoteComposing READ getIsRemoteComposing NOTIFY isRemoteComposingChanged);
public:
ChatProxyModel (QObject *parent = Q_NULLPTR);
@@ -54,8 +55,12 @@ public:
Q_INVOKABLE void openFile (int id);
Q_INVOKABLE void openFileDirectory (int id);
+ Q_INVOKABLE void compose ();
+
signals:
void sipAddressChanged (const QString &sipAddress);
+ bool isRemoteComposingChanged (bool status);
+
void moreEntriesLoaded (int n);
void entryTypeFilterChanged (ChatModel::EntryType type);
@@ -67,6 +72,8 @@ private:
QString getSipAddress () const;
void setSipAddress (const QString &sipAddress);
+ bool getIsRemoteComposing () const;
+
ChatModelFilter *mChatModelFilter;
int mMaxDisplayedEntries = ENTRIES_CHUNK_SIZE;
diff --git a/ui/modules/Linphone/Chat/Chat.js b/ui/modules/Linphone/Chat/Chat.js
index c2fe755f3..23194335d 100644
--- a/ui/modules/Linphone/Chat/Chat.js
+++ b/ui/modules/Linphone/Chat/Chat.js
@@ -2,6 +2,12 @@
// `Chat.qml` Logic.
// =============================================================================
+.import Linphone 1.0 as Linphone
+
+.import 'qrc:/ui/scripts/LinphoneUtils/linphone-utils.js' as LinphoneUtils
+
+// =============================================================================
+
function initView () {
chat.tryToLoadMoreEntries = false
chat.bindToEnd = true
@@ -11,7 +17,7 @@ function loadMoreEntries () {
if (chat.atYBeginning && !chat.tryToLoadMoreEntries) {
chat.tryToLoadMoreEntries = true
chat.positionViewAtBeginning()
- proxyModel.loadMoreEntries()
+ container.proxyModel.loadMoreEntries()
}
}
@@ -20,16 +26,28 @@ function getComponentFromEntry (chatEntry) {
return 'FileMessage.qml'
}
- if (chatEntry.type === ChatModel.CallEntry) {
+ if (chatEntry.type === Linphone.ChatModel.CallEntry) {
return 'Event.qml'
}
return chatEntry.isOutgoing ? 'OutgoingMessage.qml' : 'IncomingMessage.qml'
}
+function getIsComposingMessage () {
+ if (!container.proxyModel.isRemoteComposing) {
+ return ''
+ }
+
+ var sipAddressObserver = chat.sipAddressObserver
+ return qsTr('isComposing').replace(
+ '%1',
+ LinphoneUtils.getContactUsername(sipAddressObserver.contact || sipAddressObserver.sipAddress)
+ )
+}
+
function handleFilesDropped (files) {
chat.bindToEnd = true
- files.forEach(proxyModel.sendFileMessage)
+ files.forEach(container.proxyModel.sendFileMessage)
}
function handleMoreEntriesLoaded (n) {
@@ -47,8 +65,12 @@ function handleMovementStarted () {
chat.bindToEnd = false
}
+function handleTextChanged () {
+ container.proxyModel.compose()
+}
+
function sendMessage (text) {
textArea.text = ''
chat.bindToEnd = true
- proxyModel.sendMessage(text)
+ container.proxyModel.sendMessage(text)
}
diff --git a/ui/modules/Linphone/Chat/Chat.qml b/ui/modules/Linphone/Chat/Chat.qml
index 099f3563b..d5747b0e4 100644
--- a/ui/modules/Linphone/Chat/Chat.qml
+++ b/ui/modules/Linphone/Chat/Chat.qml
@@ -11,6 +11,8 @@ import 'Chat.js' as Logic
// =============================================================================
Rectangle {
+ id: container
+
property alias proxyModel: chat.model
// ---------------------------------------------------------------------------
@@ -196,6 +198,8 @@ Rectangle {
Layout.preferredHeight: ChatStyle.sendArea.height + ChatStyle.sendArea.border.width
borderColor: ChatStyle.sendArea.border.color
+
+ bottomWidth: ChatStyle.sendArea.border.width
topWidth: ChatStyle.sendArea.border.width
DroppableTextArea {
@@ -208,9 +212,20 @@ Rectangle {
placeholderText: qsTr('newMessagePlaceholder')
onDropped: Logic.handleFilesDropped(files)
+ onTextChanged: Logic.handleTextChanged(text)
onValidText: Logic.sendMessage(text)
}
}
+
+ Text {
+ Layout.fillWidth: true
+
+ color: ChatStyle.composingText.color
+ font.pointSize: ChatStyle.composingText.pointSize
+ leftPadding: ChatStyle.composingText.leftPadding
+
+ text: Logic.getIsComposingMessage()
+ }
}
// ---------------------------------------------------------------------------
diff --git a/ui/modules/Linphone/Styles/Chat/ChatStyle.qml b/ui/modules/Linphone/Styles/Chat/ChatStyle.qml
index 8add96be1..2fac6eca8 100644
--- a/ui/modules/Linphone/Styles/Chat/ChatStyle.qml
+++ b/ui/modules/Linphone/Styles/Chat/ChatStyle.qml
@@ -33,6 +33,12 @@ QtObject {
}
}
+ property QtObject composingText: QtObject {
+ property color color: Colors.b
+ property int pointSize: Units.dp * 9
+ property int leftPadding: 6
+ }
+
property QtObject entry: QtObject {
property int bottomMargin: 10
property int deleteIconSize: 17
@@ -101,7 +107,6 @@ QtObject {
property QtObject images: QtObject {
property int height: 48
- // `width` can be used.
}
property QtObject incoming: QtObject {