Fix the Timeline Part 1

This introduces a new base delegate that handles sizing the content of delegate in the timeline, i.e. it handles all the size helper stuff. This is then used for all the other main delegates:
- messages
- state
- read marker

This means they now all have identical base code to do the sizing (read marker still had legacy code).

Because the new base delegate is called `TimelineDelegate` both `TimelineContainer` and `MessageDelegate` have been renamed:
- MessageDelegate -> TextDelegate - this never made sense before images, videos, etc are all technically messages in Matrix parlance
- TimelineContainer -> MessageDelegate - this has always really been the base for messages

Note - this is mostly groundwork for dealing with the layout polish loop spam which will hopefully be fixed in part 2 with a bubble rework.
This commit is contained in:
James Graham
2023-09-21 16:26:34 +00:00
parent 69087c2117
commit e926b22524
19 changed files with 904 additions and 848 deletions

View File

@@ -13,9 +13,9 @@ import org.kde.neochat 1.0
/**
* @brief A timeline delegate for an audio message.
*
* @inherit TimelineContainer
* @inherit MessageDelegate
*/
TimelineContainer {
MessageDelegate {
id: root
/**

View File

@@ -10,9 +10,9 @@ import org.kde.neochat 1.0
/**
* @brief A timeline delegate for an encrypted message that can't be decrypted.
*
* @inherit TimelineContainer
* @inherit MessageDelegate
*/
TimelineContainer {
MessageDelegate {
id: encryptedDelegate
innerObject: TextEdit {

View File

@@ -22,21 +22,21 @@ DelegateChooser {
DelegateChoice {
roleValue: DelegateType.Emote
delegate: MessageDelegate {
delegate: TextDelegate {
connection: root.connection
}
}
DelegateChoice {
roleValue: DelegateType.Message
delegate: MessageDelegate {
delegate: TextDelegate {
connection: root.connection
}
}
DelegateChoice {
roleValue: DelegateType.Notice
delegate: MessageDelegate {
delegate: TextDelegate {
connection: root.connection
}
}

View File

@@ -13,9 +13,9 @@ import org.kde.neochat 1.0
/**
* @brief A timeline delegate for an file message.
*
* @inherit TimelineContainer
* @inherit MessageDelegate
*/
TimelineContainer {
MessageDelegate {
id: root
/**

View File

@@ -14,9 +14,9 @@ import org.kde.neochat 1.0
/**
* @brief A timeline delegate for an image message.
*
* @inherit TimelineContainer
* @inherit MessageDelegate
*/
TimelineContainer {
MessageDelegate {
id: root
/**

View File

@@ -14,9 +14,9 @@ import org.kde.neochat 1.0
/**
* @brief A timeline delegate for a location message.
*
* @inherit TimelineContainer
* @inherit MessageDelegate
*/
TimelineContainer {
MessageDelegate {
id: root
property alias room: liveLocationModel.room

View File

@@ -13,9 +13,9 @@ import org.kde.neochat 1.0
/**
* @brief A timeline delegate for a location message.
*
* @inherit TimelineContainer
* @inherit MessageDelegate
*/
TimelineContainer {
MessageDelegate {
id: root
/**

View File

@@ -1,76 +1,583 @@
// SPDX-FileCopyrightText: 2021 Tobias Fella <tobias.fella@kde.org>
// SPDX-FileCopyrightText: 2020 Black Hat <bhat@encom.eu.org>
// SPDX-License-Identifier: GPL-3.0-only
import QtQuick 2.15
import QtQuick.Controls 2.15 as QQC2
import QtQuick.Layouts 1.15
import Qt.labs.qmlmodels 1.0
import org.kde.kirigami 2.15 as Kirigami
import org.kde.kirigamiaddons.labs.components 1.0 as KirigamiComponents
import org.kde.neochat 1.0
/**
* @brief A timeline delegate for an text message.
* @brief The base delegate for all messages in the timeline.
*
* @inherit TimelineContainer
* 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.
*/
TimelineContainer {
TimelineDelegate {
id: root
/**
* @brief The link preview properties.
*
* This is a list or object containing the following:
* - url - The URL being previewed.
* - loaded - Whether the URL preview has been loaded.
* - title - the title of the URL preview.
* - description - the description of the URL preview.
* - imageSource - a source URL for the preview image.
*
* @note An empty link previewer should be passed if there are no links to
* preview.
*/
required property var linkPreview
* @brief The index of the delegate in the model.
*/
required property var index
/**
* @brief Whether there are any links to preview.
* @brief The matrix ID of the message event.
*/
required property string eventId
/**
* @brief The timestamp of the message.
*/
required property var time
/**
* @brief The timestamp of the message as a string.
*/
required property string timeString
/**
* @brief The message author.
*
* This should consist of the following:
* - id - The matrix ID of the author.
* - isLocalUser - Whether the author is the local user.
* - avatarSource - The mxc URL for the author's avatar in the current room.
* - avatarMediaId - The media ID of the author's avatar.
* - avatarUrl - The mxc URL for the author's avatar.
* - displayName - The display name of the author.
* - display - The name of the author.
* - color - The color for the author.
* - object - The Quotient::User object for the author.
*
* @sa Quotient::User
*/
required property var author
/**
* @brief Whether the author should be shown.
*/
required property bool showAuthor
/**
* @brief Whether the author should always be shown.
*
* This is primarily used when these delegates are used in a filtered list of
* events rather than a sequential timeline, e.g. the media model view.
*
* @note This setting still respects the avatar configuration settings.
*/
property bool alwaysShowAuthor: false
/**
* @brief The delegate type of the message.
*/
required property int delegateType
/**
* @brief The display text of the message.
*/
required property string display
/**
* @brief The display text of the message as plain text.
*/
required property string plainText
/**
* @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
/**
* @brief The matrix ID of the reply event.
*/
required property var replyId
/**
* @brief The reply author.
*
* This should consist of the following:
* - id - The matrix ID of the reply author.
* - isLocalUser - Whether the reply author is the local user.
* - avatarSource - The mxc URL for the reply author's avatar in the current room.
* - avatarMediaId - The media ID of the reply author's avatar.
* - avatarUrl - The mxc URL for the reply author's avatar.
* - displayName - The display name of the reply author.
* - display - The name of the reply author.
* - color - The color for the reply author.
* - object - The Quotient::User object for the reply author.
*
* @sa Quotient::User
*/
required property var replyAuthor
/**
* @brief The delegate type of the message replied to.
*/
required property int replyDelegateType
/**
* @brief The display text of the message replied to.
*/
required property string replyDisplay
/**
* @brief The media info for the reply event.
*
* This could be an image, audio, video or file.
*
* This should consist of the following:
* - source - The mxc URL for the media.
* - mimeType - The MIME type of the media.
* - mimeIcon - The MIME icon name.
* - size - The file size in bytes.
* - duration - The length in seconds of the audio media (audio/video only).
* - width - The width in pixels of the audio media (image/video only).
* - height - The height in pixels of the audio media (image/video only).
* - tempInfo - mediaInfo (with the same properties as this except no tempInfo) for a temporary image while the file downloads (image/video only).
*/
required property var replyMediaInfo
/**
* @brief Whether this message is replying to another.
*/
required property bool isReply
/**
* @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 Progress info when downloading files.
*
* @sa Quotient::FileTransferInfo
*/
required property var progressInfo
/**
* @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
required property NeoChatConnection connection
/**
* @brief Open the context menu for the message.
*/
signal openContextMenu
/**
* @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 component to display the delegate type.
*
* This is used by the inherited delegates to assign a component to visualise
* the message content for that delegate type.
*/
default property alias innerObject : column.children
/**
* @brief Whether the bubble background is enabled.
*/
property bool cardBackground: true
/**
* @brief Whether the delegate should always stretch to the maximum availabel width.
*/
property bool alwaysMaxWidth: false
/**
* @brief Whether local user messages should be aligned right.
*
* TODO: make private
*/
property bool showUserMessageOnRight: Config.showLocalMessagesOnRight && root.author.isLocalUser && !Config.compactLayout && !alwaysMaxWidth
/**
* @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
onIsTemporaryHighlightedChanged: if (isTemporaryHighlighted) temporaryHighlightTimer.start()
Timer {
id: temporaryHighlightTimer
interval: 1500
onTriggered: isTemporaryHighlighted = false
}
/**
* @brief The width available to the bubble content.
*/
required property bool showLinkPreview
property alias contentMaxWidth: bubbleSizeHelper.currentWidth
onOpenContextMenu: RoomManager.viewEventMenu(eventId, author, delegateType, plainText, display, label.selectedText)
contentItem: ColumnLayout {
spacing: Kirigami.Units.smallSpacing
innerObject: ColumnLayout {
Layout.maximumWidth: root.contentMaxWidth
RichLabel {
id: label
SectionDelegate {
id: sectionDelegate
Layout.fillWidth: true
visible: currentRoom.chatBoxEditId !== root.eventId
visible: root.showSection
labelText: root.section
colorSet: Config.compactLayout || root.alwaysMaxWidth ? Kirigami.Theme.View : Kirigami.Theme.Window
}
QQC2.ItemDelegate {
id: mainContainer
isReply: root.isReply
Layout.fillWidth: true
Layout.topMargin: root.showAuthor || root.alwaysShowAuthor ? Kirigami.Units.largeSpacing : (Config.compactLayout ? 1 : Kirigami.Units.smallSpacing)
Layout.leftMargin: Kirigami.Units.smallSpacing
Layout.rightMargin: Kirigami.Units.smallSpacing
textMessage: root.display
implicitHeight: Math.max(root.showAuthor || root.alwaysShowAuthor ? avatar.implicitHeight : 0, bubble.height)
Component.onCompleted: {
if (root.isReply && root.replyDelegateType === DelegateType.Other) {
currentRoom.loadReply(root.eventId, root.replyId)
}
}
// show hover actions
onHoveredChanged: {
if (hovered && !Kirigami.Settings.isMobile) {
root.setHoverActionsToDelegate()
}
}
KirigamiComponents.Avatar {
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.showAuthor || root.alwaysShowAuthor) &&
Config.showAvatarInTimeline &&
(Config.compactLayout || !showUserMessageOnRight)
name: root.author.displayName
source: root.author.avatarSource
color: root.author.color
MouseArea {
anchors.fill: parent
onClicked: {
RoomManager.visitUser(root.author.object, "mention")
}
cursorShape: Qt.PointingHandCursor
}
}
QQC2.Control {
id: bubble
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
hoverEnabled: true
anchors {
left: avatar.right
leftMargin: Kirigami.Units.largeSpacing
rightMargin: Kirigami.Units.largeSpacing
}
// HACK: anchoring didn't reset anchors.right when switching from parent.right to undefined reliably
width: Config.compactLayout || root.alwaysMaxWidth ? mainContainer.width - (Config.showAvatarInTimeline ? Kirigami.Units.gridUnit * 2 : 0) + Kirigami.Units.largeSpacing * 2 : implicitWidth
state: 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
}
}
]
transitions: [
Transition {
AnchorAnimation{duration: Kirigami.Units.longDuration; easing.type: Easing.OutCubic}
}
]
contentItem: RowLayout {
Kirigami.Icon {
source: "content-loading-symbolic"
width: height
Layout.preferredWidth: Kirigami.Units.iconSizes.small
Layout.preferredHeight: Kirigami.Units.iconSizes.small
visible: root.isPending && Config.showLocalMessagesOnRight
}
ColumnLayout {
id: column
spacing: Kirigami.Units.smallSpacing
RowLayout {
id: rowLayout
spacing: Kirigami.Units.smallSpacing
visible: root.showAuthor || root.alwaysShowAuthor
QQC2.Label {
id: nameLabel
Layout.maximumWidth: contentMaxWidth - timeLabel.implicitWidth - rowLayout.spacing
text: visible ? root.author.displayName : ""
textFormat: Text.PlainText
font.weight: Font.Bold
color: root.author.color
elide: Text.ElideRight
MouseArea {
anchors.fill: parent
cursorShape: Qt.PointingHandCursor
onClicked: {
RoomManager.visitUser(root.author.object, "mention")
}
}
}
QQC2.Label {
id: timeLabel
text: root.timeString
color: Kirigami.Theme.disabledTextColor
QQC2.ToolTip.visible: hoverHandler.hovered
QQC2.ToolTip.text: root.time.toLocaleString(Qt.locale(), Locale.LongFormat)
QQC2.ToolTip.delay: Kirigami.Units.toolTipDelay
HoverHandler {
id: hoverHandler
}
}
}
Loader {
id: replyLoader
Layout.maximumWidth: contentMaxWidth
active: root.isReply && root.replyDelegateType !== DelegateType.Other
visible: active
sourceComponent: ReplyComponent {
author: root.replyAuthor
type: root.replyDelegateType
display: root.replyDisplay
mediaInfo: root.replyMediaInfo
contentMaxWidth: bubbleSizeHelper.currentWidth
}
Connections {
target: replyLoader.item
function onReplyClicked() {
replyClicked(root.replyId)
}
}
}
}
Kirigami.Icon {
source: "content-loading-symbolic"
width: height
Layout.preferredWidth: Kirigami.Units.iconSizes.small
Layout.preferredHeight: Kirigami.Units.iconSizes.small
visible: root.isPending && !Config.showLocalMessagesOnRight
}
}
background: Item {
Kirigami.ShadowedRectangle {
id: bubbleBackground
visible: cardBackground && !Config.compactLayout
anchors.fill: parent
Kirigami.Theme.colorSet: Kirigami.Theme.View
Kirigami.Theme.inherit: false
color: if (root.author.isLocalUser) {
return Kirigami.ColorUtils.tintWithAlpha(Kirigami.Theme.backgroundColor, Kirigami.Theme.highlightColor, 0.15)
} else if (root.showHighlight) {
return Kirigami.Theme.positiveBackgroundColor
} else {
return Kirigami.Theme.backgroundColor
}
radius: Kirigami.Units.smallSpacing
shadow {
size: Kirigami.Units.smallSpacing
color: root.isHighlighted ? Qt.rgba(0.0, 0.0, 0.0, 0.10) : Qt.rgba(Kirigami.Theme.textColor.r, Kirigami.Theme.textColor.g, Kirigami.Theme.textColor.b, 0.10)
}
Behavior on color {
ColorAnimation { duration: Kirigami.Units.shortDuration }
}
}
}
}
background: Rectangle {
visible: mainContainer.hovered && (Config.compactLayout || root.alwaysMaxWidth)
color: Kirigami.ColorUtils.tintWithAlpha(Kirigami.Theme.backgroundColor, Kirigami.Theme.highlightColor, 0.15)
radius: Kirigami.Units.smallSpacing
}
TapHandler {
acceptedButtons: Qt.RightButton
onTapped: root.openContextMenu()
}
TapHandler {
enabled: !label.hoveredLink
acceptedButtons: Qt.LeftButton
onLongPressed: root.openContextMenu()
}
}
Loader {
Layout.fillWidth: true
Layout.minimumHeight: item ? item.minimumHeight : -1
Layout.preferredWidth: item ? item.preferredWidth : -1
visible: currentRoom.chatBoxEditId === root.eventId
active: visible
sourceComponent: MessageEditComponent {
room: currentRoom
messageId: root.eventId
}
ReactionDelegate {
Layout.maximumWidth: root.width - Kirigami.Units.largeSpacing * 2
Layout.alignment: showUserMessageOnRight ? Qt.AlignRight : Qt.AlignLeft
Layout.leftMargin: showUserMessageOnRight ? 0 : bubble.x + bubble.anchors.leftMargin
Layout.rightMargin: showUserMessageOnRight ? Kirigami.Units.largeSpacing : 0
visible: root.showReactions
model: root.reaction
onReactionClicked: (reaction) => currentRoom.toggleReaction(root.eventId, reaction)
}
LinkPreviewDelegate {
Layout.fillWidth: true
active: !currentRoom.usesEncryption && currentRoom.urlPreviewEnabled && Config.showLinkPreview && root.showLinkPreview && !root.linkPreview.empty
linkPreviewer: root.linkPreview
indicatorEnabled: root.isVisibleInTimeline()
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: Config.compactLayout || root.alwaysMaxWidth ? 100 : 90
endPercentWidth: Config.compactLayout || root.alwaysMaxWidth ? 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)
}
}
}

View File

@@ -12,9 +12,9 @@ import org.kde.neochat 1.0
/**
* @brief A timeline delegate for a poll message.
*
* @inherit TimelineContainer
* @inherit MessageDelegate
*/
TimelineContainer {
MessageDelegate {
id: root
/**

View File

@@ -10,86 +10,49 @@ import org.kde.kirigami 2.15 as Kirigami
import org.kde.neochat 1.0
QQC2.ItemDelegate {
TimelineDelegate {
id: root
contentItem: QQC2.ItemDelegate {
padding: Kirigami.Units.largeSpacing
topInset: Kirigami.Units.largeSpacing
topPadding: Kirigami.Units.largeSpacing * 2
padding: Kirigami.Units.largeSpacing
topInset: Kirigami.Units.largeSpacing
topPadding: Kirigami.Units.largeSpacing * 2
property bool isTemporaryHighlighted: false
// extraWidth defines how the delegate can grow after the listView gets very wide
readonly property int extraWidth: parent ? (parent.width >= Kirigami.Units.gridUnit * 46 ? Math.min((parent.width - Kirigami.Units.gridUnit * 46), Kirigami.Units.gridUnit * 20) : 0) : 0
readonly property int delegateMaxWidth: parent ? (Config.compactLayout ? parent.width - Kirigami.Units.largeSpacing * 2 : Math.min(parent.width - Kirigami.Units.largeSpacing * 2, Kirigami.Units.gridUnit * 40 + extraWidth)) : 0
onIsTemporaryHighlightedChanged: if (isTemporaryHighlighted) temporaryHighlightTimer.start()
property bool isTemporaryHighlighted: false
Timer {
id: temporaryHighlightTimer
onIsTemporaryHighlightedChanged: if (isTemporaryHighlighted) temporaryHighlightTimer.start()
Timer {
id: temporaryHighlightTimer
interval: 1500
onTriggered: isTemporaryHighlighted = false
}
width: delegateMaxWidth
anchors.leftMargin: Kirigami.Units.largeSpacing
anchors.rightMargin: Kirigami.Units.largeSpacing
state: Config.compactLayout ? "alignLeft" : "alignCenter"
// Align left when in compact mode and center when using bubbles
states: [
State {
name: "alignLeft"
AnchorChanges {
target: root
anchors.horizontalCenter: undefined
anchors.left: parent ? parent.left : undefined
}
},
State {
name: "alignCenter"
AnchorChanges {
target: root
anchors.horizontalCenter: parent ? parent.horizontalCenter : undefined
anchors.left: undefined
}
interval: 1500
onTriggered: isTemporaryHighlighted = false
}
]
transitions: [
Transition {
AnchorAnimation {
duration: Kirigami.Units.longDuration
easing.type: Easing.OutCubic
}
contentItem: QQC2.Label {
text: i18nc("Relative time since the room was last read", "Last read: %1", time)
}
]
contentItem: QQC2.Label {
text: i18nc("Relative time since the room was last read", "Last read: %1", time)
}
background: Kirigami.ShadowedRectangle {
id: readMarkerBackground
color: {
if (root.isTemporaryHighlighted) {
return Kirigami.Theme.positiveBackgroundColor
} else {
return Kirigami.Theme.backgroundColor
background: Kirigami.ShadowedRectangle {
id: readMarkerBackground
color: {
if (root.isTemporaryHighlighted) {
return Kirigami.Theme.positiveBackgroundColor
} else {
return Kirigami.Theme.backgroundColor
}
}
}
Kirigami.Theme.inherit: false
Kirigami.Theme.colorSet: Kirigami.Theme.View
opacity: root.isTemporaryHighlighted ? 1 : 0.6
radius: Kirigami.Units.smallSpacing
shadow.size: Kirigami.Units.smallSpacing
shadow.color: Qt.rgba(0.0, 0.0, 0.0, 0.10)
border.color: Kirigami.ColorUtils.tintWithAlpha(color, Kirigami.Theme.textColor, 0.15)
border.width: 1
Kirigami.Theme.inherit: false
Kirigami.Theme.colorSet: Kirigami.Theme.View
opacity: root.isTemporaryHighlighted ? 1 : 0.6
radius: Kirigami.Units.smallSpacing
shadow.size: Kirigami.Units.smallSpacing
shadow.color: Qt.rgba(0.0, 0.0, 0.0, 0.10)
border.color: Kirigami.ColorUtils.tintWithAlpha(color, Kirigami.Theme.textColor, 0.15)
border.width: 1
Behavior on color {
ColorAnimation {target: readMarkerBackground; duration: Kirigami.Units.veryLongDuration; easing.type: Easing.InOutCubic}
Behavior on color {
ColorAnimation {target: readMarkerBackground; duration: Kirigami.Units.veryLongDuration; easing.type: Easing.InOutCubic}
}
}
}
}

View File

@@ -18,6 +18,8 @@ QQC2.ItemDelegate {
property int colorSet: Kirigami.Theme.Window
leftPadding: 0
rightPadding: 0
topPadding: Kirigami.Units.largeSpacing
bottomPadding: 0 // Note not 0 by default

View File

@@ -10,17 +10,44 @@ import org.kde.kirigamiaddons.labs.components 1.0 as KirigamiComponents
import org.kde.neochat 1.0
/**
* @brief A component for visualising a single state event
*/
RowLayout {
id: root
property var name
property alias avatar: stateAvatar.source
property var color
property alias text: label.text
signal avatarClicked()
signal linkClicked(string link)
/**
* @brief All model roles as a map with the property names as the keys.
*/
required property var modelData
implicitHeight: Math.max(label.contentHeight, stateAvatar.implicitHeight)
/**
* @brief The message author.
*
* This should consist of the following:
* - id - The matrix ID of the author.
* - isLocalUser - Whether the author is the local user.
* - avatarSource - The mxc URL for the author's avatar in the current room.
* - avatarMediaId - The media ID of the author's avatar.
* - avatarUrl - The mxc URL for the author's avatar.
* - displayName - The display name of the author.
* - display - The name of the author.
* - color - The color for the author.
* - object - The Quotient::User object for the author.
*
* @sa Quotient::User
*/
property var author: modelData.author
/**
* @brief The displayname for the event's sender; for name change events, the old displayname.
*/
property string authorDisplayName: modelData.authorDisplayName
/**
* @brief The display text for the state event.
*/
property string text: modelData.text
KirigamiComponents.Avatar {
id: stateAvatar
@@ -28,21 +55,22 @@ RowLayout {
Layout.preferredWidth: Kirigami.Units.iconSizes.small
Layout.preferredHeight: Kirigami.Units.iconSizes.small
name: root.name
color: root.color
source: root.author?.avatarUrl ?? ""
name: root.author?.displayName ?? ""
color: root.author?.color ?? undefined
Rectangle {
radius: height
height: 4
width: 4
color: root.color
color: root.author.color
anchors.centerIn: parent
}
MouseArea {
anchors.fill: parent
cursorShape: Qt.PointingHandCursor
onClicked: avatarClicked()
onClicked: RoomManager.openResource("https://matrix.to/#/" + root.author.id)
}
}
@@ -50,8 +78,9 @@ RowLayout {
id: label
Layout.alignment: Qt.AlignVCenter
Layout.fillWidth: true
text: `<style>a {text-decoration: none;}</style><a href="https://matrix.to/#/${root.author.id}" style="color: ${root.author.color}">${root.authorDisplayName}</a> ${root.text}`
wrapMode: Text.WordWrap
textFormat: Text.RichText
onLinkActivated: link => linkClicked(link)
onLinkActivated: link => RoomManager.openResource(link)
}
}

View File

@@ -10,72 +10,88 @@ import org.kde.kirigamiaddons.labs.components 1.0 as KirigamiComponents
import org.kde.neochat 1.0
QQC2.Control {
/**
* @brief A timeline delegate for visualising an aggregated list of consecutive state events.
*
* @inherit TimelineDelegate
*/
TimelineDelegate {
id: root
readonly property bool sectionVisible: model.showSection
/**
* @brief List of the first 5 unique authors of the aggregated state event.
*/
required property var authorList
width: stateDelegateSizeHelper.currentWidth
/**
* @brief The number of unique authors beyond the first 5.
*/
required property string excessAuthors
state: Config.compactLayout ? "alignLeft" : "alignCenter"
// Align left when in compact mode and center when using bubbles
states: [
State {
name: "alignLeft"
AnchorChanges {
target: root
anchors.horizontalCenter: undefined
anchors.left: parent ? parent.left : undefined
}
},
State {
name: "alignCenter"
AnchorChanges {
target: root
anchors.horizontalCenter: parent ? parent.horizontalCenter : undefined
anchors.left: undefined
}
}
]
/**
* @brief Single line aggregation of all the state events.
*/
required property string aggregateDisplay
transitions: [
Transition {
AnchorAnimation{duration: Kirigami.Units.longDuration; easing.type: Easing.OutCubic}
}
]
/**
* @brief List of state events in the aggregated state.
*/
required property var stateEvents
height: columnLayout.implicitHeight + columnLayout.anchors.topMargin
/**
* @brief Whether the section header should be shown.
*/
required property bool showSection
ColumnLayout {
id: columnLayout
/**
* @brief The date of the event as a string.
*/
required property string section
property bool folded: true
/**
* @brief A model with the first 5 other user read markers for this message.
*/
required property var readMarkers
spacing: sectionVisible ? Kirigami.Units.largeSpacing : 0
anchors.top: parent.top
anchors.topMargin: sectionVisible ? 0 : Kirigami.Units.largeSpacing
anchors.left: parent.left
anchors.right: parent.right
/**
* @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
/**
* @brief Whether the state event is folded to a single line.
*/
property bool folded: true
contentItem: ColumnLayout {
SectionDelegate {
id: sectionDelegate
Layout.fillWidth: true
visible: sectionVisible
labelText: sectionVisible ? section : ""
visible: root.showSection
labelText: root.section
colorSet: Config.compactLayout ? Kirigami.Theme.View : Kirigami.Theme.Window
}
RowLayout {
Layout.fillWidth: true
Layout.leftMargin: Kirigami.Units.gridUnit * 1.5 + Kirigami.Units.smallSpacing * 1.5 + (Config.compactLayout ? Kirigami.Units.largeSpacing * 1.25 : 0)
Layout.rightMargin: Kirigami.Units.largeSpacing
Layout.leftMargin: Kirigami.Units.largeSpacing * 1.5
Layout.rightMargin: Kirigami.Units.largeSpacing * 1.5
visible: stateEventRepeater.count !== 1
Flow {
visible: columnLayout.folded
visible: root.folded
spacing: -Kirigami.Units.iconSizes.small / 2
Repeater {
model: authorList
model: root.authorList
delegate: Item {
id: avatarDelegate
@@ -108,8 +124,8 @@ QQC2.Control {
QQC2.Label {
id: excessAuthorsLabel
text: model.excessAuthors
visible: model.excessAuthors !== ""
text: root.excessAuthors
visible: root.excessAuthors !== ""
color: Kirigami.Theme.textColor
horizontalAlignment: Text.AlignHCenter
background: Kirigami.ShadowedRectangle {
@@ -141,9 +157,9 @@ QQC2.Control {
}
QQC2.Label {
Layout.fillWidth: true
visible: columnLayout.folded
visible: root.folded
text: aggregateDisplay
text: root.aggregateDisplay
elide: Qt.ElideRight
textFormat: Text.RichText
wrapMode: Text.WordWrap
@@ -151,59 +167,41 @@ QQC2.Control {
}
Item {
Layout.fillWidth: true
visible: !columnLayout.folded
implicitHeight: foldButton.implicitHeight
visible: !root.folded
}
QQC2.ToolButton {
id: foldButton
icon {
name: (!columnLayout.folded ? "go-up" : "go-down")
name: (!root.folded ? "go-up" : "go-down")
width: Kirigami.Units.iconSizes.small
height: Kirigami.Units.iconSizes.small
}
onClicked: columnLayout.toggleFolded()
onClicked: root.toggleFolded()
}
}
Repeater {
id: stateEventRepeater
model: stateEvents
model: root.stateEvents
delegate: StateComponent {
Layout.fillWidth: true
Layout.leftMargin: Kirigami.Units.gridUnit * 1.5 + Kirigami.Units.smallSpacing * 1.5 + (Config.compactLayout ? Kirigami.Units.largeSpacing * 1.25 : 0)
Layout.rightMargin: Kirigami.Units.largeSpacing
visible: !columnLayout.folded || stateEventRepeater.count === 1
name: modelData.author.displayName
avatar: modelData.author.avatarSource
color: modelData.author.color
text: `<style>a {text-decoration: none;}</style><a href="https://matrix.to/#/${modelData.author.id}" style="color: ${modelData.author.color}">${modelData.authorDisplayName}</a> ${modelData.text}`
onAvatarClicked: RoomManager.openResource("https://matrix.to/#/" + modelData.author.id)
onLinkClicked: link => RoomManager.openResource(link)
Layout.leftMargin: Kirigami.Units.largeSpacing * 1.5
Layout.rightMargin: Kirigami.Units.largeSpacing * 1.5
visible: !root.folded || stateEventRepeater.count === 1
}
}
function toggleFolded() {
folded = !folded
foldedChanged()
}
AvatarFlow {
Layout.alignment: Qt.AlignRight
Layout.rightMargin: Kirigami.Units.largeSpacing
visible: showReadMarkers
model: readMarkers
toolTipText: readMarkersString
excessAvatars: excessReadMarkers
visible: root.showReadMarkers
model: root.readMarkers
toolTipText: root.readMarkersString
excessAvatars: root.excessReadMarkers
}
}
DelegateSizeHelper {
id: stateDelegateSizeHelper
startBreakpoint: Kirigami.Units.gridUnit * 46
endBreakpoint: Kirigami.Units.gridUnit * 66
startPercentWidth: 100
endPercentWidth: Config.compactLayout ? 100 : 85
maxWidth: Config.compactLayout ? -1 : Kirigami.Units.gridUnit * 60
parentWidth: root.parent ? root.parent.width : 0
function toggleFolded() {
folded = !folded
foldedChanged()
}
}

View File

@@ -0,0 +1,76 @@
// SPDX-FileCopyrightText: 2021 Tobias Fella <tobias.fella@kde.org>
// SPDX-License-Identifier: GPL-3.0-only
import QtQuick 2.15
import QtQuick.Layouts 1.15
import Qt.labs.qmlmodels 1.0
import org.kde.neochat 1.0
/**
* @brief A timeline delegate for a text message.
*
* @inherit MessageDelegate
*/
MessageDelegate {
id: root
/**
* @brief The link preview properties.
*
* This is a list or object containing the following:
* - url - The URL being previewed.
* - loaded - Whether the URL preview has been loaded.
* - title - the title of the URL preview.
* - description - the description of the URL preview.
* - imageSource - a source URL for the preview image.
*
* @note An empty link previewer should be passed if there are no links to
* preview.
*/
required property var linkPreview
/**
* @brief Whether there are any links to preview.
*/
required property bool showLinkPreview
onOpenContextMenu: RoomManager.viewEventMenu(eventId, author, delegateType, plainText, display, label.selectedText)
innerObject: ColumnLayout {
Layout.maximumWidth: root.contentMaxWidth
RichLabel {
id: label
Layout.fillWidth: true
visible: currentRoom.chatBoxEditId !== root.eventId
isReply: root.isReply
textMessage: root.display
TapHandler {
enabled: !label.hoveredLink
acceptedButtons: Qt.LeftButton
onLongPressed: root.openContextMenu()
}
}
Loader {
Layout.fillWidth: true
Layout.minimumHeight: item ? item.minimumHeight : -1
Layout.preferredWidth: item ? item.preferredWidth : -1
visible: currentRoom.chatBoxEditId === root.eventId
active: visible
sourceComponent: MessageEditComponent {
room: currentRoom
messageId: root.eventId
}
}
LinkPreviewDelegate {
Layout.fillWidth: true
active: !currentRoom.usesEncryption && currentRoom.urlPreviewEnabled && Config.showLinkPreview && root.showLinkPreview && !root.linkPreview.empty
linkPreviewer: root.linkPreview
indicatorEnabled: root.isVisibleInTimeline()
}
}
}

View File

@@ -1,617 +0,0 @@
// SPDX-FileCopyrightText: 2020 Black Hat <bhat@encom.eu.org>
// SPDX-License-Identifier: GPL-3.0-only
import QtQuick 2.15
import QtQuick.Controls 2.15 as QQC2
import QtQuick.Layouts 1.15
import org.kde.kirigami 2.15 as Kirigami
import org.kde.kirigamiaddons.labs.components 1.0 as KirigamiComponents
import org.kde.neochat 1.0
/**
* @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.
*/
ColumnLayout {
id: root
/**
* @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 timestamp of the message.
*/
required property var time
/**
* @brief The timestamp of the message as a string.
*/
required property string timeString
/**
* @brief The message author.
*
* This should consist of the following:
* - id - The matrix ID of the author.
* - isLocalUser - Whether the author is the local user.
* - avatarSource - The mxc URL for the author's avatar in the current room.
* - avatarMediaId - The media ID of the author's avatar.
* - avatarUrl - The mxc URL for the author's avatar.
* - displayName - The display name of the author.
* - display - The name of the author.
* - color - The color for the author.
* - object - The Quotient::User object for the author.
*
* @sa Quotient::User
*/
required property var author
/**
* @brief Whether the author should be shown.
*/
required property bool showAuthor
/**
* @brief Whether the author should always be shown.
*
* This is primarily used when these delegates are used in a filtered list of
* events rather than a sequential timeline, e.g. the media model view.
*
* @note This setting still respects the avatar configuration settings.
*/
property bool alwaysShowAuthor: false
/**
* @brief The delegate type of the message.
*/
required property int delegateType
/**
* @brief The display text of the message.
*/
required property string display
/**
* @brief The display text of the message as plain text.
*/
required property string plainText
/**
* @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
/**
* @brief The matrix ID of the reply event.
*/
required property var replyId
/**
* @brief The reply author.
*
* This should consist of the following:
* - id - The matrix ID of the reply author.
* - isLocalUser - Whether the reply author is the local user.
* - avatarSource - The mxc URL for the reply author's avatar in the current room.
* - avatarMediaId - The media ID of the reply author's avatar.
* - avatarUrl - The mxc URL for the reply author's avatar.
* - displayName - The display name of the reply author.
* - display - The name of the reply author.
* - color - The color for the reply author.
* - object - The Quotient::User object for the reply author.
*
* @sa Quotient::User
*/
required property var replyAuthor
/**
* @brief The delegate type of the message replied to.
*/
required property int replyDelegateType
/**
* @brief The display text of the message replied to.
*/
required property string replyDisplay
/**
* @brief The media info for the reply event.
*
* This could be an image, audio, video or file.
*
* This should consist of the following:
* - source - The mxc URL for the media.
* - mimeType - The MIME type of the media.
* - mimeIcon - The MIME icon name.
* - size - The file size in bytes.
* - duration - The length in seconds of the audio media (audio/video only).
* - width - The width in pixels of the audio media (image/video only).
* - height - The height in pixels of the audio media (image/video only).
* - tempInfo - mediaInfo (with the same properties as this except no tempInfo) for a temporary image while the file downloads (image/video only).
*/
required property var replyMediaInfo
/**
* @brief Whether this message is replying to another.
*/
required property bool isReply
/**
* @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 Progress info when downloading files.
*
* @sa Quotient::FileTransferInfo
*/
required property var progressInfo
/**
* @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
required property NeoChatConnection connection
/**
* @brief Open the context menu for the message.
*/
signal openContextMenu
/**
* @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 component to display the delegate type.
*
* This is used by the inherited delegates to assign a component to visualise
* the message content for that delegate type.
*/
default property alias innerObject : column.children
/**
* @brief Whether the bubble background is enabled.
*/
property bool cardBackground: true
/**
* @brief Whether the delegate should always stretch to the maximum availabel width.
*/
property bool alwaysMaxWidth: false
/**
* @brief Whether local user messages should be aligned right.
*
* TODO: make private
*/
property bool showUserMessageOnRight: Config.showLocalMessagesOnRight && root.author.isLocalUser && !Config.compactLayout && !alwaysMaxWidth
/**
* @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
onIsTemporaryHighlightedChanged: if (isTemporaryHighlighted) temporaryHighlightTimer.start()
Timer {
id: temporaryHighlightTimer
interval: 1500
onTriggered: isTemporaryHighlighted = false
}
readonly property int contentMaxWidth: bubbleSizeHelper.currentWidth
width: parent ? timelineDelegateSizeHelper.currentWidth : 0
spacing: Kirigami.Units.smallSpacing
state: Config.compactLayout || root.alwaysMaxWidth ? "alignLeft" : "alignCenter"
// Align left when in compact mode and center when using bubbles
states: [
State {
name: "alignLeft"
AnchorChanges {
target: root
anchors.horizontalCenter: undefined
anchors.left: parent ? parent.left : undefined
}
},
State {
name: "alignCenter"
AnchorChanges {
target: root
anchors.horizontalCenter: parent ? parent.horizontalCenter : undefined
anchors.left: undefined
}
}
]
transitions: [
Transition {
AnchorAnimation{duration: Kirigami.Units.longDuration; easing.type: Easing.OutCubic}
}
]
SectionDelegate {
id: sectionDelegate
Layout.fillWidth: true
visible: root.showSection
labelText: root.section
colorSet: Config.compactLayout || root.alwaysMaxWidth ? Kirigami.Theme.View : Kirigami.Theme.Window
}
QQC2.ItemDelegate {
id: mainContainer
Layout.fillWidth: true
Layout.topMargin: root.showAuthor || root.alwaysShowAuthor ? Kirigami.Units.largeSpacing : (Config.compactLayout ? 1 : Kirigami.Units.smallSpacing)
Layout.leftMargin: Kirigami.Units.smallSpacing
Layout.rightMargin: Kirigami.Units.smallSpacing
implicitHeight: Math.max(root.showAuthor || root.alwaysShowAuthor ? avatar.implicitHeight : 0, bubble.height)
Component.onCompleted: {
if (root.isReply && root.replyDelegateType === DelegateType.Other) {
currentRoom.loadReply(root.eventId, root.replyId)
}
}
// show hover actions
onHoveredChanged: {
if (hovered && !Kirigami.Settings.isMobile) {
root.setHoverActionsToDelegate()
}
}
KirigamiComponents.Avatar {
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.showAuthor || root.alwaysShowAuthor) &&
Config.showAvatarInTimeline &&
(Config.compactLayout || !showUserMessageOnRight)
name: root.author.displayName
source: root.author.avatarSource
color: root.author.color
MouseArea {
anchors.fill: parent
onClicked: {
RoomManager.visitUser(root.author.object, "mention")
}
cursorShape: Qt.PointingHandCursor
}
}
QQC2.Control {
id: bubble
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
hoverEnabled: true
anchors {
left: avatar.right
leftMargin: Kirigami.Units.largeSpacing
rightMargin: Kirigami.Units.largeSpacing
}
// HACK: anchoring didn't reset anchors.right when switching from parent.right to undefined reliably
width: Config.compactLayout || root.alwaysMaxWidth ? mainContainer.width - (Config.showAvatarInTimeline ? Kirigami.Units.gridUnit * 2 : 0) + Kirigami.Units.largeSpacing * 2 : implicitWidth
state: 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
}
}
]
transitions: [
Transition {
AnchorAnimation{duration: Kirigami.Units.longDuration; easing.type: Easing.OutCubic}
}
]
contentItem: RowLayout {
Kirigami.Icon {
source: "content-loading-symbolic"
width: height
Layout.preferredWidth: Kirigami.Units.iconSizes.small
Layout.preferredHeight: Kirigami.Units.iconSizes.small
visible: root.isPending && Config.showLocalMessagesOnRight
}
ColumnLayout {
id: column
spacing: Kirigami.Units.smallSpacing
RowLayout {
id: rowLayout
spacing: Kirigami.Units.smallSpacing
visible: root.showAuthor || root.alwaysShowAuthor
QQC2.Label {
id: nameLabel
Layout.maximumWidth: contentMaxWidth - timeLabel.implicitWidth - rowLayout.spacing
text: visible ? root.author.displayName : ""
textFormat: Text.PlainText
font.weight: Font.Bold
color: root.author.color
elide: Text.ElideRight
MouseArea {
anchors.fill: parent
cursorShape: Qt.PointingHandCursor
onClicked: {
RoomManager.visitUser(root.author.object, "mention")
}
}
}
QQC2.Label {
id: timeLabel
text: root.timeString
color: Kirigami.Theme.disabledTextColor
QQC2.ToolTip.visible: hoverHandler.hovered
QQC2.ToolTip.text: root.time.toLocaleString(Qt.locale(), Locale.LongFormat)
QQC2.ToolTip.delay: Kirigami.Units.toolTipDelay
HoverHandler {
id: hoverHandler
}
}
}
Loader {
id: replyLoader
Layout.maximumWidth: contentMaxWidth
active: root.isReply && root.replyDelegateType !== DelegateType.Other
visible: active
sourceComponent: ReplyComponent {
author: root.replyAuthor
type: root.replyDelegateType
display: root.replyDisplay
mediaInfo: root.replyMediaInfo
contentMaxWidth: bubbleSizeHelper.currentWidth
}
Connections {
target: replyLoader.item
function onReplyClicked() {
replyClicked(root.replyId)
}
}
}
}
Kirigami.Icon {
source: "content-loading-symbolic"
width: height
Layout.preferredWidth: Kirigami.Units.iconSizes.small
Layout.preferredHeight: Kirigami.Units.iconSizes.small
visible: root.isPending && !Config.showLocalMessagesOnRight
}
}
background: Item {
Kirigami.ShadowedRectangle {
id: bubbleBackground
visible: cardBackground && !Config.compactLayout
anchors.fill: parent
Kirigami.Theme.colorSet: Kirigami.Theme.View
Kirigami.Theme.inherit: false
color: if (root.author.isLocalUser) {
return Kirigami.ColorUtils.tintWithAlpha(Kirigami.Theme.backgroundColor, Kirigami.Theme.highlightColor, 0.15)
} else if (root.showHighlight) {
return Kirigami.Theme.positiveBackgroundColor
} else {
return Kirigami.Theme.backgroundColor
}
radius: Kirigami.Units.smallSpacing
shadow {
size: Kirigami.Units.smallSpacing
color: root.isHighlighted ? Qt.rgba(0.0, 0.0, 0.0, 0.10) : Qt.rgba(Kirigami.Theme.textColor.r, Kirigami.Theme.textColor.g, Kirigami.Theme.textColor.b, 0.10)
}
Behavior on color {
ColorAnimation { duration: Kirigami.Units.shortDuration }
}
}
}
}
background: Rectangle {
visible: mainContainer.hovered && (Config.compactLayout || root.alwaysMaxWidth)
color: Kirigami.ColorUtils.tintWithAlpha(Kirigami.Theme.backgroundColor, Kirigami.Theme.highlightColor, 0.15)
radius: Kirigami.Units.smallSpacing
}
TapHandler {
acceptedButtons: Qt.RightButton
onTapped: root.openContextMenu()
}
TapHandler {
acceptedButtons: Qt.LeftButton
onLongPressed: root.openContextMenu()
}
}
ReactionDelegate {
Layout.maximumWidth: root.width - Kirigami.Units.largeSpacing * 2
Layout.alignment: showUserMessageOnRight ? Qt.AlignRight : Qt.AlignLeft
Layout.leftMargin: showUserMessageOnRight ? 0 : bubble.x + bubble.anchors.leftMargin
Layout.rightMargin: showUserMessageOnRight ? Kirigami.Units.largeSpacing : 0
visible: root.showReactions
model: root.reaction
onReactionClicked: (reaction) => currentRoom.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
}
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)
}
}
DelegateSizeHelper {
id: timelineDelegateSizeHelper
startBreakpoint: Kirigami.Units.gridUnit * 46
endBreakpoint: Kirigami.Units.gridUnit * 66
startPercentWidth: 100
endPercentWidth: Config.compactLayout || root.alwaysMaxWidth ? 100 : 85
maxWidth: Config.compactLayout || root.alwaysMaxWidth ? -1 : Kirigami.Units.gridUnit * 60
parentWidth: root.parent ? root.parent.width - (Config.compactLayout && root.ListView.view.width >= Kirigami.Units.gridUnit * 20 ? Kirigami.Units.gridUnit * 2 + Kirigami.Units.largeSpacing : 0) : 0
}
DelegateSizeHelper {
id: bubbleSizeHelper
startBreakpoint: Kirigami.Units.gridUnit * 25
endBreakpoint: Kirigami.Units.gridUnit * 40
startPercentWidth: Config.compactLayout || root.alwaysMaxWidth ? 100 : 90
endPercentWidth: Config.compactLayout || root.alwaysMaxWidth ? 100 : 60
parentWidth: mainContainer.availableWidth - (Config.showAvatarInTimeline ? avatar.width + bubble.anchors.leftMargin : 0)
}
}

View File

@@ -0,0 +1,97 @@
// SPDX-FileCopyrightText: 2022 James Graham <james.h.graham@protonmail.com>
// SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
import QtQuick 2.15
import QtQuick.Controls 2.15 as QQC2
import org.kde.kirigami 2.15 as Kirigami
import org.kde.neochat 1.0
/**
* @brief The base Item for all delegates in the timeline.
*
* This component handles the placing of the main content for a delegate in the
* timeline. The component is designed for all delegates, positioning them in the
* timeline with variable padding depending on the window width.
*
* This component also supports always setting the delegate to fill the available
* width in the timeline, e.g. in compact mode.
*/
Item {
id: root
/**
* @brief The Item representing the delegate's main content.
*/
property Item contentItem
/**
* @brief Whether the delegate should always stretch to the maximum available width.
*/
property bool alwaysMaxWidth: false
/**
* @brief The padding to the left of the content.
*/
property real leftPadding: Kirigami.Units.largeSpacing
/**
* @brief The padding to the right of the content.
*/
property real rightPadding: Config.compactLayout && root.ListView.view.width >= Kirigami.Units.gridUnit * 20 ? Kirigami.Units.gridUnit * 2 + Kirigami.Units.largeSpacing : Kirigami.Units.largeSpacing
width: parent?.width
implicitHeight: contentItemParent.implicitHeight
Item {
id: contentItemParent
anchors.top: parent.top
anchors.bottom: parent.bottom
anchors.leftMargin: state === "alignLeft" ? Kirigami.Units.largeSpacing : 0
state: Config.compactLayout || root.alwaysMaxWidth ? "alignLeft" : "alignCenter"
// Align left when in compact mode and center when using bubbles
states: [
State {
name: "alignLeft"
AnchorChanges {
target: contentItemParent
anchors.horizontalCenter: undefined
anchors.left: parent ? parent.left : undefined
}
},
State {
name: "alignCenter"
AnchorChanges {
target: contentItemParent
anchors.horizontalCenter: parent ? parent.horizontalCenter : undefined
anchors.left: undefined
}
}
]
width: (Config.compactLayout || root.alwaysMaxWidth ? root.width : delegateSizeHelper.currentWidth) - root.leftPadding - root.rightPadding
implicitHeight: root.contentItem?.implicitHeight ?? 0
}
DelegateSizeHelper {
id: delegateSizeHelper
startBreakpoint: Kirigami.Units.gridUnit * 46
endBreakpoint: Kirigami.Units.gridUnit * 66
startPercentWidth: 100
endPercentWidth: 85
maxWidth: Kirigami.Units.gridUnit * 60
parentWidth: root.width
}
onContentItemChanged: {
if (!contentItem) {
return;
}
contentItem.parent = contentItemParent;
contentItem.anchors.fill = contentItem.parent;
}
}

View File

@@ -14,9 +14,9 @@ import org.kde.neochat 1.0
/**
* @brief A timeline delegate for a video message.
*
* @inherit TimelineContainer
* @inherit MessageDelegate
*/
TimelineContainer {
MessageDelegate {
id: root
/**

View File

@@ -88,7 +88,7 @@ QQC2.ScrollView {
}
}
// Not rendered because the sections are part of the TimelineContainer.qml, this is only so that items have the section property available for use by sectionBanner.
// Not rendered because the sections are part of the MessageDelegate.qml, this is only so that items have the section property available for use by sectionBanner.
// This is due to the fact that the ListView verticalLayout is BottomToTop.
// This also flips the sections which would appear at the bottom but for a timeline they still need to be at the top (bottom from the qml perspective).
// There is currently no option to put section headings at the bottom in qml.
@@ -110,10 +110,10 @@ QQC2.ScrollView {
id: sectionBanner
anchors.left: parent.left
anchors.leftMargin: messageListView.sectionBannerItem ? messageListView.sectionBannerItem.x : 0
anchors.leftMargin: messageListView.sectionBannerItem ? messageListView.sectionBannerItem.contentItem.parent.x : 0
anchors.right: parent.right
maxWidth: Config.compactLayout ? messageListView.width : (messageListView.sectionBannerItem ? messageListView.sectionBannerItem.width - Kirigami.Units.largeSpacing * 2 : 0)
maxWidth: messageListView.sectionBannerItem ? messageListView.sectionBannerItem.contentItem.width : 0
z: 3
visible: !!messageListView.sectionBannerItem && messageListView.sectionBannerItem.ListView.section !== "" && !Config.blur
labelText: messageListView.sectionBannerItem ? messageListView.sectionBannerItem.ListView.section : ""

View File

@@ -44,10 +44,11 @@
<file alias="RoomData.qml">qml/Component/Devtools/RoomData.qml</file>
<file alias="ServerData.qml">qml/Component/Devtools/ServerData.qml</file>
<file alias="EmojiPicker.qml">qml/Component/Emoji/EmojiPicker.qml</file>
<file alias="TimelineDelegate.qml">qml/Component/Timeline/TimelineDelegate.qml</file>
<file alias="ReplyComponent.qml">qml/Component/Timeline/ReplyComponent.qml</file>
<file alias="StateDelegate.qml">qml/Component/Timeline/StateDelegate.qml</file>
<file alias="RichLabel.qml">qml/Component/Timeline/RichLabel.qml</file>
<file alias="TimelineContainer.qml">qml/Component/Timeline/TimelineContainer.qml</file>
<file alias="MessageDelegate.qml">qml/Component/Timeline/MessageDelegate.qml</file>
<file alias="SectionDelegate.qml">qml/Component/Timeline/SectionDelegate.qml</file>
<file alias="VideoDelegate.qml">qml/Component/Timeline/VideoDelegate.qml</file>
<file alias="ReactionDelegate.qml">qml/Component/Timeline/ReactionDelegate.qml</file>
@@ -57,7 +58,7 @@
<file alias="ImageDelegate.qml">qml/Component/Timeline/ImageDelegate.qml</file>
<file alias="EncryptedDelegate.qml">qml/Component/Timeline/EncryptedDelegate.qml</file>
<file alias="EventDelegate.qml">qml/Component/Timeline/EventDelegate.qml</file>
<file alias="MessageDelegate.qml">qml/Component/Timeline/MessageDelegate.qml</file>
<file alias="TextDelegate.qml">qml/Component/Timeline/TextDelegate.qml</file>
<file alias="ReadMarkerDelegate.qml">qml/Component/Timeline/ReadMarkerDelegate.qml</file>
<file alias="PollDelegate.qml">qml/Component/Timeline/PollDelegate.qml</file>
<file alias="MimeComponent.qml">qml/Component/Timeline/MimeComponent.qml</file>