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.
This commit is contained in:
Joshua Goins
2026-01-11 17:28:08 -05:00
parent 58b47b8711
commit 5b6e5a25e5
15 changed files with 92 additions and 37 deletions

View File

@@ -154,7 +154,10 @@ Components.AlbumMaximizeComponent {
onOpened: forceActiveFocus() onOpened: forceActiveFocus()
onItemRightClicked: RoomManager.viewEventMenu(root.currentEventId, root.currentRoom) onItemRightClicked: {
const event = root.currentRoom.findEvent(root.currentEventId);
RoomManager.viewEventMenu(event, root.currentRoom)
}
onSaveItem: { onSaveItem: {
var dialog = saveAsDialog.createObject(QQC2.Overlay.overlay) as Dialogs.FileDialog; var dialog = saveAsDialog.createObject(QQC2.Overlay.overlay) as Dialogs.FileDialog;

View File

@@ -282,26 +282,20 @@ void RoomManager::viewEventSource(const QString &eventId)
Q_EMIT showEventSource(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()) { if (!event) {
qWarning() << "Tried to open event menu with empty event id"; qWarning() << "Tried to open event menu with empty event";
return; return;
} }
const auto it = room->findInTimeline(eventId); Q_EMIT showDelegateMenu(event->id(),
if (it == room->historyEdge()) { room->qmlSafeMember(event->senderId()),
// This is probably a pending event MessageComponentType::typeForEvent(*event),
return; EventHandler::plainBody(room, event),
} EventHandler::richBody(room, event),
const auto &event = **it; EventHandler::mediaInfo(room, event)["mimeType"_L1].toString(),
Q_EMIT showDelegateMenu(eventId, room->fileTransferInfo(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(eventId),
selectedText, selectedText,
hoveredLink); hoveredLink);
} }

View File

@@ -233,7 +233,7 @@ public:
/** /**
* @brief Show a context menu for the given event. * @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. * @brief Set a URL to be loaded as the initial room.

View File

@@ -1694,6 +1694,11 @@ std::pair<const Quotient::RoomEvent *, bool> NeoChatRoom::getEvent(const QString
return std::make_pair(extraIt != m_extraEvents.end() ? extraIt->get() : nullptr, false); 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 const RoomEvent *NeoChatRoom::getReplyForEvent(const RoomEvent &event) const
{ {
#if Quotient_VERSION_MINOR > 9 #if Quotient_VERSION_MINOR > 9

View File

@@ -538,6 +538,13 @@ public:
*/ */
std::pair<const Quotient::RoomEvent *, bool> getEvent(const QString &eventId) const; std::pair<const Quotient::RoomEvent *, bool> 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. * @brief Returns the event that is being replied to. This includes events that were manually loaded using NeoChatRoom::loadReply.
*/ */

View File

@@ -130,7 +130,10 @@ QQC2.Control {
TapHandler { TapHandler {
acceptedDevices: PointerDevice.TouchScreen 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 background: null

View File

@@ -66,7 +66,10 @@ QQC2.Control {
enabled: !quoteText.hoveredLink enabled: !quoteText.hoveredLink
acceptedDevices: PointerDevice.TouchScreen acceptedDevices: PointerDevice.TouchScreen
acceptedButtons: Qt.LeftButton 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);
}
} }
} }

View File

@@ -86,11 +86,12 @@ RowLayout {
QtObject { QtObject {
id: _private id: _private
function showMessageMenu() { function showMessageMenu(): void {
if (!NeoChatConfig.developerTools) { if (!NeoChatConfig.developerTools) {
return; 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, "", "");
} }
} }
} }

View File

@@ -97,12 +97,18 @@ TextEdit {
enabled: !root.hoveredLink enabled: !root.hoveredLink
acceptedButtons: Qt.LeftButton acceptedButtons: Qt.LeftButton
acceptedDevices: PointerDevice.TouchScreen 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 { TapHandler {
acceptedButtons: Qt.RightButton acceptedButtons: Qt.RightButton
acceptedDevices: PointerDevice.Mouse | PointerDevice.TouchPad | PointerDevice.Stylus acceptedDevices: PointerDevice.Mouse | PointerDevice.TouchPad | PointerDevice.Stylus
gesturePolicy: TapHandler.WithinBounds 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);
}
} }
} }

View File

@@ -92,8 +92,10 @@ TimelineDelegate {
QtObject { QtObject {
id: _private 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, "");
} }
} }
} }

View File

@@ -232,8 +232,9 @@ MessageDelegateBase {
QtObject { QtObject {
id: _private id: _private
function showMessageMenu() { function showMessageMenu(): void {
RoomManager.viewEventMenu(root.eventId, root.room, root.Message.selectedText, root.Message.hoveredLink); let event = root.ListView.view.model.findEvent(root.eventId);
RoomManager.viewEventMenu(event, root.room, root.Message.selectedText, root.Message.hoveredLink);
} }
} }
} }

View File

@@ -152,6 +152,23 @@ QModelIndex MessageFilterModel::indexForEventId(const QString &eventId) const
return mapFromSource(eventIndex); return mapFromSource(eventIndex);
} }
const Quotient::RoomEvent *MessageFilterModel::findEvent(const QString &eventId) const
{
// Check if sourceModel is a message model.
auto messageModel = dynamic_cast<MessageModel *>(sourceModel());
// See if it's a timeline model.
if (!messageModel) {
if (const auto timelineModel = dynamic_cast<TimelineModel *>(sourceModel())) {
messageModel = timelineModel->timelineMessageModel();
if (!messageModel) {
return nullptr;
}
}
}
return messageModel->findEvent(eventId);
}
bool MessageFilterModel::showAuthor(QModelIndex index) const bool MessageFilterModel::showAuthor(QModelIndex index) const
{ {
for (auto r = index.row() + 1; r < rowCount(); ++r) { for (auto r = index.row() + 1; r < rowCount(); ++r) {

View File

@@ -67,9 +67,13 @@ public:
/** /**
* @brief Get the QModelIndex the given event ID in the model. * @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; 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 setShowAllEvents(bool enabled);
static void setShowDeletedMessages(bool enabled); static void setShowDeletedMessages(bool enabled);

View File

@@ -351,16 +351,21 @@ QHash<int, QByteArray> MessageModel::roleNames() const
QModelIndex MessageModel::indexForEventId(const QString &eventId) const QModelIndex MessageModel::indexForEventId(const QString &eventId) const
{ {
if (m_room == nullptr) { const auto matches = match(index(0, 0), EventIdRole, eventId);
return {}; if (matches.isEmpty()) {
}
const auto it = m_room->findInTimeline(eventId);
if (it == m_room->historyEdge()) {
qWarning() << "Trying to find non-existent event:" << eventId; qWarning() << "Trying to find non-existent event:" << eventId;
return {}; 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) void MessageModel::fullEventRefresh(int row)

View File

@@ -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. * @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; 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<bool(const Quotient::RoomEvent *)> hiddenFilter); static void setHiddenFilter(std::function<bool(const Quotient::RoomEvent *)> hiddenFilter);
static void setThreadsEnabled(bool enableThreads); static void setThreadsEnabled(bool enableThreads);