Files
neochat/src/timeline/MessageDelegate.qml
Joshua Goins 5b6e5a25e5 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.
2026-01-11 18:30:02 -05:00

241 lines
7.1 KiB
QML

// SPDX-FileCopyrightText: 2020 Black Hat <bhat@encom.eu.org>
// SPDX-License-Identifier: GPL-3.0-only
import QtQuick
import QtQuick.Controls as QQC2
import QtQuick.Layouts
import Qt.labs.qmlmodels
import org.kde.kirigami as Kirigami
import org.kde.kirigamiaddons.components as KirigamiComponents
import org.kde.neochat
import org.kde.neochat.libneochat as LibNeoChat
/**
* @brief The base delegate for all messages in the timeline.
*
* This supports a message bubble plus sender avatar for each message
* and read markers. A date section can be show for when the message is on a different
* day to the previous one.
*
* The component is designed for all messages, positioning them in the timeline with
* variable padding depending on the window width. Local user messages are highlighted
* and can also be aligned to the right if configured.
*
* This component also supports a compact mode where the padding is adjusted, the
* background is hidden and the delegate spans the full width of the timeline.
*/
MessageDelegateBase {
id: root
/**
* @brief The NeoChatRoom the delegate is being displayed in.
*/
required property NeoChatRoom room
/**
* @brief The index of the delegate in the model.
*/
required property var index
/**
* @brief The matrix ID of the message event.
*/
required property string eventId
/**
* @brief The model to visualise the content of the message.
*/
required property MessageContentModel contentModel
/**
* @brief The date of the event as a string.
*/
required property string section
/**
* @brief A model with the first 5 other user read markers for this message.
*/
required property var readMarkers
/**
* @brief The Matrix ID of the root message in the thread, if any.
*/
required property string threadRoot
/**
* @brief Whether the message in a poll.
*/
required property bool isPoll
/**
* @brief Whether this message has a local user mention.
*/
required property bool isHighlighted
/**
* @brief Whether an event is waiting to be accepted by the server.
*/
required property bool isPending
/**
* @brief Whether the event can be edited by the local user.
*/
required property bool isEditable
/**
* @brief Whether an encrypted message is sent in a verified session.
*/
required property bool verified
/**
* @brief Open the any message media externally.
*/
signal openExternally
/**
* @brief The main delegate content item to show in the bubble.
*/
property var bubbleContent
/**
* @brief Whether the bubble background is enabled.
*/
property bool cardBackground: true
/**
* @brief Whether the message should be highlighted.
*/
property bool showHighlight: root.isHighlighted || isTemporaryHighlighted
Message.room: root.room
Message.timeline: root.ListView.view
Message.contentModel: root.contentModel
Message.index: root.index
Message.maxContentWidth: maxContentWidth - bubble.leftPadding - bubble.rightPadding
width: parent?.width
enableAvatars: NeoChatConfig?.showAvatarInTimeline ?? false
compactMode: NeoChatConfig?.compactLayout ?? false
showLocalMessagesOnRight: NeoChatConfig.showLocalMessagesOnRight
contentItem: Bubble {
id: bubble
topPadding: NeoChatConfig.compactLayout ? Kirigami.Units.smallSpacing / 2 : Kirigami.Units.largeSpacing
bottomPadding: NeoChatConfig.compactLayout ? Kirigami.Units.mediumSpacing / 2 : Kirigami.Units.largeSpacing
leftPadding: NeoChatConfig.compactLayout ? 0 : Kirigami.Units.largeSpacing + Kirigami.Units.smallSpacing
rightPadding: Kirigami.Units.largeSpacing + Kirigami.Units.smallSpacing
author: root.author
showAuthor: root.showAuthor
isThreaded: root.isThreaded
contentModel: root.contentModel
showHighlight: root.showHighlight
isPending: root.isPending
onSelectedTextChanged: selectedText => {
root.Message.selectedText = selectedText;
}
onHoveredLinkChanged: hoveredLink => {
root.Message.hoveredLink = hoveredLink;
}
showBackground: root.cardBackground && !NeoChatConfig.compactLayout
TapHandler {
acceptedDevices: PointerDevice.Mouse | PointerDevice.TouchPad | PointerDevice.Stylus
acceptedButtons: Qt.RightButton
onTapped: _private.showMessageMenu()
}
TapHandler {
acceptedDevices: PointerDevice.TouchScreen
acceptedButtons: Qt.LeftButton
onLongPressed: _private.showMessageMenu()
}
}
avatarComponent: KirigamiComponents.Avatar {
id: avatar
implicitWidth: Kirigami.Units.gridUnit + Kirigami.Units.largeSpacing * 2
implicitHeight: width
name: root.author.displayName
source: root.author.avatarUrl
color: root.author.color
asynchronous: true
QQC2.ToolTip.text: root.author.htmlSafeDisambiguatedName
function openUserMenu(): void {
const menu = Qt.createComponent("org.kde.neochat", "UserMenu").createObject(root, {
window: QQC2.ApplicationWindow.window as Kirigami.ApplicationWindow,
author: root.author,
});
console.info(Qt.createComponent("org.kde.neochat", "UserMenu").errorString());
menu.popup(root.QQC2.Overlay.overlay);
}
HoverHandler {
cursorShape: Qt.PointingHandCursor
}
// tapping to open profile
TapHandler {
onTapped: RoomManager.resolveResource(root.author.uri)
}
// right-clicking/long-press for context menu
TapHandler {
acceptedDevices: PointerDevice.Mouse | PointerDevice.TouchPad | PointerDevice.Stylus
acceptedButtons: Qt.RightButton
onTapped: avatar.openUserMenu()
}
TapHandler {
acceptedDevices: PointerDevice.TouchScreen
onTapped: avatar.openUserMenu()
}
}
sectionComponent: Kirigami.ListSectionHeader {
horizontalPadding: 0
text: root.section
}
readMarkerComponent: AvatarFlow {
model: root.readMarkers
TapHandler {
onTapped: {
const dialog = Qt.createComponent("org.kde.neochat", "SeenByDialog").createObject(root, {
model: root.readMarkers
}) as SeenByDialog;
dialog.open();
}
}
}
compactBackgroundComponent: Rectangle {
color: Kirigami.ColorUtils.tintWithAlpha(Kirigami.Theme.backgroundColor, Kirigami.Theme.highlightColor, 0.15)
radius: Kirigami.Units.cornerRadius
}
quickActionComponent: QuickActions {
room: root.room
eventId: root.eventId
}
QtObject {
id: _private
function showMessageMenu(): void {
let event = root.ListView.view.model.findEvent(root.eventId);
RoomManager.viewEventMenu(event, root.room, root.Message.selectedText, root.Message.hoveredLink);
}
}
}