diff --git a/tests/assets/images/chat_amount.svg b/tests/assets/images/chat_amount.svg new file mode 100644 index 000000000..ff60f7d77 --- /dev/null +++ b/tests/assets/images/chat_amount.svg @@ -0,0 +1,18 @@ + + + + statut_avatar_busy_l + Created with Sketch. + + + + + + + + + + + + + diff --git a/tests/assets/images/chat_count.svg b/tests/assets/images/chat_count.svg new file mode 100644 index 000000000..4045488a6 --- /dev/null +++ b/tests/assets/images/chat_count.svg @@ -0,0 +1,14 @@ + + + + chat_count + Created with Sketch. + + + + + + + + + diff --git a/tests/resources.qrc b/tests/resources.qrc index 1e43911b4..5b7e9f2ce 100644 --- a/tests/resources.qrc +++ b/tests/resources.qrc @@ -24,6 +24,8 @@ assets/images/camera_on_hovered.svg assets/images/camera_on_normal.svg assets/images/camera_on_pressed.svg + assets/images/chat_amount.svg + assets/images/chat_count.svg assets/images/chat_error.svg assets/images/chat_hovered.svg assets/images/chat_normal.svg @@ -208,6 +210,7 @@ ui/modules/Linphone/Contact/Avatar.qml ui/modules/Linphone/Contact/ContactDescription.qml ui/modules/Linphone/Contact/Contact.qml + ui/modules/Linphone/Contact/MessagesCounter.qml ui/modules/Linphone/Notifications/CallNotification.qml ui/modules/Linphone/Notifications/Notification.qml ui/modules/Linphone/Notifications/ReceivedMessageNotification.qml @@ -220,6 +223,7 @@ ui/modules/Linphone/Styles/Contact/AvatarStyle.qml ui/modules/Linphone/Styles/Contact/ContactDescriptionStyle.qml ui/modules/Linphone/Styles/Contact/ContactStyle.qml + ui/modules/Linphone/Styles/Contact/MessagesCounterStyle.qml ui/modules/Linphone/Styles/NotificationStyle.qml ui/modules/Linphone/Styles/Presence/PresenceStringStyle.qml ui/modules/Linphone/Styles/qmldir diff --git a/tests/src/components/chat/ChatModel.cpp b/tests/src/components/chat/ChatModel.cpp index fda7dba23..f605ca3c3 100644 --- a/tests/src/components/chat/ChatModel.cpp +++ b/tests/src/components/chat/ChatModel.cpp @@ -78,11 +78,11 @@ ChatModel::ChatModel (QObject *parent) : QAbstractListModel(parent) { CoreManager::getInstance()->getSipAddressesModel()->connectToChatModel(this); QObject::connect( - &(*m_core_handlers), &CoreHandlers::receivedMessage, + &(*m_core_handlers), &CoreHandlers::messageReceived, this, [this](const shared_ptr &message) { if (m_chat_room == message->getChatRoom()) { insertMessageAtEnd(message); - m_chat_room->markAsRead(); + resetMessagesCount(); emit messageReceived(message); } @@ -165,10 +165,11 @@ void ChatModel::setSipAddress (const QString &sip_address) { m_entries.clear(); shared_ptr core = CoreManager::getInstance()->getCore(); - string std_sip_address = ::Utils::qStringToLinphoneString(sip_address); - m_chat_room = core->getChatRoomFromUri(std_sip_address); - m_chat_room->markAsRead(); + m_chat_room = core->getChatRoomFromUri(::Utils::qStringToLinphoneString(sip_address)); + + if (m_chat_room->getUnreadMessagesCount() > 0) + resetMessagesCount(); // Get messages. for (auto &message : m_chat_room->getHistory(0)) { @@ -336,3 +337,8 @@ void ChatModel::insertMessageAtEnd (const shared_ptr &mes endInsertRows(); } + +void ChatModel::resetMessagesCount () { + m_chat_room->markAsRead(); + emit messagesCountReset(); +} diff --git a/tests/src/components/chat/ChatModel.hpp b/tests/src/components/chat/ChatModel.hpp index 888db62c4..0ba1d300f 100644 --- a/tests/src/components/chat/ChatModel.hpp +++ b/tests/src/components/chat/ChatModel.hpp @@ -75,6 +75,8 @@ signals: void messageSent (const std::shared_ptr &message); void messageReceived (const std::shared_ptr &message); + void messagesCountReset (); + private: void fillMessageEntry ( QVariantMap &dest, @@ -95,6 +97,8 @@ private: void insertMessageAtEnd (const std::shared_ptr &message); + void resetMessagesCount (); + QList m_entries; std::shared_ptr m_chat_room; diff --git a/tests/src/components/chat/ChatProxyModel.cpp b/tests/src/components/chat/ChatProxyModel.cpp index 80567244d..257863c3e 100644 --- a/tests/src/components/chat/ChatProxyModel.cpp +++ b/tests/src/components/chat/ChatProxyModel.cpp @@ -2,33 +2,48 @@ // ============================================================================= -ChatModelFilter::ChatModelFilter (QObject *parent) : QSortFilterProxyModel(parent) { - setSourceModel(&m_chat_model); -} +// Fetch the L last filtered chat entries. +class ChatProxyModel::ChatModelFilter : public QSortFilterProxyModel { +public: + ChatModelFilter (QObject *parent) : QSortFilterProxyModel(parent) { + setSourceModel(&m_chat_model); + } -bool ChatModelFilter::filterAcceptsRow (int source_row, const QModelIndex &) const { - if (m_entry_type_filter == ChatModel::EntryType::GenericEntry) - return true; + ChatModel::EntryType getEntryTypeFilter () { + return m_entry_type_filter; + } - QModelIndex index = sourceModel()->index(source_row, 0, QModelIndex()); - const QVariantMap &data = index.data().toMap(); + void setEntryTypeFilter (ChatModel::EntryType type) { + m_entry_type_filter = type; + invalidate(); + } - return data["type"].toInt() == m_entry_type_filter; -} +protected: + bool filterAcceptsRow (int source_row, const QModelIndex &) const override { + if (m_entry_type_filter == ChatModel::EntryType::GenericEntry) + return true; -void ChatModelFilter::setEntryTypeFilter (ChatModel::EntryType type) { - m_entry_type_filter = type; - invalidate(); -} + QModelIndex index = sourceModel()->index(source_row, 0, QModelIndex()); + const QVariantMap &data = index.data().toMap(); + + return data["type"].toInt() == m_entry_type_filter; + } + +private: + ChatModel m_chat_model; + ChatModel::EntryType m_entry_type_filter = ChatModel::EntryType::GenericEntry; +}; // ============================================================================= const unsigned int ChatProxyModel::ENTRIES_CHUNK_SIZE = 50; ChatProxyModel::ChatProxyModel (QObject *parent) : QSortFilterProxyModel(parent) { - setSourceModel(&m_chat_model_filter); + m_chat_model_filter = new ChatModelFilter(this); - ChatModel *chat = static_cast(m_chat_model_filter.sourceModel()); + setSourceModel(m_chat_model_filter); + + ChatModel *chat = static_cast(m_chat_model_filter->sourceModel()); QObject::connect( chat, &ChatModel::messageReceived, this, [this](const shared_ptr &) { @@ -45,7 +60,7 @@ ChatProxyModel::ChatProxyModel (QObject *parent) : QSortFilterProxyModel(parent) void ChatProxyModel::loadMoreEntries () { int count = rowCount(); - int parent_count = m_chat_model_filter.rowCount(); + int parent_count = m_chat_model_filter->rowCount(); if (count < parent_count) { // Do not increase `m_n_max_displayed_entries` if it's not necessary... @@ -62,19 +77,37 @@ void ChatProxyModel::loadMoreEntries () { } void ChatProxyModel::setEntryTypeFilter (ChatModel::EntryType type) { - if (m_chat_model_filter.getEntryTypeFilter() != type) { - m_chat_model_filter.setEntryTypeFilter(type); + if (m_chat_model_filter->getEntryTypeFilter() != type) { + m_chat_model_filter->setEntryTypeFilter(type); emit entryTypeFilterChanged(type); } } void ChatProxyModel::removeEntry (int id) { QModelIndex source_index = mapToSource(index(id, 0)); - static_cast(m_chat_model_filter.sourceModel())->removeEntry( - m_chat_model_filter.mapToSource(source_index).row() + static_cast(m_chat_model_filter->sourceModel())->removeEntry( + m_chat_model_filter->mapToSource(source_index).row() ); } -bool ChatProxyModel::filterAcceptsRow (int source_row, const QModelIndex &) const { - return m_chat_model_filter.rowCount() - source_row <= m_n_max_displayed_entries; +void ChatProxyModel::removeAllEntries () { + static_cast(m_chat_model_filter->sourceModel())->removeAllEntries(); +} + +void ChatProxyModel::sendMessage (const QString &message) { + static_cast(m_chat_model_filter->sourceModel())->sendMessage(message); +} + +bool ChatProxyModel::filterAcceptsRow (int source_row, const QModelIndex &) const { + return m_chat_model_filter->rowCount() - source_row <= m_n_max_displayed_entries; +} + +QString ChatProxyModel::getSipAddress () const { + return static_cast(m_chat_model_filter->sourceModel())->getSipAddress(); +} + +void ChatProxyModel::setSipAddress (const QString &sip_address) { + static_cast(m_chat_model_filter->sourceModel())->setSipAddress( + sip_address + ); } diff --git a/tests/src/components/chat/ChatProxyModel.hpp b/tests/src/components/chat/ChatProxyModel.hpp index 4b6f5ece0..dbee58dd0 100644 --- a/tests/src/components/chat/ChatProxyModel.hpp +++ b/tests/src/components/chat/ChatProxyModel.hpp @@ -5,35 +5,11 @@ #include "ChatModel.hpp" -// ============================================================================= -// Fetch the L last filtered chat entries. -// ============================================================================= - -// Cannot be used as a nested class by Qt and `Q_OBJECT` macro -// must be used in header c++ file. -class ChatModelFilter : public QSortFilterProxyModel { - Q_OBJECT; - -public: - ChatModelFilter (QObject *parent = Q_NULLPTR); - - ChatModel::EntryType getEntryTypeFilter () { - return m_entry_type_filter; - } - - void setEntryTypeFilter (ChatModel::EntryType type); - -protected: - bool filterAcceptsRow (int source_row, const QModelIndex &parent) const override; - -private: - ChatModel m_chat_model; - ChatModel::EntryType m_entry_type_filter = ChatModel::EntryType::GenericEntry; -}; - // ============================================================================= class ChatProxyModel : public QSortFilterProxyModel { + class ChatModelFilter; + Q_OBJECT; Q_PROPERTY( @@ -50,16 +26,12 @@ public: Q_INVOKABLE void setEntryTypeFilter (ChatModel::EntryType type); Q_INVOKABLE void removeEntry (int id); - Q_INVOKABLE void removeAllEntries () { - static_cast(m_chat_model_filter.sourceModel())->removeAllEntries(); - } + Q_INVOKABLE void removeAllEntries (); - Q_INVOKABLE void sendMessage (const QString &message) { - static_cast(m_chat_model_filter.sourceModel())->sendMessage(message); - } + Q_INVOKABLE void sendMessage (const QString &message); signals: - void sipAddressChanged (const QString &sipAddress); + void sipAddressChanged (const QString &sip_address); void moreEntriesLoaded (int n); void entryTypeFilterChanged (ChatModel::EntryType type); @@ -68,17 +40,10 @@ protected: bool filterAcceptsRow (int source_row, const QModelIndex &source_parent) const override; private: - QString getSipAddress () const { - return static_cast(m_chat_model_filter.sourceModel())->getSipAddress(); - } + QString getSipAddress () const; + void setSipAddress (const QString &sip_address); - void setSipAddress (const QString &sip_address) { - static_cast(m_chat_model_filter.sourceModel())->setSipAddress( - sip_address - ); - } - - ChatModelFilter m_chat_model_filter; + ChatModelFilter *m_chat_model_filter; int m_n_max_displayed_entries = ENTRIES_CHUNK_SIZE; static const unsigned int ENTRIES_CHUNK_SIZE; diff --git a/tests/src/components/core/CoreHandlers.cpp b/tests/src/components/core/CoreHandlers.cpp index 33e19df5c..d7e8a13dc 100644 --- a/tests/src/components/core/CoreHandlers.cpp +++ b/tests/src/components/core/CoreHandlers.cpp @@ -31,7 +31,7 @@ void CoreHandlers::onMessageReceived ( const shared_ptr &room, const shared_ptr &message ) { - emit receivedMessage(message); + emit messageReceived(message); const App *app = App::getInstance(); if (!app->hasFocus()) diff --git a/tests/src/components/core/CoreHandlers.hpp b/tests/src/components/core/CoreHandlers.hpp index 916faa0ba..d1bb63cde 100644 --- a/tests/src/components/core/CoreHandlers.hpp +++ b/tests/src/components/core/CoreHandlers.hpp @@ -12,7 +12,7 @@ class CoreHandlers : Q_OBJECT; signals: - void receivedMessage (const std::shared_ptr &message); + void messageReceived (const std::shared_ptr &message); private: void onAuthenticationRequested ( diff --git a/tests/src/components/sip-addresses/SipAddressesModel.cpp b/tests/src/components/sip-addresses/SipAddressesModel.cpp index d9cdd87de..6474adcac 100644 --- a/tests/src/components/sip-addresses/SipAddressesModel.cpp +++ b/tests/src/components/sip-addresses/SipAddressesModel.cpp @@ -21,7 +21,7 @@ SipAddressesModel::SipAddressesModel (QObject *parent) : QAbstractListModel(pare m_handlers = CoreManager::getInstance()->getHandlers(); QObject::connect( - &(*m_handlers), &CoreHandlers::receivedMessage, + &(*m_handlers), &CoreHandlers::messageReceived, this, [this](const std::shared_ptr &message) { const QString &sip_address = ::Utils::linphoneStringToQString(message->getFromAddress()->asStringUriOnly()); addOrUpdateSipAddress(sip_address, nullptr, message); @@ -104,13 +104,33 @@ void SipAddressesModel::connectToChatModel (ChatModel *chat_model) { } ); + for (auto &signal : { &ChatModel::messageSent, &ChatModel::messageReceived }) { + QObject::connect( + chat_model, signal, + this, [this](const std::shared_ptr &message) { + addOrUpdateSipAddress( + ::Utils::linphoneStringToQString(message->getToAddress()->asStringUriOnly()), nullptr, message + ); + } + ); + } + QObject::connect( - chat_model, &ChatModel::messageSent, - this, [this](const std::shared_ptr &message) { - addOrUpdateSipAddress( - ::Utils::linphoneStringToQString(message->getToAddress()->asStringUriOnly()), nullptr, message - ); - }); + chat_model, &ChatModel::messagesCountReset, this, [this, chat_model]() { + const QString &sip_address = chat_model->getSipAddress(); + + auto it = m_sip_addresses.find(sip_address); + if (it != m_sip_addresses.end()) { + (*it)["unreadMessagesCount"] = 0; + + int row = m_refs.indexOf(&(*it)); + Q_ASSERT(row != -1); + emit dataChanged(index(row, 0), index(row, 0)); + + return; + } + } + ); } // ----------------------------------------------------------------------------- diff --git a/tests/ui/modules/Linphone/Contact/Contact.qml b/tests/ui/modules/Linphone/Contact/Contact.qml index 7d653b032..ac5be8627 100644 --- a/tests/ui/modules/Linphone/Contact/Contact.qml +++ b/tests/ui/modules/Linphone/Contact/Contact.qml @@ -1,11 +1,9 @@ import QtQuick 2.7 import QtQuick.Layouts 1.3 -import Common 1.0 import Linphone 1.0 import LinphoneUtils 1.0 import Linphone.Styles 1.0 -import Utils 1.0 // ============================================================================= @@ -14,10 +12,11 @@ Rectangle { // --------------------------------------------------------------------------- - property alias actions: actionBar.data property alias sipAddressColor: description.sipAddressColor property alias usernameColor: description.usernameColor + property bool displayUnreadMessagesCount: false + property var entry property var _contact: entry.contact @@ -33,7 +32,7 @@ Rectangle { leftMargin: ContactStyle.leftMargin rightMargin: ContactStyle.rightMargin } - spacing: ContactStyle.spacing + spacing: 0 Avatar { id: avatar @@ -50,14 +49,17 @@ Rectangle { Layout.fillHeight: true Layout.fillWidth: true + Layout.leftMargin: ContactStyle.spacing + sipAddress: entry.sipAddress username: avatar.username } - ActionBar { - id: actionBar + MessagesCounter { + Layout.alignment: Qt.AlignTop - Layout.preferredHeight: ContactStyle.contentHeight + count: entry.unreadMessagesCount || 0 + visible: displayUnreadMessagesCount && entry.unreadMessagesCount > 0 } } } diff --git a/tests/ui/modules/Linphone/Contact/MessagesCounter.qml b/tests/ui/modules/Linphone/Contact/MessagesCounter.qml new file mode 100644 index 000000000..eb019ed36 --- /dev/null +++ b/tests/ui/modules/Linphone/Contact/MessagesCounter.qml @@ -0,0 +1,40 @@ +import QtQuick 2.7 +import QtQuick.Layouts 1.3 + +import Common 1.0 +import Linphone.Styles 1.0 + +// ============================================================================= + +Item { + id: messagesCounter + + property int count + + implicitHeight: counterIcon.height + MessagesCounterStyle.verticalMargins * 2 + implicitWidth: counterIcon.width + MessagesCounterStyle.horizontalMargins * 2 + + Icon { + id: counterIcon + + anchors.centerIn: parent + + icon: 'chat_count' + iconSize: MessagesCounterStyle.iconSize.message + + Icon { + anchors.horizontalCenter: parent.right + anchors.verticalCenter: parent.bottom + + icon: 'chat_amount' + iconSize: MessagesCounterStyle.iconSize.amount + + Text { + anchors.centerIn: parent + color: MessagesCounterStyle.text.color + font.pointSize: MessagesCounterStyle.text.fontSize + text: messagesCounter.count + } + } + } +} diff --git a/tests/ui/modules/Linphone/Styles/Contact/MessagesCounterStyle.qml b/tests/ui/modules/Linphone/Styles/Contact/MessagesCounterStyle.qml new file mode 100644 index 000000000..ff0763df4 --- /dev/null +++ b/tests/ui/modules/Linphone/Styles/Contact/MessagesCounterStyle.qml @@ -0,0 +1,21 @@ +pragma Singleton +import QtQuick 2.7 + +import Common 1.0 + +// ============================================================================= + +QtObject { + property int horizontalMargins: 0 + property int verticalMargins: 10 + + property QtObject iconSize: QtObject { + property int amount: 16 + property int message: 18 + } + + property QtObject text: QtObject { + property color color: Colors.k + property int fontSize: 7 + } +} diff --git a/tests/ui/modules/Linphone/Styles/qmldir b/tests/ui/modules/Linphone/Styles/qmldir index 5ae24cc2f..9b23e21cf 100644 --- a/tests/ui/modules/Linphone/Styles/qmldir +++ b/tests/ui/modules/Linphone/Styles/qmldir @@ -11,6 +11,7 @@ singleton ChatStyle 1.0 ChatStyle.qml singleton AvatarStyle 1.0 Contact/AvatarStyle.qml singleton ContactDescriptionStyle 1.0 Contact/ContactDescriptionStyle.qml singleton ContactStyle 1.0 Contact/ContactStyle.qml +singleton MessagesCounterStyle 1.0 Contact/MessagesCounterStyle.qml singleton NotificationStyle 1.0 NotificationStyle.qml diff --git a/tests/ui/modules/Linphone/Timeline.qml b/tests/ui/modules/Linphone/Timeline.qml index 79f3fdb9f..5cdf09fb5 100644 --- a/tests/ui/modules/Linphone/Timeline.qml +++ b/tests/ui/modules/Linphone/Timeline.qml @@ -113,6 +113,7 @@ ColumnLayout { ? TimelineStyle.contact.backgroundColor.a : TimelineStyle.contact.backgroundColor.b ) + displayUnreadMessagesCount: view.currentIndex !== index entry: $timelineEntry sipAddressColor: view.currentIndex === index ? TimelineStyle.contact.sipAddress.color.selected