From 5b6e5a25e563c260ee35337a86934dd0001dade3 Mon Sep 17 00:00:00 2001 From: Joshua Goins Date: Sun, 11 Jan 2026 17:28:08 -0500 Subject: [PATCH] Allow opening message menus for out-of-room events These are more common than we thought, good examples are pinned or searched messages - which are not going to be in the room's history unless you happen to have them loaded. But currently our message menu infrastructure expects them to be, since its looked up by the room + event ID. To fix this is simple, we now move the job of finding the event to the caller which may use a model instead. I didn't fix all existing call-sites yet, mainly the message menu opening one since that was the most obvious bug. But this opens up the door for other assumptions about room history to be fixed too. I had to do a bit of C++ re-jiggering in order to expose useful functions to QML. --- src/app/qml/NeochatMaximizeComponent.qml | 5 ++++- src/app/roommanager.cpp | 26 +++++++++------------- src/app/roommanager.h | 2 +- src/libneochat/neochatroom.cpp | 5 +++++ src/libneochat/neochatroom.h | 7 ++++++ src/messagecontent/CodeComponent.qml | 5 ++++- src/messagecontent/QuoteComponent.qml | 5 ++++- src/messagecontent/StateComponent.qml | 5 +++-- src/messagecontent/TextComponent.qml | 10 +++++++-- src/timeline/HiddenDelegate.qml | 6 +++-- src/timeline/MessageDelegate.qml | 5 +++-- src/timeline/models/messagefiltermodel.cpp | 17 ++++++++++++++ src/timeline/models/messagefiltermodel.h | 6 ++++- src/timeline/models/messagemodel.cpp | 19 ++++++++++------ src/timeline/models/messagemodel.h | 6 ++++- 15 files changed, 92 insertions(+), 37 deletions(-) diff --git a/src/app/qml/NeochatMaximizeComponent.qml b/src/app/qml/NeochatMaximizeComponent.qml index ecf5c234d..b465cabaa 100644 --- a/src/app/qml/NeochatMaximizeComponent.qml +++ b/src/app/qml/NeochatMaximizeComponent.qml @@ -154,7 +154,10 @@ Components.AlbumMaximizeComponent { onOpened: forceActiveFocus() - onItemRightClicked: RoomManager.viewEventMenu(root.currentEventId, root.currentRoom) + onItemRightClicked: { + const event = root.currentRoom.findEvent(root.currentEventId); + RoomManager.viewEventMenu(event, root.currentRoom) + } onSaveItem: { var dialog = saveAsDialog.createObject(QQC2.Overlay.overlay) as Dialogs.FileDialog; diff --git a/src/app/roommanager.cpp b/src/app/roommanager.cpp index 0845b6a16..feaf3b90f 100644 --- a/src/app/roommanager.cpp +++ b/src/app/roommanager.cpp @@ -282,26 +282,20 @@ void RoomManager::viewEventSource(const QString &eventId) Q_EMIT showEventSource(eventId); } -void RoomManager::viewEventMenu(const QString &eventId, NeoChatRoom *room, const QString &selectedText, const QString &hoveredLink) +void RoomManager::viewEventMenu(const RoomEvent *event, NeoChatRoom *room, const QString &selectedText, const QString &hoveredLink) { - if (eventId.isEmpty()) { - qWarning() << "Tried to open event menu with empty event id"; + if (!event) { + qWarning() << "Tried to open event menu with empty event"; return; } - const auto it = room->findInTimeline(eventId); - if (it == room->historyEdge()) { - // This is probably a pending event - return; - } - const auto &event = **it; - Q_EMIT showDelegateMenu(eventId, - room->qmlSafeMember(event.senderId()), - MessageComponentType::typeForEvent(event), - EventHandler::plainBody(room, &event), - EventHandler::richBody(room, &event), - EventHandler::mediaInfo(room, &event)["mimeType"_L1].toString(), - room->fileTransferInfo(eventId), + Q_EMIT showDelegateMenu(event->id(), + room->qmlSafeMember(event->senderId()), + MessageComponentType::typeForEvent(*event), + EventHandler::plainBody(room, event), + EventHandler::richBody(room, event), + EventHandler::mediaInfo(room, event)["mimeType"_L1].toString(), + room->fileTransferInfo(event->id()), selectedText, hoveredLink); } diff --git a/src/app/roommanager.h b/src/app/roommanager.h index e8feff45d..e2b8fac3d 100644 --- a/src/app/roommanager.h +++ b/src/app/roommanager.h @@ -233,7 +233,7 @@ public: /** * @brief Show a context menu for the given event. */ - Q_INVOKABLE void viewEventMenu(const QString &eventId, NeoChatRoom *room, const QString &selectedText = {}, const QString &hoveredLink = {}); + Q_INVOKABLE void viewEventMenu(const RoomEvent *event, NeoChatRoom *room, const QString &selectedText = {}, const QString &hoveredLink = {}); /** * @brief Set a URL to be loaded as the initial room. diff --git a/src/libneochat/neochatroom.cpp b/src/libneochat/neochatroom.cpp index 74b82329b..7a0a7a76c 100644 --- a/src/libneochat/neochatroom.cpp +++ b/src/libneochat/neochatroom.cpp @@ -1694,6 +1694,11 @@ std::pair NeoChatRoom::getEvent(const QString return std::make_pair(extraIt != m_extraEvents.end() ? extraIt->get() : nullptr, false); } +const RoomEvent *NeoChatRoom::findEvent(const QString &eventId) const +{ + return getEvent(eventId).first; +} + const RoomEvent *NeoChatRoom::getReplyForEvent(const RoomEvent &event) const { #if Quotient_VERSION_MINOR > 9 diff --git a/src/libneochat/neochatroom.h b/src/libneochat/neochatroom.h index 720306acd..39894713f 100644 --- a/src/libneochat/neochatroom.h +++ b/src/libneochat/neochatroom.h @@ -538,6 +538,13 @@ public: */ std::pair getEvent(const QString &eventId) const; + /** + * @brief Returns the event object with the given ID if available. + * + * This function works identically to getEvent, except this is usable from QML. + */ + Q_INVOKABLE const Quotient::RoomEvent *findEvent(const QString &eventId) const; + /** * @brief Returns the event that is being replied to. This includes events that were manually loaded using NeoChatRoom::loadReply. */ diff --git a/src/messagecontent/CodeComponent.qml b/src/messagecontent/CodeComponent.qml index 73e448240..0c2be60a2 100644 --- a/src/messagecontent/CodeComponent.qml +++ b/src/messagecontent/CodeComponent.qml @@ -130,7 +130,10 @@ QQC2.Control { TapHandler { acceptedDevices: PointerDevice.TouchScreen - onLongPressed: RoomManager.viewEventMenu(root.eventId, root.Message.room, root.Message.selectedText, root.Message.hoveredLink); + onLongPressed: { + const event = root.Message.room.findEvent(root.eventId); + RoomManager.viewEventMenu(event, root.Message.room, root.Message.selectedText, root.Message.hoveredLink); + } } background: null diff --git a/src/messagecontent/QuoteComponent.qml b/src/messagecontent/QuoteComponent.qml index c4c3271a6..26a69542d 100644 --- a/src/messagecontent/QuoteComponent.qml +++ b/src/messagecontent/QuoteComponent.qml @@ -66,7 +66,10 @@ QQC2.Control { enabled: !quoteText.hoveredLink acceptedDevices: PointerDevice.TouchScreen acceptedButtons: Qt.LeftButton - onLongPressed: RoomManager.viewEventMenu(root.eventId, root.Message.room, root.Message.selectedText, root.Message.hoveredLink); + onLongPressed: { + const event = root.Message.room.findEvent(root.eventId); + RoomManager.viewEventMenu(event, root.Message.room, root.Message.selectedText, root.Message.hoveredLink); + } } } diff --git a/src/messagecontent/StateComponent.qml b/src/messagecontent/StateComponent.qml index d35757c56..34d789592 100644 --- a/src/messagecontent/StateComponent.qml +++ b/src/messagecontent/StateComponent.qml @@ -86,11 +86,12 @@ RowLayout { QtObject { id: _private - function showMessageMenu() { + function showMessageMenu(): void { if (!NeoChatConfig.developerTools) { return; } - RoomManager.viewEventMenu(root.modelData.eventId, root.Message.room, root.author, "", ""); + const event = root.Message.room.findEvent(root.modelData.eventId); + RoomManager.viewEventMenu(event, root.Message.room, root.author, "", ""); } } } diff --git a/src/messagecontent/TextComponent.qml b/src/messagecontent/TextComponent.qml index d1a7fdc8f..26bf3532b 100644 --- a/src/messagecontent/TextComponent.qml +++ b/src/messagecontent/TextComponent.qml @@ -97,12 +97,18 @@ TextEdit { enabled: !root.hoveredLink acceptedButtons: Qt.LeftButton acceptedDevices: PointerDevice.TouchScreen - onLongPressed: RoomManager.viewEventMenu(root.eventId, root.Message.room, root.Message.selectedText, root.Message.hoveredLink); + onLongPressed: { + const event = root.Message.room.findEvent(root.eventId); + RoomManager.viewEventMenu(event, root.Message.room, root.Message.selectedText, root.Message.hoveredLink); + } } TapHandler { acceptedButtons: Qt.RightButton acceptedDevices: PointerDevice.Mouse | PointerDevice.TouchPad | PointerDevice.Stylus gesturePolicy: TapHandler.WithinBounds - onTapped: RoomManager.viewEventMenu(root.eventId, root.Message.room, root.Message.selectedText, root.Message.hoveredLink); + onTapped: { + const event = root.Message.room.findEvent(root.eventId); + RoomManager.viewEventMenu(event, root.Message.room, root.Message.selectedText, root.Message.hoveredLink); + } } } diff --git a/src/timeline/HiddenDelegate.qml b/src/timeline/HiddenDelegate.qml index 1365658a5..501aa2e02 100644 --- a/src/timeline/HiddenDelegate.qml +++ b/src/timeline/HiddenDelegate.qml @@ -92,8 +92,10 @@ TimelineDelegate { QtObject { id: _private - function showMessageMenu() { - RoomManager.viewEventMenu(root.eventId, root.room, ""); + + function showMessageMenu(): void { + let event = root.Message.room.findEvent(root.eventId); + RoomManager.viewEventMenu(event, root.room, ""); } } } diff --git a/src/timeline/MessageDelegate.qml b/src/timeline/MessageDelegate.qml index 4a2c5d680..ebe07bb4c 100644 --- a/src/timeline/MessageDelegate.qml +++ b/src/timeline/MessageDelegate.qml @@ -232,8 +232,9 @@ MessageDelegateBase { QtObject { id: _private - function showMessageMenu() { - RoomManager.viewEventMenu(root.eventId, root.room, root.Message.selectedText, root.Message.hoveredLink); + function showMessageMenu(): void { + let event = root.ListView.view.model.findEvent(root.eventId); + RoomManager.viewEventMenu(event, root.room, root.Message.selectedText, root.Message.hoveredLink); } } } diff --git a/src/timeline/models/messagefiltermodel.cpp b/src/timeline/models/messagefiltermodel.cpp index a19d8fdcb..6ae5a5b6b 100644 --- a/src/timeline/models/messagefiltermodel.cpp +++ b/src/timeline/models/messagefiltermodel.cpp @@ -152,6 +152,23 @@ QModelIndex MessageFilterModel::indexForEventId(const QString &eventId) const return mapFromSource(eventIndex); } +const Quotient::RoomEvent *MessageFilterModel::findEvent(const QString &eventId) const +{ + // Check if sourceModel is a message model. + auto messageModel = dynamic_cast(sourceModel()); + // See if it's a timeline model. + if (!messageModel) { + if (const auto timelineModel = dynamic_cast(sourceModel())) { + messageModel = timelineModel->timelineMessageModel(); + if (!messageModel) { + return nullptr; + } + } + } + + return messageModel->findEvent(eventId); +} + bool MessageFilterModel::showAuthor(QModelIndex index) const { for (auto r = index.row() + 1; r < rowCount(); ++r) { diff --git a/src/timeline/models/messagefiltermodel.h b/src/timeline/models/messagefiltermodel.h index b049bec6f..cb8314398 100644 --- a/src/timeline/models/messagefiltermodel.h +++ b/src/timeline/models/messagefiltermodel.h @@ -67,9 +67,13 @@ public: /** * @brief Get the QModelIndex the given event ID in the model. */ - Q_INVOKABLE QModelIndex indexforEventId(const QString &eventId) const; Q_INVOKABLE QModelIndex indexForEventId(const QString &eventId) const; + /** + * @brief Finds the event of the given event ID in the model, returning nullptr if no matches were found. + */ + Q_INVOKABLE const Quotient::RoomEvent *findEvent(const QString &eventId) const; + static void setShowAllEvents(bool enabled); static void setShowDeletedMessages(bool enabled); diff --git a/src/timeline/models/messagemodel.cpp b/src/timeline/models/messagemodel.cpp index d6149a7ac..29067b2b8 100644 --- a/src/timeline/models/messagemodel.cpp +++ b/src/timeline/models/messagemodel.cpp @@ -351,16 +351,21 @@ QHash MessageModel::roleNames() const QModelIndex MessageModel::indexForEventId(const QString &eventId) const { - if (m_room == nullptr) { - return {}; - } - - const auto it = m_room->findInTimeline(eventId); - if (it == m_room->historyEdge()) { + const auto matches = match(index(0, 0), EventIdRole, eventId); + if (matches.isEmpty()) { qWarning() << "Trying to find non-existent event:" << eventId; return {}; } - return index(it - m_room->messageEvents().rbegin() + timelineServerIndex()); + return matches.constFirst(); +} + +const RoomEvent *MessageModel::findEvent(const QString &eventId) const +{ + const auto index = indexForEventId(eventId); + if (const auto event = getEventForIndex(index)) { + return &event.value().get(); + } + return nullptr; } void MessageModel::fullEventRefresh(int row) diff --git a/src/timeline/models/messagemodel.h b/src/timeline/models/messagemodel.h index f2cf92a71..687845fd6 100644 --- a/src/timeline/models/messagemodel.h +++ b/src/timeline/models/messagemodel.h @@ -115,9 +115,13 @@ public: /** * @brief Get the QModelIndex of the given event ID in the model, returning an invalid QModelIndex if no matches were found. */ - Q_INVOKABLE QModelIndex indexforEventId(const QString &eventId) const; Q_INVOKABLE QModelIndex indexForEventId(const QString &eventId) const; + /** + * @brief Finds the event of the given event ID in the model, returning nullptr if no matches were found. + */ + Q_INVOKABLE const Quotient::RoomEvent *findEvent(const QString &eventId) const; + static void setHiddenFilter(std::function hiddenFilter); static void setThreadsEnabled(bool enableThreads);