scroll to original message on click on reply message #LINQT-2390

This commit is contained in:
Gaelle Braud 2026-02-25 17:17:13 +01:00
parent ca0bf41ee7
commit 92f4a92f86
9 changed files with 122 additions and 16 deletions

View file

@ -194,6 +194,7 @@ ChatMessageCore::ChatMessageCore(const std::shared_ptr<linphone::ChatMessage> &c
mIsForward = chatmessage->isForward(); mIsForward = chatmessage->isForward();
mIsReply = chatmessage->isReply(); mIsReply = chatmessage->isReply();
mReplyMessageId = Utils::coreStringToAppString(chatmessage->getReplyMessageId());
if (mIsReply) { if (mIsReply) {
auto replymessage = chatmessage->getReplyMessage(); auto replymessage = chatmessage->getReplyMessage();
if (replymessage) { if (replymessage) {

View file

@ -107,6 +107,7 @@ class ChatMessageCore : public QObject, public AbstractObject {
Q_PROPERTY(bool isForward MEMBER mIsForward CONSTANT) Q_PROPERTY(bool isForward MEMBER mIsForward CONSTANT)
Q_PROPERTY(bool isReply MEMBER mIsReply CONSTANT) Q_PROPERTY(bool isReply MEMBER mIsReply CONSTANT)
Q_PROPERTY(QString replyText MEMBER mReplyText CONSTANT) Q_PROPERTY(QString replyText MEMBER mReplyText CONSTANT)
Q_PROPERTY(QString replyMessageId MEMBER mReplyMessageId CONSTANT)
Q_PROPERTY(QString repliedToName MEMBER mRepliedToName CONSTANT) Q_PROPERTY(QString repliedToName MEMBER mRepliedToName CONSTANT)
Q_PROPERTY(bool hasFileContent MEMBER mHasFileContent CONSTANT) Q_PROPERTY(bool hasFileContent MEMBER mHasFileContent CONSTANT)
Q_PROPERTY(bool isVoiceRecording MEMBER mIsVoiceRecording CONSTANT) Q_PROPERTY(bool isVoiceRecording MEMBER mIsVoiceRecording CONSTANT)
@ -220,6 +221,7 @@ private:
bool mIsForward = false; bool mIsForward = false;
bool mIsReply = false; bool mIsReply = false;
QString mReplyText; QString mReplyText;
QString mReplyMessageId;
QString mRepliedToName; QString mRepliedToName;
bool mHasFileContent = false; bool mHasFileContent = false;
bool mIsCalendarInvite = false; bool mIsCalendarInvite = false;

View file

@ -201,7 +201,10 @@ void EventLogList::loadMessagesUpTo(std::shared_ptr<linphone::EventLog> event) {
: nullptr; : nullptr;
auto chatModel = mChatCore->getModel(); auto chatModel = mChatCore->getModel();
assert(chatModel); assert(chatModel);
if (!chatModel) return; if (!chatModel) {
emit messagesLoadedUpTo(nullptr);
return;
}
int filters = static_cast<int>(linphone::ChatRoom::HistoryFilter::ChatMessage) | int filters = static_cast<int>(linphone::ChatRoom::HistoryFilter::ChatMessage) |
static_cast<int>(linphone::ChatRoom::HistoryFilter::InfoNoDevice); static_cast<int>(linphone::ChatRoom::HistoryFilter::InfoNoDevice);
auto beforeEvents = chatModel->getHistoryRangeNear(mItemsToLoadBeforeSearchResult, 0, event, filters); auto beforeEvents = chatModel->getHistoryRangeNear(mItemsToLoadBeforeSearchResult, 0, event, filters);
@ -273,19 +276,24 @@ void EventLogList::findChatMessageWithFilter(QString filter, int startIndex, boo
mLastFoundResult = *it; mLastFoundResult = *it;
mCoreModelConnection->invokeToCore([this, index] { emit messageWithFilterFound(index); }); mCoreModelConnection->invokeToCore([this, index] { emit messageWithFilterFound(index); });
} else { } else {
connect(this, &EventLogList::messagesLoadedUpTo, this, connect(
[this](std::shared_ptr<linphone::EventLog> event) { this, &EventLogList::messagesLoadedUpTo, this,
auto eventList = getSharedList<EventLogCore>(); [this](std::shared_ptr<linphone::EventLog> event) {
auto it = std::find_if(eventList.begin(), eventList.end(), if (event == nullptr) emit messageWithFilterFound(-1);
[event](const QSharedPointer<EventLogCore> item) { else {
return item->getModel()->getEventLog() == event; auto eventList = getSharedList<EventLogCore>();
}); auto it = std::find_if(eventList.begin(), eventList.end(),
int index = it != eventList.end() ? std::distance(eventList.begin(), it) : -1; [event](const QSharedPointer<EventLogCore> item) {
if (mLastFoundResult && mLastFoundResult == *it) index = -1; return item->getModel()->getEventLog() == event;
mLastFoundResult = *it; });
mCoreModelConnection->invokeToCore( int index = it != eventList.end() ? std::distance(eventList.begin(), it) : -1;
[this, index] { emit messageWithFilterFound(index); }); if (mLastFoundResult && mLastFoundResult == *it) index = -1;
}); mLastFoundResult = *it;
mCoreModelConnection->invokeToCore(
[this, index] { emit messageWithFilterFound(index); });
}
},
Qt::SingleShotConnection);
loadMessagesUpTo(eventLog); loadMessagesUpTo(eventLog);
} }
} else { } 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<EventLogCore>();
auto it = std::find_if(eventList.begin(), eventList.end(), [messageId](const QSharedPointer<EventLogCore> 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<linphone::EventLog> 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<linphone::EventLog> event) {
if (event == nullptr) return;
auto eventList = getSharedList<EventLogCore>();
auto it = std::find_if(eventList.begin(), eventList.end(),
[event](const QSharedPointer<EventLogCore> 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<EventLogList> me) { void EventLogList::setSelf(QSharedPointer<EventLogList> me) {
mCoreModelConnection = SafeConnection<EventLogList, CoreModel>::create(me, CoreModel::getInstance()); mCoreModelConnection = SafeConnection<EventLogList, CoreModel>::create(me, CoreModel::getInstance());

View file

@ -60,6 +60,7 @@ public:
void setDisplayItemsStep(int displayItemsStep); void setDisplayItemsStep(int displayItemsStep);
void findChatMessageWithFilter(QString filter, int startIndex, bool forward = true, bool isFirstResearch = true); void findChatMessageWithFilter(QString filter, int startIndex, bool forward = true, bool isFirstResearch = true);
void findChatMessageById(const QString &messageId);
void setSelf(QSharedPointer<EventLogList> me); void setSelf(QSharedPointer<EventLogList> me);
virtual QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override; virtual QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override;
@ -71,6 +72,7 @@ signals:
void filterChanged(QString filter); void filterChanged(QString filter);
void eventInsertedByUser(int index); void eventInsertedByUser(int index);
void messageWithFilterFound(int index); void messageWithFilterFound(int index);
void foundMessagById(int index);
void listAboutToBeReset(); void listAboutToBeReset();
void chatGuiChanged(); void chatGuiChanged();
void displayItemsStepChanged(); void displayItemsStepChanged();

View file

@ -39,6 +39,7 @@ void EventLogProxy::setSourceModel(QAbstractItemModel *model) {
if (oldEventLogList) { if (oldEventLogList) {
disconnect(oldEventLogList, &EventLogList::displayItemsStepChanged, this, nullptr); disconnect(oldEventLogList, &EventLogList::displayItemsStepChanged, this, nullptr);
disconnect(oldEventLogList, &EventLogList::messageWithFilterFound, this, nullptr); disconnect(oldEventLogList, &EventLogList::messageWithFilterFound, this, nullptr);
disconnect(oldEventLogList, &EventLogList::foundMessagById, this, nullptr);
disconnect(oldEventLogList, &EventLogList::eventInsertedByUser, this, nullptr); disconnect(oldEventLogList, &EventLogList::eventInsertedByUser, this, nullptr);
} }
auto newEventLogList = dynamic_cast<EventLogList *>(model); auto newEventLogList = dynamic_cast<EventLogList *>(model);
@ -53,6 +54,19 @@ void EventLogProxy::setSourceModel(QAbstractItemModel *model) {
} }
emit indexWithFilterFound(proxyIndex); emit indexWithFilterFound(proxyIndex);
}); });
connect(newEventLogList, &EventLogList::foundMessagById, this, [this, newEventLogList](int i) {
auto model = dynamic_cast<EventLogList *>(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) { connect(newEventLogList, &EventLogList::eventInsertedByUser, this, [this, newEventLogList](int i) {
int proxyIndex = mapFromSource(newEventLogList->index(i, 0)).row(); int proxyIndex = mapFromSource(newEventLogList->index(i, 0)).row();
emit eventInsertedByUser(proxyIndex); emit eventInsertedByUser(proxyIndex);
@ -207,3 +221,10 @@ void EventLogProxy::findIndexCorrespondingToFilter(int startIndex, bool forward,
eventLogList->findChatMessageWithFilter(filter, listIndex, forward, isFirstResearch); eventLogList->findChatMessageWithFilter(filter, listIndex, forward, isFirstResearch);
} }
} }
void EventLogProxy::findChatMessageById(const QString &messageId) {
auto eventLogList = dynamic_cast<EventLogList *>(sourceModel());
if (eventLogList) {
eventLogList->findChatMessageById(messageId);
}
}

View file

@ -77,10 +77,12 @@ public:
Q_INVOKABLE int findFirstUnreadIndex(); Q_INVOKABLE int findFirstUnreadIndex();
Q_INVOKABLE void markIndexAsRead(int proxyIndex); Q_INVOKABLE void markIndexAsRead(int proxyIndex);
Q_INVOKABLE void findIndexCorrespondingToFilter(int startIndex, bool forward = true, bool isFirstResearch = true); Q_INVOKABLE void findIndexCorrespondingToFilter(int startIndex, bool forward = true, bool isFirstResearch = true);
Q_INVOKABLE void findChatMessageById(const QString &messageId);
signals: signals:
void eventInsertedByUser(int index); void eventInsertedByUser(int index);
void indexWithFilterFound(int index); void indexWithFilterFound(int index);
void foundMessagById(int index);
void chatGuiChanged(); void chatGuiChanged();
void countChanged(); void countChanged();
void initialDisplayItemsChanged(); void initialDisplayItemsChanged();

View file

@ -109,7 +109,7 @@ void ChatMessageContentCore::setSelf(QSharedPointer<ChatMessageContentCore> me)
mChatMessageContentModelConnection->invokeToCore([this, error] { mChatMessageContentModelConnection->invokeToCore([this, error] {
//: Error downloading file %1 //: Error downloading file %1
if (error->isEmpty()) *error = tr("download_file_default_error").arg(mName); 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; delete error;
}); });
} else delete error; } else delete error;

View file

@ -23,7 +23,8 @@ Control.Control {
property bool isFromChatGroup: chatMessage? chatMessage.core.isFromChatGroup : false property bool isFromChatGroup: chatMessage? chatMessage.core.isFromChatGroup : false
property bool isReply: chatMessage? chatMessage.core.isReply : false property bool isReply: chatMessage? chatMessage.core.isReply : false
property bool isForward: chatMessage? chatMessage.core.isForward : 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 property var msgState: chatMessage ? chatMessage.core.messageState : LinphoneEnums.ChatMessageState.StateIdle
hoverEnabled: true hoverEnabled: true
property bool linkHovered: false property bool linkHovered: false
@ -40,6 +41,7 @@ Control.Control {
signal forwardMessageRequested() signal forwardMessageRequested()
signal endOfVoiceRecordingReached() signal endOfVoiceRecordingReached()
signal requestAutoPlayVoiceRecording() signal requestAutoPlayVoiceRecording()
signal searchMessageByIdRequested(string id)
onRequestAutoPlayVoiceRecording: chatBubbleContent.requestAutoPlayVoiceRecording() onRequestAutoPlayVoiceRecording: chatBubbleContent.requestAutoPlayVoiceRecording()
Timer { Timer {
@ -154,6 +156,13 @@ Control.Control {
anchors.fill: parent anchors.fill: parent
color: DefaultStyle.grey_200 color: DefaultStyle.grey_200
radius: Utils.getSizeWithScreenRatio(16) radius: Utils.getSizeWithScreenRatio(16)
MouseArea {
anchors.fill: parent
cursorShape: Qt.PointingHandCursor
onClicked: {
mainItem.searchMessageByIdRequested(mainItem.replyMessageId)
}
}
} }
contentItem: Text { contentItem: Text {
Layout.fillWidth: true Layout.fillWidth: true

View file

@ -29,6 +29,7 @@ ListView {
signal editMessageRequested(ChatMessageGui chatMessage) signal editMessageRequested(ChatMessageGui chatMessage)
signal requestHighlight(int indexToHighlight) signal requestHighlight(int indexToHighlight)
signal requestAutoPlayVoiceRecording(int indexToPlay) signal requestAutoPlayVoiceRecording(int indexToPlay)
signal searchMessageByIdRequested(string id)
currentIndex: -1 currentIndex: -1
property string filterText property string filterText
@ -45,6 +46,9 @@ ListView {
searchForward = forward searchForward = forward
eventLogProxy.findIndexCorrespondingToFilter(currentIndex, searchForward, false) eventLogProxy.findIndexCorrespondingToFilter(currentIndex, searchForward, false)
} }
onSearchMessageByIdRequested: (id) => {
eventLogProxy.findChatMessageById(id)
}
Button { Button {
visible: !mainItem.lastItemVisible 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 { footer: Item {
@ -327,6 +338,7 @@ ListView {
onShowReactionsForMessageRequested: mainItem.showReactionsForMessageRequested(chatMessage) onShowReactionsForMessageRequested: mainItem.showReactionsForMessageRequested(chatMessage)
onShowImdnStatusForMessageRequested: mainItem.showImdnStatusForMessageRequested(chatMessage) onShowImdnStatusForMessageRequested: mainItem.showImdnStatusForMessageRequested(chatMessage)
onReplyToMessageRequested: mainItem.replyToMessageRequested(chatMessage) onReplyToMessageRequested: mainItem.replyToMessageRequested(chatMessage)
onSearchMessageByIdRequested: (id) => mainItem.searchMessageByIdRequested(id)
onForwardMessageRequested: mainItem.forwardMessageRequested(chatMessage) onForwardMessageRequested: mainItem.forwardMessageRequested(chatMessage)
onEndOfVoiceRecordingReached: { onEndOfVoiceRecordingReached: {
if (nextChatMessage && nextChatMessage.core.isVoiceRecording) mainItem.requestAutoPlayVoiceRecording(index - 1) if (nextChatMessage && nextChatMessage.core.isVoiceRecording) mainItem.requestAutoPlayVoiceRecording(index - 1)