Files
neochat/src/timeline/MessageDelegate.qml

384 lines
12 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
/**
* @brief The base delegate for all messages in the timeline.
*
* This supports a message bubble plus sender avatar for each message as well as reactions
* 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.
*/
TimelineDelegate {
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 message author.
*
* A Quotient::RoomMember object.
*
* @sa Quotient::RoomMember
*/
required property var author
/**
* @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 Whether the section header should be shown.
*/
required property bool showSection
/**
* @brief A model with the reactions to the message in.
*/
required property var reaction
/**
* @brief Whether the reaction component should be shown.
*/
required property bool showReactions
/**
* @brief A model with the first 5 other user read markers for this message.
*/
required property var readMarkers
/**
* @brief String with the display name and matrix ID of the other user read markers.
*/
required property string readMarkersString
/**
* @brief The number of other users at the event after the first 5.
*/
required property var excessReadMarkers
/**
* @brief Whether the other user read marker component should be shown.
*/
required property bool showReadMarkers
required property bool isThreaded
required property string threadRoot
/**
* @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 The x position of the message bubble.
*
* @note Used for positioning the hover actions.
*/
readonly property real bubbleX: bubble.x + bubble.anchors.leftMargin
/**
* @brief The y position of the message bubble.
*
* @note Used for positioning the hover actions.
*/
readonly property alias bubbleY: mainContainer.y
/**
* @brief The width of the message bubble.
*
* @note Used for sizing the hover actions.
*/
readonly property alias bubbleWidth: bubble.width
/**
* @brief Whether this message is hovered.
*/
readonly property alias hovered: bubble.hovered
/**
* @brief Open the any message media externally.
*/
signal openExternally
/**
* @brief The reply has been clicked.
*/
signal replyClicked(string eventID)
onReplyClicked: eventID => ListView.view.goToEvent(eventID)
/**
* @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
/**
* @brief Whether the message should temporarily be highlighted.
*
* Normally triggered when jumping to the event in the timeline, e.g. when a reply
* is clicked.
*/
property bool isTemporaryHighlighted: false
/**
* @brief The user selected text.
*/
property string selectedText: ""
onIsTemporaryHighlightedChanged: if (isTemporaryHighlighted) {
temporaryHighlightTimer.start();
}
Timer {
id: temporaryHighlightTimer
interval: 1500
onTriggered: isTemporaryHighlighted = false
}
/**
* @brief The width available to the bubble content.
*/
property real contentMaxWidth: bubbleSizeHelper.currentWidth - bubble.leftPadding - bubble.rightPadding
width: parent?.width
rightPadding: Config.compactLayout && root.ListView.view.width >= Kirigami.Units.gridUnit * 20 ? Kirigami.Units.gridUnit * 2 + Kirigami.Units.largeSpacing : Kirigami.Units.largeSpacing
alwaysFillWidth: Config.compactLayout
contentItem: ColumnLayout {
spacing: Kirigami.Units.smallSpacing
SectionDelegate {
id: sectionDelegate
Layout.fillWidth: true
visible: root.showSection
labelText: root.section
colorSet: Config.compactLayout || root.alwaysFillWidth ? Kirigami.Theme.View : Kirigami.Theme.Window
}
QQC2.ItemDelegate {
id: mainContainer
Layout.fillWidth: true
Layout.topMargin: root.contentModel?.showAuthor ? Kirigami.Units.largeSpacing : (Config.compactLayout ? 1 : Kirigami.Units.smallSpacing)
Layout.leftMargin: Kirigami.Units.smallSpacing
Layout.rightMargin: Kirigami.Units.smallSpacing
implicitHeight: Math.max(root.contentModel?.showAuthor ? avatar.implicitHeight : 0, bubble.height)
// show hover actions
onHoveredChanged: {
if (hovered && !Kirigami.Settings.isMobile) {
root.setHoverActionsToDelegate();
}
}
KirigamiComponents.AvatarButton {
id: avatar
width: visible || Config.showAvatarInTimeline ? Kirigami.Units.gridUnit + Kirigami.Units.largeSpacing * 2 : 0
height: width
anchors {
left: parent.left
leftMargin: Kirigami.Units.smallSpacing
top: parent.top
topMargin: Kirigami.Units.smallSpacing
}
visible: root.contentModel?.showAuthor && Config.showAvatarInTimeline && (Config.compactLayout || !_private.showUserMessageOnRight)
name: root.author.displayName
source: root.author.avatarUrl
color: root.author.color
QQC2.ToolTip.text: root.author.htmlSafeDisambiguatedName
onClicked: RoomManager.resolveResource(root.author.uri)
}
Bubble {
id: bubble
anchors.left: avatar.right
anchors.leftMargin: Kirigami.Units.largeSpacing
anchors.rightMargin: Kirigami.Units.largeSpacing
maxContentWidth: root.contentMaxWidth
topPadding: Config.compactLayout ? Kirigami.Units.smallSpacing / 2 : Kirigami.Units.largeSpacing
bottomPadding: Config.compactLayout ? Kirigami.Units.mediumSpacing / 2 : Kirigami.Units.largeSpacing
leftPadding: Config.compactLayout ? 0 : Kirigami.Units.largeSpacing + Kirigami.Units.smallSpacing
rightPadding: Kirigami.Units.largeSpacing + Kirigami.Units.smallSpacing
state: _private.showUserMessageOnRight ? "userMessageOnRight" : "userMessageOnLeft"
// states for anchor animations on window resize
// as setting anchors to undefined did not work reliably
states: [
State {
name: "userMessageOnRight"
AnchorChanges {
target: bubble
anchors.left: undefined
anchors.right: parent.right
}
},
State {
name: "userMessageOnLeft"
AnchorChanges {
target: bubble
anchors.left: avatar.right
anchors.right: undefined
}
}
]
room: root.room
index: root.index
author: root.author
contentModel: root.contentModel
actionsHandler: root.ListView.view?.actionsHandler ?? null
timeline: root.ListView.view
showHighlight: root.showHighlight
onReplyClicked: eventId => {
root.replyClicked(eventId);
}
onSelectedTextChanged: selectedText => {
root.selectedText = selectedText;
}
onShowMessageMenu: _private.showMessageMenu()
showBackground: root.cardBackground && !Config.compactLayout
}
background: Rectangle {
visible: mainContainer.hovered && (Config.compactLayout || root.alwaysFillWidth)
color: Kirigami.ColorUtils.tintWithAlpha(Kirigami.Theme.backgroundColor, Kirigami.Theme.highlightColor, 0.15)
radius: Kirigami.Units.cornerRadius
}
TapHandler {
acceptedButtons: Qt.RightButton
onTapped: _private.showMessageMenu()
}
TapHandler {
acceptedButtons: Qt.LeftButton
onLongPressed: _private.showMessageMenu()
}
}
ReactionDelegate {
Layout.maximumWidth: root.width - Kirigami.Units.largeSpacing * 2
Layout.alignment: _private.showUserMessageOnRight ? Qt.AlignRight : Qt.AlignLeft
Layout.leftMargin: _private.showUserMessageOnRight ? 0 : bubble.x + bubble.anchors.leftMargin
Layout.rightMargin: _private.showUserMessageOnRight ? Kirigami.Units.largeSpacing : 0
visible: root.showReactions
model: root.reaction
onReactionClicked: reaction => root.room.toggleReaction(root.eventId, reaction)
}
AvatarFlow {
Layout.alignment: Qt.AlignRight
Layout.rightMargin: Kirigami.Units.largeSpacing
visible: root.showReadMarkers
model: root.readMarkers
toolTipText: root.readMarkersString
excessAvatars: root.excessReadMarkers
}
DelegateSizeHelper {
id: bubbleSizeHelper
startBreakpoint: Kirigami.Units.gridUnit * 25
endBreakpoint: Kirigami.Units.gridUnit * 40
startPercentWidth: root.alwaysFillWidth ? 100 : 90
endPercentWidth: root.alwaysFillWidth ? 100 : 60
parentWidth: mainContainer.availableWidth - (Config.showAvatarInTimeline ? avatar.width + bubble.anchors.leftMargin : 0)
}
}
function isVisibleInTimeline() {
let yoff = Math.round(y - ListView.view.contentY);
return (yoff + height > 0 && yoff < ListView.view.height);
}
function setHoverActionsToDelegate() {
if (ListView.view.setHoverActionsToDelegate) {
ListView.view.setHoverActionsToDelegate(root);
}
}
QtObject {
id: _private
/**
* @brief Whether local user messages should be aligned right.
*/
property bool showUserMessageOnRight: Config.showLocalMessagesOnRight && root.author.isLocalMember && !Config.compactLayout && !root.alwaysFillWidth
function showMessageMenu() {
RoomManager.viewEventMenu(root.eventId, root.room, root.selectedText);
}
}
}