From 92f4a92f86e43e3970a26de9d323620c652ed02f Mon Sep 17 00:00:00 2001 From: Gaelle Braud Date: Wed, 25 Feb 2026 17:17:13 +0100 Subject: [PATCH] scroll to original message on click on reply message #LINQT-2390 --- .../core/chat/message/ChatMessageCore.cpp | 1 + .../core/chat/message/ChatMessageCore.hpp | 2 + Linphone/core/chat/message/EventLogList.cpp | 85 ++++++++++++++++--- Linphone/core/chat/message/EventLogList.hpp | 2 + Linphone/core/chat/message/EventLogProxy.cpp | 21 +++++ Linphone/core/chat/message/EventLogProxy.hpp | 2 + .../content/ChatMessageContentCore.cpp | 2 +- .../view/Control/Display/Chat/ChatMessage.qml | 11 ++- .../Display/Chat/ChatMessagesListView.qml | 12 +++ 9 files changed, 122 insertions(+), 16 deletions(-) diff --git a/Linphone/core/chat/message/ChatMessageCore.cpp b/Linphone/core/chat/message/ChatMessageCore.cpp index 786ce9c9d..f2df156b4 100644 --- a/Linphone/core/chat/message/ChatMessageCore.cpp +++ b/Linphone/core/chat/message/ChatMessageCore.cpp @@ -194,6 +194,7 @@ ChatMessageCore::ChatMessageCore(const std::shared_ptr &c mIsForward = chatmessage->isForward(); mIsReply = chatmessage->isReply(); + mReplyMessageId = Utils::coreStringToAppString(chatmessage->getReplyMessageId()); if (mIsReply) { auto replymessage = chatmessage->getReplyMessage(); if (replymessage) { diff --git a/Linphone/core/chat/message/ChatMessageCore.hpp b/Linphone/core/chat/message/ChatMessageCore.hpp index b4115b3f6..757d33cd5 100644 --- a/Linphone/core/chat/message/ChatMessageCore.hpp +++ b/Linphone/core/chat/message/ChatMessageCore.hpp @@ -107,6 +107,7 @@ class ChatMessageCore : public QObject, public AbstractObject { Q_PROPERTY(bool isForward MEMBER mIsForward CONSTANT) Q_PROPERTY(bool isReply MEMBER mIsReply CONSTANT) Q_PROPERTY(QString replyText MEMBER mReplyText CONSTANT) + Q_PROPERTY(QString replyMessageId MEMBER mReplyMessageId CONSTANT) Q_PROPERTY(QString repliedToName MEMBER mRepliedToName CONSTANT) Q_PROPERTY(bool hasFileContent MEMBER mHasFileContent CONSTANT) Q_PROPERTY(bool isVoiceRecording MEMBER mIsVoiceRecording CONSTANT) @@ -220,6 +221,7 @@ private: bool mIsForward = false; bool mIsReply = false; QString mReplyText; + QString mReplyMessageId; QString mRepliedToName; bool mHasFileContent = false; bool mIsCalendarInvite = false; diff --git a/Linphone/core/chat/message/EventLogList.cpp b/Linphone/core/chat/message/EventLogList.cpp index 183db38d2..eba2da6c9 100644 --- a/Linphone/core/chat/message/EventLogList.cpp +++ b/Linphone/core/chat/message/EventLogList.cpp @@ -201,7 +201,10 @@ void EventLogList::loadMessagesUpTo(std::shared_ptr event) { : nullptr; auto chatModel = mChatCore->getModel(); assert(chatModel); - if (!chatModel) return; + if (!chatModel) { + emit messagesLoadedUpTo(nullptr); + return; + } int filters = static_cast(linphone::ChatRoom::HistoryFilter::ChatMessage) | static_cast(linphone::ChatRoom::HistoryFilter::InfoNoDevice); auto beforeEvents = chatModel->getHistoryRangeNear(mItemsToLoadBeforeSearchResult, 0, event, filters); @@ -273,19 +276,24 @@ void EventLogList::findChatMessageWithFilter(QString filter, int startIndex, boo mLastFoundResult = *it; mCoreModelConnection->invokeToCore([this, index] { emit messageWithFilterFound(index); }); } else { - connect(this, &EventLogList::messagesLoadedUpTo, this, - [this](std::shared_ptr event) { - auto eventList = getSharedList(); - auto it = std::find_if(eventList.begin(), eventList.end(), - [event](const QSharedPointer item) { - return item->getModel()->getEventLog() == event; - }); - int index = it != eventList.end() ? std::distance(eventList.begin(), it) : -1; - if (mLastFoundResult && mLastFoundResult == *it) index = -1; - mLastFoundResult = *it; - mCoreModelConnection->invokeToCore( - [this, index] { emit messageWithFilterFound(index); }); - }); + connect( + this, &EventLogList::messagesLoadedUpTo, this, + [this](std::shared_ptr event) { + if (event == nullptr) emit messageWithFilterFound(-1); + else { + auto eventList = getSharedList(); + auto it = std::find_if(eventList.begin(), eventList.end(), + [event](const QSharedPointer item) { + return item->getModel()->getEventLog() == event; + }); + int index = it != eventList.end() ? std::distance(eventList.begin(), it) : -1; + if (mLastFoundResult && mLastFoundResult == *it) index = -1; + mLastFoundResult = *it; + mCoreModelConnection->invokeToCore( + [this, index] { emit messageWithFilterFound(index); }); + } + }, + Qt::SingleShotConnection); loadMessagesUpTo(eventLog); } } else { @@ -296,6 +304,55 @@ void EventLogList::findChatMessageWithFilter(QString filter, int startIndex, boo } } +void EventLogList::findChatMessageById(const QString &messageId) { + lInfo() << log().arg("Try to find message by id :"); + auto eventList = getSharedList(); + auto it = std::find_if(eventList.begin(), eventList.end(), [messageId](const QSharedPointer item) { + auto messageCore = item->getChatMessageCore(); + if (!messageCore) return false; + return messageCore->getMessageId() == messageId; + }); + if (it != eventList.end()) { + int index = std::distance(eventList.begin(), it); + if (index != -1) emit foundMessagById(index); + } else { + lInfo() << log().arg("Not found in displayed messages, search in entire history"); + auto chatModel = mChatCore->getModel(); + mCoreModelConnection->invokeToModel([this, chatModel, messageId] { + auto history = chatModel->getHistory(); + auto it = std::find_if(history.begin(), history.end(), + [messageId](const std::shared_ptr eventLog) { + auto chatMessage = eventLog->getChatMessage(); + if (!chatMessage) return false; + return Utils::coreStringToAppString(chatMessage->getMessageId()) == messageId; + }); + // int index = it != history.end() ? std::distance(history.begin(), it) : -1; + // if (index != -1) emit foundMessagById(index); + if (it != history.end()) { + lInfo() << log().arg("Found in entire history, load messages up to it"); + connect( + this, &EventLogList::messagesLoadedUpTo, this, + [this](std::shared_ptr event) { + if (event == nullptr) return; + auto eventList = getSharedList(); + auto it = std::find_if(eventList.begin(), eventList.end(), + [event](const QSharedPointer item) { + return item->getModel()->getEventLog() == event; + }); + int index = it != eventList.end() ? std::distance(eventList.begin(), it) : -1; + lInfo() << log().arg("Index corresponding to chat message id :") << index; + mCoreModelConnection->invokeToCore([this, index] { emit foundMessagById(index); }); + }, + Qt::SingleShotConnection); + loadMessagesUpTo(*it); + } else { + lInfo() << log().arg("Not found in entire history, event must have been deleted"); + emit foundMessagById(-1); + } + }); + } +} + void EventLogList::setSelf(QSharedPointer me) { mCoreModelConnection = SafeConnection::create(me, CoreModel::getInstance()); diff --git a/Linphone/core/chat/message/EventLogList.hpp b/Linphone/core/chat/message/EventLogList.hpp index a9393ba5d..2cdb8e5d4 100644 --- a/Linphone/core/chat/message/EventLogList.hpp +++ b/Linphone/core/chat/message/EventLogList.hpp @@ -60,6 +60,7 @@ public: void setDisplayItemsStep(int displayItemsStep); void findChatMessageWithFilter(QString filter, int startIndex, bool forward = true, bool isFirstResearch = true); + void findChatMessageById(const QString &messageId); void setSelf(QSharedPointer me); virtual QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override; @@ -71,6 +72,7 @@ signals: void filterChanged(QString filter); void eventInsertedByUser(int index); void messageWithFilterFound(int index); + void foundMessagById(int index); void listAboutToBeReset(); void chatGuiChanged(); void displayItemsStepChanged(); diff --git a/Linphone/core/chat/message/EventLogProxy.cpp b/Linphone/core/chat/message/EventLogProxy.cpp index bb916b1ec..3a766bc62 100644 --- a/Linphone/core/chat/message/EventLogProxy.cpp +++ b/Linphone/core/chat/message/EventLogProxy.cpp @@ -39,6 +39,7 @@ void EventLogProxy::setSourceModel(QAbstractItemModel *model) { if (oldEventLogList) { disconnect(oldEventLogList, &EventLogList::displayItemsStepChanged, this, nullptr); disconnect(oldEventLogList, &EventLogList::messageWithFilterFound, this, nullptr); + disconnect(oldEventLogList, &EventLogList::foundMessagById, this, nullptr); disconnect(oldEventLogList, &EventLogList::eventInsertedByUser, this, nullptr); } auto newEventLogList = dynamic_cast(model); @@ -53,6 +54,19 @@ void EventLogProxy::setSourceModel(QAbstractItemModel *model) { } emit indexWithFilterFound(proxyIndex); }); + connect(newEventLogList, &EventLogList::foundMessagById, this, [this, newEventLogList](int i) { + auto model = dynamic_cast(sourceModel()); + int proxyIndex = mapFromSource(newEventLogList->index(i, 0)).row(); + if (i != -1) { + loadUntil(proxyIndex); + lInfo() << "Found index by id, request highlight at index" << proxyIndex; + emit foundMessagById(proxyIndex); + } else { + Utils::showInformationPopup("info_popup_error_title", + //: Original message not found. It may have been deleted + "info_popup_reply_message_not_found_error"); + } + }); connect(newEventLogList, &EventLogList::eventInsertedByUser, this, [this, newEventLogList](int i) { int proxyIndex = mapFromSource(newEventLogList->index(i, 0)).row(); emit eventInsertedByUser(proxyIndex); @@ -207,3 +221,10 @@ void EventLogProxy::findIndexCorrespondingToFilter(int startIndex, bool forward, eventLogList->findChatMessageWithFilter(filter, listIndex, forward, isFirstResearch); } } + +void EventLogProxy::findChatMessageById(const QString &messageId) { + auto eventLogList = dynamic_cast(sourceModel()); + if (eventLogList) { + eventLogList->findChatMessageById(messageId); + } +} diff --git a/Linphone/core/chat/message/EventLogProxy.hpp b/Linphone/core/chat/message/EventLogProxy.hpp index dde4f67c2..665d8a310 100644 --- a/Linphone/core/chat/message/EventLogProxy.hpp +++ b/Linphone/core/chat/message/EventLogProxy.hpp @@ -77,10 +77,12 @@ public: Q_INVOKABLE int findFirstUnreadIndex(); Q_INVOKABLE void markIndexAsRead(int proxyIndex); Q_INVOKABLE void findIndexCorrespondingToFilter(int startIndex, bool forward = true, bool isFirstResearch = true); + Q_INVOKABLE void findChatMessageById(const QString &messageId); signals: void eventInsertedByUser(int index); void indexWithFilterFound(int index); + void foundMessagById(int index); void chatGuiChanged(); void countChanged(); void initialDisplayItemsChanged(); diff --git a/Linphone/core/chat/message/content/ChatMessageContentCore.cpp b/Linphone/core/chat/message/content/ChatMessageContentCore.cpp index 9d7524ebc..5437e97e1 100644 --- a/Linphone/core/chat/message/content/ChatMessageContentCore.cpp +++ b/Linphone/core/chat/message/content/ChatMessageContentCore.cpp @@ -109,7 +109,7 @@ void ChatMessageContentCore::setSelf(QSharedPointer me) mChatMessageContentModelConnection->invokeToCore([this, error] { //: Error downloading file %1 if (error->isEmpty()) *error = tr("download_file_default_error").arg(mName); - Utils::showInformationPopup(tr("info_popup_error_titile"), *error, false); + Utils::showInformationPopup(tr("info_popup_error_title"), *error, false); delete error; }); } else delete error; diff --git a/Linphone/view/Control/Display/Chat/ChatMessage.qml b/Linphone/view/Control/Display/Chat/ChatMessage.qml index c43dee6a4..5de419f2a 100644 --- a/Linphone/view/Control/Display/Chat/ChatMessage.qml +++ b/Linphone/view/Control/Display/Chat/ChatMessage.qml @@ -23,7 +23,8 @@ Control.Control { property bool isFromChatGroup: chatMessage? chatMessage.core.isFromChatGroup : false property bool isReply: chatMessage? chatMessage.core.isReply : false property bool isForward: chatMessage? chatMessage.core.isForward : false - property string replyText: chatMessage? chatMessage.core.replyText : false + property string replyText: chatMessage? chatMessage.core.replyText : "" + property string replyMessageId: chatMessage? chatMessage.core.replyMessageId : "" property var msgState: chatMessage ? chatMessage.core.messageState : LinphoneEnums.ChatMessageState.StateIdle hoverEnabled: true property bool linkHovered: false @@ -40,6 +41,7 @@ Control.Control { signal forwardMessageRequested() signal endOfVoiceRecordingReached() signal requestAutoPlayVoiceRecording() + signal searchMessageByIdRequested(string id) onRequestAutoPlayVoiceRecording: chatBubbleContent.requestAutoPlayVoiceRecording() Timer { @@ -154,6 +156,13 @@ Control.Control { anchors.fill: parent color: DefaultStyle.grey_200 radius: Utils.getSizeWithScreenRatio(16) + MouseArea { + anchors.fill: parent + cursorShape: Qt.PointingHandCursor + onClicked: { + mainItem.searchMessageByIdRequested(mainItem.replyMessageId) + } + } } contentItem: Text { Layout.fillWidth: true diff --git a/Linphone/view/Control/Display/Chat/ChatMessagesListView.qml b/Linphone/view/Control/Display/Chat/ChatMessagesListView.qml index a3bcc05df..dffc12209 100644 --- a/Linphone/view/Control/Display/Chat/ChatMessagesListView.qml +++ b/Linphone/view/Control/Display/Chat/ChatMessagesListView.qml @@ -29,6 +29,7 @@ ListView { signal editMessageRequested(ChatMessageGui chatMessage) signal requestHighlight(int indexToHighlight) signal requestAutoPlayVoiceRecording(int indexToPlay) + signal searchMessageByIdRequested(string id) currentIndex: -1 property string filterText @@ -45,6 +46,9 @@ ListView { searchForward = forward eventLogProxy.findIndexCorrespondingToFilter(currentIndex, searchForward, false) } + onSearchMessageByIdRequested: (id) => { + eventLogProxy.findChatMessageById(id) + } Button { visible: !mainItem.lastItemVisible @@ -127,6 +131,13 @@ ListView { } } } + onFoundMessagById: (index) => { + if (index !== -1) { + currentIndex = index + mainItem.positionViewAtIndex(index, ListView.Center) + mainItem.requestHighlight(index) + } + } } footer: Item { @@ -327,6 +338,7 @@ ListView { onShowReactionsForMessageRequested: mainItem.showReactionsForMessageRequested(chatMessage) onShowImdnStatusForMessageRequested: mainItem.showImdnStatusForMessageRequested(chatMessage) onReplyToMessageRequested: mainItem.replyToMessageRequested(chatMessage) + onSearchMessageByIdRequested: (id) => mainItem.searchMessageByIdRequested(id) onForwardMessageRequested: mainItem.forwardMessageRequested(chatMessage) onEndOfVoiceRecordingReached: { if (nextChatMessage && nextChatMessage.core.isVoiceRecording) mainItem.requestAutoPlayVoiceRecording(index - 1)