From 33f382d80a3ef03737e6d234dd66775f500b751d Mon Sep 17 00:00:00 2001 From: Julien Wadel Date: Fri, 28 Jan 2022 16:14:57 +0100 Subject: [PATCH] - Shortcut in Reply to message's origin. - Avoid to load huge chat room at the start of a call. - Asynchronous chat room load. - Fix video freeze on network change. --- CHANGELOG.md | 7 +- .../components/chat-room/ChatRoomModel.cpp | 38 +++++++- .../components/chat-room/ChatRoomModel.hpp | 5 +- .../chat-room/ChatRoomProxyModel.cpp | 93 ++++++++----------- .../chat-room/ChatRoomProxyModel.hpp | 7 +- .../src/components/timeline/TimelineModel.cpp | 1 - .../ui/modules/Linphone/Chat/Chat.qml | 53 +++++++---- .../Linphone/Chat/ChatReplyMessage.qml | 9 ++ .../modules/Linphone/Chat/IncomingMessage.qml | 2 + .../ui/modules/Linphone/Chat/Message.qml | 2 + .../modules/Linphone/Chat/OutgoingMessage.qml | 2 + linphone-sdk | 2 +- 12 files changed, 140 insertions(+), 81 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7e2ecb348..5ae844d3b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,13 +8,13 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added - Features: - * messages features : Reply, forward (to contact, to a SIP address or to a timeline), Vocal record and play, multi contents. - + * Messages features : Reply, forward (to contact, to a SIP address or to a timeline), Vocal record and play, multi contents. - Add a feedback on fetching remote provisioning when it failed. - Option to enable message notifications. - CPIM on basic chat rooms. -- New event on new messages in chat and a shortcut to go to the end of chat if last message is not shown. - Device name can be changed from settings. +- New event on new messages in chat and a shortcut to go to the end of chat if last message is not shown. +- Shortcut in Reply to message's origin. - Based on Linphone SDK 5.1 ### Fixed @@ -25,6 +25,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Take account of return key on Numpad - Huge messages are better shown and with less flickering. - Adapt UserAgent with device name. +- Video freeze on network change. ## 4.3.2 diff --git a/linphone-app/src/components/chat-room/ChatRoomModel.cpp b/linphone-app/src/components/chat-room/ChatRoomModel.cpp index 92534bbb8..74de4fc8e 100644 --- a/linphone-app/src/components/chat-room/ChatRoomModel.cpp +++ b/linphone-app/src/components/chat-room/ChatRoomModel.cpp @@ -881,12 +881,48 @@ void ChatRoomModel::updateNewMessageNotice(const int& count){ beginInsertRows(QModelIndex(), 0, 0); mEntries.prepend(mUnreadMessageNotice); endInsertRows(); - qWarning() << "New message notice timestamp to :" << lastUnreadMessage.toString(); + qDebug() << "New message notice timestamp to :" << lastUnreadMessage.toString(); } //emit layoutChanged(); } } +int ChatRoomModel::loadTillMessage(ChatMessageModel * message){ + if( message){ + qDebug() << "Load history till message : " << message->getChatMessage()->getMessageId().c_str(); + auto linphoneMessage = message->getChatMessage(); + // First find on current list + auto entry = std::find_if(mEntries.begin(), mEntries.end(), [linphoneMessage](const std::shared_ptr& entry ){ + return entry->mType == ChatRoomModel::EntryType::MessageEntry && dynamic_cast(entry.get())->getChatMessage() == linphoneMessage; + }); + // if not find, load more entries and find it in new entries. + if( entry == mEntries.end()){ + int newEntries = loadMoreEntries(); + while( newEntries > 0){// no more new entries + int entryCount = 0; + entry = mEntries.begin(); + while(entryCount < newEntries && + ((*entry)->mType != ChatRoomModel::EntryType::MessageEntry || dynamic_cast(entry->get())->getChatMessage() != linphoneMessage) + ){ + ++entryCount; + ++entry; + } + if( entryCount < newEntries){// We got it + qDebug() << "Find message at " << entryCount << " after loading new entries"; + return entryCount; + }else + newEntries = loadMoreEntries();// continue + } + }else{ + int entryCount = entry - mEntries.begin(); + qDebug() << "Find message at " << entryCount; + return entryCount; + } + qWarning() << "Message has not been found in history"; + } + return -1; +} + void ChatRoomModel::initEntries(){ qDebug() << "Internal Entries : Init"; // On call : reinitialize all entries. This allow to free up memory diff --git a/linphone-app/src/components/chat-room/ChatRoomModel.hpp b/linphone-app/src/components/chat-room/ChatRoomModel.hpp index 81c62ceb5..86b2e4f29 100644 --- a/linphone-app/src/components/chat-room/ChatRoomModel.hpp +++ b/linphone-app/src/components/chat-room/ChatRoomModel.hpp @@ -97,7 +97,6 @@ signals: void chatMessageShouldBeStored(const std::shared_ptr & chatRoom, const std::shared_ptr & message); void chatMessageParticipantImdnStateChanged(const std::shared_ptr & chatRoom, const std::shared_ptr & message, const std::shared_ptr & state); - }; class ChatRoomModel : public QAbstractListModel { @@ -119,9 +118,6 @@ public: }; Q_ENUM(EntryType) - - //Q_PROPERTY(QString participants READ getParticipants NOTIFY participantsChanged); - //Q_PROPERTY(ParticipantProxyModel participants READ getParticipants NOTIFY participantsChanged); Q_PROPERTY(QString subject READ getSubject WRITE setSubject NOTIFY subjectChanged) Q_PROPERTY(QDateTime lastUpdateTime MEMBER mLastUpdateTime WRITE setLastUpdateTime NOTIFY lastUpdateTimeChanged) Q_PROPERTY(int unreadMessagesCount MEMBER mUnreadMessagesCount WRITE setUnreadMessagesCount NOTIFY unreadMessagesCountChanged) @@ -238,6 +234,7 @@ public: Q_INVOKABLE int loadMoreEntries(); // return new entries count void callEnded(std::shared_ptr call); void updateNewMessageNotice(const int& count); + Q_INVOKABLE int loadTillMessage(ChatMessageModel * message);// Load all entries till message and return its index. -1 if not found. QDateTime mLastUpdateTime; int mUnreadMessagesCount = 0; diff --git a/linphone-app/src/components/chat-room/ChatRoomProxyModel.cpp b/linphone-app/src/components/chat-room/ChatRoomProxyModel.cpp index 872746b24..7eb98792f 100644 --- a/linphone-app/src/components/chat-room/ChatRoomProxyModel.cpp +++ b/linphone-app/src/components/chat-room/ChatRoomProxyModel.cpp @@ -38,47 +38,10 @@ using namespace std; QString ChatRoomProxyModel::gCachedText; -// Fetch the L last filtered chat entries. -class ChatRoomProxyModel::ChatRoomModelFilter : public QSortFilterProxyModel { -public: - ChatRoomModelFilter (QObject *parent) : QSortFilterProxyModel(parent) {} - - int getEntryTypeFilter () { - return mEntryTypeFilter; - } - - void setEntryTypeFilter (int type) { - mEntryTypeFilter = type; - invalidate(); - } - -protected: - bool filterAcceptsRow (int sourceRow, const QModelIndex &) const override { - if (mEntryTypeFilter == ChatRoomModel::EntryType::GenericEntry) - return true; - - QModelIndex index = sourceModel()->index(sourceRow, 0, QModelIndex()); - auto eventModel = sourceModel()->data(index); - - if( mEntryTypeFilter == ChatRoomModel::EntryType::CallEntry && eventModel.value() != nullptr) - return true; - if( mEntryTypeFilter == ChatRoomModel::EntryType::MessageEntry && eventModel.value() != nullptr) - return true; - if( mEntryTypeFilter == ChatRoomModel::EntryType::NoticeEntry && eventModel.value() != nullptr) - return true; - return false; - } - -private: - int mEntryTypeFilter = ChatRoomModel::EntryType::GenericEntry; -}; - // ============================================================================= ChatRoomProxyModel::ChatRoomProxyModel (QObject *parent) : QSortFilterProxyModel(parent) { - setSourceModel(new ChatRoomModelFilter(this)); mMarkAsReadEnabled = true; - //mIsSecure = false; App *app = App::getInstance(); QObject::connect(app->getMainWindow(), &QWindow::activeChanged, this, [this]() { @@ -109,13 +72,12 @@ ChatRoomProxyModel::ChatRoomProxyModel (QObject *parent) : QSortFilterProxyModel void ChatRoomProxyModel::METHOD (ARG_TYPE value) { \ GET_CHAT_MODEL()->METHOD(value); \ } - + #define CREATE_PARENT_MODEL_FUNCTION_WITH_ID(METHOD) \ void ChatRoomProxyModel::METHOD (int id) { \ - QModelIndex sourceIndex = mapToSource(index(id, 0)); \ - GET_CHAT_MODEL()->METHOD( \ - static_cast(sourceModel())->mapToSource(sourceIndex).row() \ - ); \ + GET_CHAT_MODEL()->METHOD( \ + mapFromSource(static_cast(sourceModel())->index(id, 0)).row() \ + ); \ } CREATE_PARENT_MODEL_FUNCTION(removeAllEntries) @@ -139,6 +101,10 @@ void ChatRoomProxyModel::compose (const QString& text) { gCachedText = text; } +int ChatRoomProxyModel::getEntryTypeFilter () { + return mEntryTypeFilter; +} + // ----------------------------------------------------------------------------- void ChatRoomProxyModel::loadMoreEntriesAsync(){ @@ -155,10 +121,9 @@ void ChatRoomProxyModel::loadMoreEntries() { } void ChatRoomProxyModel::setEntryTypeFilter (int type) { - ChatRoomModelFilter *ChatRoomModelFilter = static_cast(sourceModel()); - - if (ChatRoomModelFilter->getEntryTypeFilter() != type) { - ChatRoomModelFilter->setEntryTypeFilter(type); + if (getEntryTypeFilter() != type) { + mEntryTypeFilter = type; + invalidate(); emit entryTypeFilterChanged(type); } } @@ -167,7 +132,21 @@ void ChatRoomProxyModel::setEntryTypeFilter (int type) { bool ChatRoomProxyModel::filterAcceptsRow (int sourceRow, const QModelIndex &sourceParent) const { bool show = false; - if(mFilterText != ""){ + + if (mEntryTypeFilter == ChatRoomModel::EntryType::GenericEntry) + show = true; + else{ + QModelIndex index = sourceModel()->index(sourceRow, 0, QModelIndex()); + auto eventModel = sourceModel()->data(index); + + if( mEntryTypeFilter == ChatRoomModel::EntryType::CallEntry && eventModel.value() != nullptr) + show = true; + else if( mEntryTypeFilter == ChatRoomModel::EntryType::MessageEntry && eventModel.value() != nullptr) + show = true; + else if( mEntryTypeFilter == ChatRoomModel::EntryType::NoticeEntry && eventModel.value() != nullptr) + show = true; + } + if( show && mFilterText != ""){ QModelIndex index = sourceModel()->index(sourceRow, 0, sourceParent); auto eventModel = sourceModel()->data(index); ChatMessageModel * chatModel = eventModel.value(); @@ -175,11 +154,10 @@ bool ChatRoomProxyModel::filterAcceptsRow (int sourceRow, const QModelIndex &sou QRegularExpression search(QRegularExpression::escape(mFilterText), QRegularExpression::CaseInsensitiveOption | QRegularExpression::UseUnicodePropertiesOption); show = chatModel->mContent.contains(search); } - }else - show = true; - + } return show; } + bool ChatRoomProxyModel::lessThan (const QModelIndex &left, const QModelIndex &right) const { auto l = sourceModel()->data(left); auto r = sourceModel()->data(right); @@ -279,7 +257,6 @@ void ChatRoomProxyModel::reload (ChatRoomModel *chatRoomModel) { QObject::disconnect(ChatRoomModel, &ChatRoomModel::moreEntriesLoaded, this, &ChatRoomProxyModel::onMoreEntriesLoaded); } - mChatRoomModel = CoreManager::getInstance()->getTimelineListModel()->getChatRoomModel(chatRoomModel); if (mChatRoomModel) { @@ -290,11 +267,12 @@ void ChatRoomProxyModel::reload (ChatRoomModel *chatRoomModel) { QObject::connect(ChatRoomModel, &ChatRoomModel::messageSent, this, &ChatRoomProxyModel::handleMessageSent); QObject::connect(ChatRoomModel, &ChatRoomModel::markAsReadEnabledChanged, this, &ChatRoomProxyModel::markAsReadEnabledChanged); QObject::connect(ChatRoomModel, &ChatRoomModel::moreEntriesLoaded, this, &ChatRoomProxyModel::onMoreEntriesLoaded); + mChatRoomModel->initEntries();// This way, we don't load huge chat rooms (that lead to freeze GUI) } - - static_cast(sourceModel())->setSourceModel(mChatRoomModel.get()); + setSourceModel(mChatRoomModel.get()); invalidate(); } + void ChatRoomProxyModel::resetMessageCount(){ if( mChatRoomModel){ mChatRoomModel->resetMessageCount(); @@ -314,6 +292,15 @@ void ChatRoomProxyModel::setFilterText(const QString& text){ } } +int ChatRoomProxyModel::loadTillMessage(ChatMessageModel * message){ + int messageIndex = mChatRoomModel->loadTillMessage(message); + if( messageIndex>= 0 ) { + messageIndex = mapFromSource(static_cast(sourceModel())->index(messageIndex, 0)).row(); + } + qDebug() << "Message index from chat room proxy : " << messageIndex; + return messageIndex; +} + ChatRoomModel *ChatRoomProxyModel::getChatRoomModel () const{ return mChatRoomModel.get(); diff --git a/linphone-app/src/components/chat-room/ChatRoomProxyModel.hpp b/linphone-app/src/components/chat-room/ChatRoomProxyModel.hpp index e7d2619ec..f25bff3ce 100644 --- a/linphone-app/src/components/chat-room/ChatRoomProxyModel.hpp +++ b/linphone-app/src/components/chat-room/ChatRoomProxyModel.hpp @@ -52,13 +52,15 @@ class ChatRoomProxyModel : public QSortFilterProxyModel { public: ChatRoomProxyModel (QObject *parent = Q_NULLPTR); + int getEntryTypeFilter (); + Q_INVOKABLE void setEntryTypeFilter (int type); + Q_INVOKABLE QString getDisplayNameComposers()const; Q_INVOKABLE QVariant getAt(int row); Q_INVOKABLE void loadMoreEntriesAsync (); Q_INVOKABLE void loadMoreEntries (); - Q_INVOKABLE void setEntryTypeFilter (int type); Q_INVOKABLE void removeAllEntries (); Q_INVOKABLE void removeRow (int index); @@ -75,6 +77,8 @@ public: Q_INVOKABLE void setFilterText(const QString& text); + Q_INVOKABLE int loadTillMessage(ChatMessageModel * message);// Load all entries till message and return its index in displayed list (-1 if not found) + public slots: void onMoreEntriesLoaded(const int& count); @@ -133,6 +137,7 @@ private: void handleMessageSent (const std::shared_ptr &message); int mMaxDisplayedEntries = EntriesChunkSize; + int mEntryTypeFilter = ChatRoomModel::EntryType::GenericEntry; QString mPeerAddress; QString mLocalAddress; diff --git a/linphone-app/src/components/timeline/TimelineModel.cpp b/linphone-app/src/components/timeline/TimelineModel.cpp index e8fd8a261..29e3cb1ac 100644 --- a/linphone-app/src/components/timeline/TimelineModel.cpp +++ b/linphone-app/src/components/timeline/TimelineModel.cpp @@ -99,7 +99,6 @@ void TimelineModel::setSelected(const bool& selected){ << ", isAdmin:"<< mChatRoomModel->isMeAdmin() << ", canHandleParticipants:"<< mChatRoomModel->canHandleParticipants() << ", hasBeenLeft:" << mChatRoomModel->hasBeenLeft(); - mChatRoomModel->initEntries(); } emit selectedChanged(mSelected); } diff --git a/linphone-app/ui/modules/Linphone/Chat/Chat.qml b/linphone-app/ui/modules/Linphone/Chat/Chat.qml index 87e1bc67f..3ea3e3db0 100644 --- a/linphone-app/ui/modules/Linphone/Chat/Chat.qml +++ b/linphone-app/ui/modules/Linphone/Chat/Chat.qml @@ -31,6 +31,15 @@ Rectangle { color: ChatStyle.color + function positionViewAtIndex(index){ + chat.bindToEnd = false + chat.positionViewAtIndex(index, ListView.Beginning) + } + + function goToMessage(message){ + positionViewAtIndex(container.proxyModel.loadTillMessage(message)) + } + ColumnLayout { anchors.fill: parent spacing: 0 @@ -41,13 +50,15 @@ Rectangle { // ----------------------------------------------------------------------- property bool bindToEnd: false property bool displaying: false - property int loaderCount: 0 - property int readyItems : 0 - property bool loadingLoader: (readyItems != loaderCount) - property bool loadingEntries: container.proxyModel.chatRoomModel.entriesLoading || displaying - property bool tryToLoadMoreEntries: loadingEntries || loadingLoader + property bool loadingEntries: (container.proxyModel.chatRoomModel && container.proxyModel.chatRoomModel.entriesLoading) || displaying + property bool tryToLoadMoreEntries: loadingEntries || remainingLoadersCount>0 property bool isMoving : false // replace moving read-only property to allow using movement signals. +// Load optimizations + property int remainingLoadersCount: 0 + property int syncLoaderBatch: 50 // batch of simultaneous loaders on synchronous mode +//------------------------------------ + onLoadingEntriesChanged: { if( loadingEntries && !displaying) displaying = true @@ -60,10 +71,9 @@ Rectangle { interval: 5000 repeat: false running: false - onTriggered: container.proxyModel.chatRoomModel.resetMessageCount() + onTriggered: if(container.proxyModel.chatRoomModel) container.proxyModel.chatRoomModel.resetMessageCount() } - //property var sipAddressObserver: SipAddressesModel.getSipAddressObserver(proxyModel.fullPeerAddress, proxyModel.fullLocalAddress) - // ----------------------------------------------------------------------- + Layout.fillHeight: true Layout.fillWidth: true @@ -250,16 +260,20 @@ Rectangle { height: (item !== null && typeof(item)!== 'undefined')? item.height: 0 Layout.fillWidth: true source: Logic.getComponentFromEntry($chatEntry) - property int count: 0 - asynchronous: chat.count - count > 100 - onStatusChanged: if( status == Loader.Ready) ++chat.readyItems - Component.onCompleted: count = ++chat.loaderCount - Component.onDestruction: { - --chat.loaderCount - if( status == Loader.Ready) - --chat.readyItems - } + property int loaderIndex: 0 // index of loader from remaining loaders + property int remainingIndex : loaderIndex % ((chat.remainingLoadersCount) / chat.syncLoaderBatch) != 0 // Check loader index to remaining loader. + onRemainingIndexChanged: if( remainingIndex == 0 && asynchronous) asynchronous = false + asynchronous: true + + onStatusChanged: if( status == Loader.Ready) { + remainingIndex = -1 // overwrite to remove signal changed. That way, there is no more binding loops. + --chat.remainingLoadersCount // Loader is ready: remove one from remaining count. + } + + Component.onCompleted: loaderIndex = ++chat.remainingLoadersCount // on new Loader : one more remaining + Component.onDestruction: if( status != Loader.Ready) --chat.remainingLoadersCount // Remove remaining count if not loaded } + Connections{ target: loader.item ignoreUnknownSignals: true @@ -289,6 +303,10 @@ Rectangle { } }) } + + onGoToMessage:{ + container.goToMessage(message) // sometimes, there is no access to chat id (maybe because of cleaning component while loading new items). Use a global intermediate. + } } } } @@ -489,3 +507,4 @@ Rectangle { } } + diff --git a/linphone-app/ui/modules/Linphone/Chat/ChatReplyMessage.qml b/linphone-app/ui/modules/Linphone/Chat/ChatReplyMessage.qml index ee7c193d2..02577f5a9 100644 --- a/linphone-app/ui/modules/Linphone/Chat/ChatReplyMessage.qml +++ b/linphone-app/ui/modules/Linphone/Chat/ChatReplyMessage.qml @@ -36,6 +36,7 @@ Item { height: fitHeight onMainChatMessageModelChanged: if( mainChatMessageModel && mainChatMessageModel.replyChatMessageModel) chatMessageModel = mainChatMessageModel.replyChatMessageModel + signal goToMessage(ChatMessageModel message) ColumnLayout{ anchors.fill: parent @@ -51,6 +52,10 @@ Item { iconSize: ChatReplyMessageStyle.header.replyIcon.iconSize height: iconSize overwriteColor: ChatReplyMessageStyle.header.color + MouseArea{ + anchors.fill: parent + onClicked: mainItem.goToMessage(mainItem.chatMessageModel) + } } Text{ id: headerText @@ -62,6 +67,10 @@ Item { font.family: mainItem.customFont.family font.pointSize: Units.dp * (mainItem.customFont.pointSize + ChatReplyMessageStyle.header.pointSizeOffset) color: ChatReplyMessageStyle.header.color + MouseArea{ + anchors.fill: parent + onClicked: mainItem.goToMessage(mainItem.chatMessageModel) + } } } Rectangle{ diff --git a/linphone-app/ui/modules/Linphone/Chat/IncomingMessage.qml b/linphone-app/ui/modules/Linphone/Chat/IncomingMessage.qml index dd09458d3..02ee1109e 100644 --- a/linphone-app/ui/modules/Linphone/Chat/IncomingMessage.qml +++ b/linphone-app/ui/modules/Linphone/Chat/IncomingMessage.qml @@ -17,6 +17,7 @@ RowLayout { signal copySelectionDone() signal replyClicked() signal forwardClicked() + signal goToMessage(ChatMessageModel message) implicitHeight: message.height spacing: 0 @@ -67,6 +68,7 @@ RowLayout { onCopySelectionDone: parent.copySelectionDone() onReplyClicked: parent.replyClicked() onForwardClicked: parent.forwardClicked() + onGoToMessage: parent.goToMessage(message) Layout.fillWidth: true diff --git a/linphone-app/ui/modules/Linphone/Chat/Message.qml b/linphone-app/ui/modules/Linphone/Chat/Message.qml index 2a95c6325..daa55799f 100644 --- a/linphone-app/ui/modules/Linphone/Chat/Message.qml +++ b/linphone-app/ui/modules/Linphone/Chat/Message.qml @@ -34,6 +34,7 @@ Item { signal copySelectionDone() signal replyClicked() signal forwardClicked() + signal goToMessage(ChatMessageModel message) // --------------------------------------------------------------------------- property string lastTextSelected @@ -87,6 +88,7 @@ Item { onFitWidthChanged:{ rectangle.updateWidth() } + onGoToMessage: container.goToMessage(message) } ListView { diff --git a/linphone-app/ui/modules/Linphone/Chat/OutgoingMessage.qml b/linphone-app/ui/modules/Linphone/Chat/OutgoingMessage.qml index 6c96745f9..345ec0754 100644 --- a/linphone-app/ui/modules/Linphone/Chat/OutgoingMessage.qml +++ b/linphone-app/ui/modules/Linphone/Chat/OutgoingMessage.qml @@ -19,6 +19,7 @@ Item { signal copySelectionDone() signal replyClicked() signal forwardClicked() + signal goToMessage(ChatMessageModel message) Message { id: message @@ -27,6 +28,7 @@ Item { onCopySelectionDone: parent.copySelectionDone() onReplyClicked: parent.replyClicked() onForwardClicked: parent.forwardClicked() + onGoToMessage: parent.goToMessage(message) anchors { left: parent.left diff --git a/linphone-sdk b/linphone-sdk index 938bcf6d2..9cc416ad8 160000 --- a/linphone-sdk +++ b/linphone-sdk @@ -1 +1 @@ -Subproject commit 938bcf6d288eba2ff430165fe3da22f8cde9d42e +Subproject commit 9cc416ad81725beb1d04baa9c6e1dc3d23db11ac