From 0829dd92faa0584da0800421604fd7c842af066d Mon Sep 17 00:00:00 2001 From: Wescoeur Date: Mon, 26 Jun 2017 16:10:50 +0200 Subject: [PATCH] feat(Chat): supports messages composing --- assets/languages/en.ts | 4 +++ assets/languages/fr.ts | 4 +++ src/components/chat/ChatModel.cpp | 24 +++++++++++++- src/components/chat/ChatModel.hpp | 9 ++++++ src/components/chat/ChatProxyModel.cpp | 31 ++++++++++++++----- src/components/chat/ChatProxyModel.hpp | 7 +++++ ui/modules/Linphone/Chat/Chat.js | 30 +++++++++++++++--- ui/modules/Linphone/Chat/Chat.qml | 15 +++++++++ ui/modules/Linphone/Styles/Chat/ChatStyle.qml | 7 ++++- 9 files changed, 118 insertions(+), 13 deletions(-) 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 {