Refactor Timeline Container

Update the base item in the timeline container to be a column layout. This means that all the items can be laid out automatically without the need to set lots of manual settings and anchoring. 

The overall height calculation for the delegate is vastly simplified (in fact it is removed) which deals with the fact that there were still instances where the manual calculation didn't work e.g. a delegate with a reaction followed by another message from the same user didn't give the correct bottom margin (see below)

before:
![timelineContainer_height_bug](/uploads/5b14568294698198dee8412f6cd19be0/timelineContainer_height_bug.png)

after:
![timelineContainer_height_bug_fix](/uploads/c5828f1b793817fd0ed523c3580a2ecc/timelineContainer_height_bug_fix.png)

This also improves upon the recently changed hover highlight behaviour. The previous patched moved it to cover the avatar as well as the bubble however it also covered the section and reaction when present which didn't look good. The highlight now only covers the avatar and bubble

before:
![highlight_bug](/uploads/0d08dc769ff737e0fb4981243d02d5f3/highlight_bug.png)

after:
![highlight_bug_fixed](/uploads/536ed672d0f1bb6cbe6c45777fed4b53/highlight_bug_fixed.png)

This also cleans up some of the margins in both bubble and compact to ensure consistency.
This commit is contained in:
James Graham
2022-11-11 17:05:14 +00:00
parent 8addf0f078
commit 460997bca3
8 changed files with 221 additions and 246 deletions

View File

@@ -13,15 +13,13 @@ import org.kde.neochat 1.0
TimelineContainer { TimelineContainer {
id: audioDelegate id: audioDelegate
onReplyClicked: ListView.view.goToEvent(eventID)
onOpenContextMenu: openFileContext(model, audioDelegate) onOpenContextMenu: openFileContext(model, audioDelegate)
readonly property bool downloaded: model.progressInfo && model.progressInfo.completed readonly property bool downloaded: model.progressInfo && model.progressInfo.completed
onDownloadedChanged: audio.play() onDownloadedChanged: audio.play()
hoverComponent: hoverActions
innerObject: ColumnLayout { innerObject: ColumnLayout {
Layout.fillWidth: true
Layout.maximumWidth: audioDelegate.contentMaxWidth Layout.maximumWidth: audioDelegate.contentMaxWidth
Audio { Audio {

View File

@@ -13,9 +13,6 @@ import org.kde.neochat 1.0
TimelineContainer { TimelineContainer {
id: fileDelegate id: fileDelegate
onReplyClicked: ListView.view.goToEvent(eventID)
hoverComponent: hoverActions
onOpenContextMenu: openFileContext(model, fileDelegate) onOpenContextMenu: openFileContext(model, fileDelegate)
readonly property bool downloaded: progressInfo && progressInfo.completed readonly property bool downloaded: progressInfo && progressInfo.completed

View File

@@ -13,9 +13,6 @@ import org.kde.neochat 1.0
TimelineContainer { TimelineContainer {
id: imageDelegate id: imageDelegate
onReplyClicked: ListView.view.goToEvent(eventID)
hoverComponent: hoverActions
onOpenContextMenu: openFileContext(model, imageDelegate) onOpenContextMenu: openFileContext(model, imageDelegate)
property var content: model.content property var content: model.content

View File

@@ -16,9 +16,6 @@ TimelineContainer {
property bool isEmote: false property bool isEmote: false
onOpenContextMenu: openMessageContext(model, label.selectedText, Controller.plainText(label.textDocument)) onOpenContextMenu: openMessageContext(model, label.selectedText, Controller.plainText(label.textDocument))
onReplyClicked: ListView.view.goToEvent(eventID)
hoverComponent: hoverActions
innerObject: ColumnLayout { innerObject: ColumnLayout {
Layout.maximumWidth: messageDelegate.contentMaxWidth Layout.maximumWidth: messageDelegate.contentMaxWidth
RichLabel { RichLabel {
@@ -29,7 +26,6 @@ TimelineContainer {
Loader { Loader {
id: linkPreviewLoader id: linkPreviewLoader
Layout.fillWidth: true Layout.fillWidth: true
height: active ? item.implicitHeight : 0
active: !currentRoom.usesEncryption && model.display && model.display.includes("http") active: !currentRoom.usesEncryption && model.display && model.display.includes("http")
visible: Config.showLinkPreview && active visible: Config.showLinkPreview && active
sourceComponent: LinkPreviewDelegate { sourceComponent: LinkPreviewDelegate {

View File

@@ -9,7 +9,8 @@ import QtQuick.Layouts 1.15
import org.kde.kirigami 2.15 as Kirigami import org.kde.kirigami 2.15 as Kirigami
Flow { Flow {
spacing: Kirigami.Units.largeSpacing spacing: Kirigami.Units.smallSpacing
Repeater { Repeater {
model: reaction ?? null model: reaction ?? null
@@ -32,7 +33,6 @@ Flow {
border.width: 1 border.width: 1
} }
checkable: true checkable: true
checked: modelData.hasLocalUser checked: modelData.hasLocalUser

View File

@@ -9,15 +9,21 @@ import org.kde.kirigami 2.15 as Kirigami
import org.kde.neochat 1.0 import org.kde.neochat 1.0
QQC2.ItemDelegate { ColumnLayout {
id: timelineContainer id: root
signal openContextMenu
signal openExternally()
signal replyClicked(string eventID)
onReplyClicked: ListView.view.goToEvent(eventID)
default property alias innerObject : column.children default property alias innerObject : column.children
// readonly property bool failed: marks == EventStatus.SendingFailed
readonly property bool sectionVisible: model.showSection
property Item hoverComponent: hoverActions
property bool isEmote: false property bool isEmote: false
property bool cardBackground: true property bool cardBackground: true
property bool showUserMessageOnRight: Config.showLocalMessagesOnRight && model.author.isLocalUser && !Config.compactLayout
property bool isHighlighted: model.isHighlighted || isTemporaryHighlighted property bool isHighlighted: model.isHighlighted || isTemporaryHighlighted
property bool isTemporaryHighlighted: false property bool isTemporaryHighlighted: false
@@ -30,58 +36,15 @@ QQC2.ItemDelegate {
onTriggered: isTemporaryHighlighted = false onTriggered: isTemporaryHighlighted = false
} }
signal openContextMenu
// The bubble and delegate widths are allowed to grow once the ListView gets beyond a certain size // The bubble and delegate widths are allowed to grow once the ListView gets beyond a certain size
// extraWidth defines this as the excess after a certain ListView width, capped to a max value // extraWidth defines this as the excess after a certain ListView width, capped to a max value
readonly property int extraWidth: messageListView.width >= Kirigami.Units.gridUnit * 46 ? Math.min((messageListView.width - Kirigami.Units.gridUnit * 46), Kirigami.Units.gridUnit * 20) : 0 readonly property int extraWidth: messageListView.width >= Kirigami.Units.gridUnit * 46 ? Math.min((messageListView.width - Kirigami.Units.gridUnit * 46), Kirigami.Units.gridUnit * 20) : 0
readonly property int bubbleMaxWidth: Kirigami.Units.gridUnit * 20 + extraWidth * 0.5 readonly property int bubbleMaxWidth: Kirigami.Units.gridUnit * 20 + extraWidth * 0.5
readonly property int delegateMaxWidth: Config.compactLayout ? messageListView.width : Math.min(messageListView.width, Kirigami.Units.gridUnit * 40 + extraWidth) readonly property int delegateWidth: Config.compactLayout ? messageListView.width : Math.min(messageListView.width, Kirigami.Units.gridUnit * 40 + extraWidth)
readonly property int contentMaxWidth: Config.compactLayout ? width - (Config.showAvatarInTimeline ? Kirigami.Units.gridUnit * 2 : 0) - Kirigami.Units.largeSpacing * 4 : Math.min(width - Kirigami.Units.gridUnit * 2 - Kirigami.Units.largeSpacing * 6, bubbleMaxWidth) readonly property int contentMaxWidth: Config.compactLayout ? width - (Config.showAvatarInTimeline ? Kirigami.Units.gridUnit * 2 : 0) - Kirigami.Units.largeSpacing * 4 : Math.min(width - Kirigami.Units.gridUnit * 2 - Kirigami.Units.largeSpacing * 6, bubbleMaxWidth)
property bool showUserMessageOnRight: Config.showLocalMessagesOnRight && width: delegateWidth
model.author.isLocalUser && !Config.compactLayout spacing: Kirigami.Units.smallSpacing
signal openExternally()
signal replyClicked(string eventID)
Component.onCompleted: {
if (model.isReply && model.reply === undefined) {
messageEventModel.loadReply(sortedMessageEventModel.mapToSource(sortedMessageEventModel.index(model.index, 0)))
}
}
topPadding: 0
bottomPadding: 0
topInset: showAuthor ? Kirigami.Units.largeSpacing : (Config.compactLayout ? 1 : Kirigami.Units.smallSpacing)
leftInset: Kirigami.Units.smallSpacing
rightInset: Kirigami.Units.smallSpacing
width: delegateMaxWidth
height: sectionDelegate.height + Math.max(model.showAuthor ? avatar.height : 0, bubble.implicitHeight) + loader.height + loader.anchors.topMargin + avatar.anchors.topMargin
background: Rectangle {
visible: timelineContainer.hovered
color: Kirigami.ColorUtils.tintWithAlpha(Kirigami.Theme.backgroundColor, Kirigami.Theme.highlightColor, 0.15)
radius: Kirigami.Units.smallSpacing
}
property Item hoverComponent
// show hover actions
onHoveredChanged: {
if (hovered && !Kirigami.Settings.isMobile) {
updateHoverComponent();
}
}
// updates the global hover component to point to this delegate, and update its position
function updateHoverComponent() {
if (hoverComponent) {
hoverComponent.delegate = timelineContainer
hoverComponent.bubble = bubble
hoverComponent.updateFunction = updateHoverComponent;
hoverComponent.event = model
}
}
state: Config.compactLayout ? "alignLeft" : "alignCenter" state: Config.compactLayout ? "alignLeft" : "alignCenter"
// Align left when in compact mode and center when using bubbles // Align left when in compact mode and center when using bubbles
@@ -89,7 +52,7 @@ QQC2.ItemDelegate {
State { State {
name: "alignLeft" name: "alignLeft"
AnchorChanges { AnchorChanges {
target: timelineContainer target: root
anchors.horizontalCenter: undefined anchors.horizontalCenter: undefined
anchors.left: parent ? parent.left : undefined anchors.left: parent ? parent.left : undefined
} }
@@ -97,7 +60,7 @@ QQC2.ItemDelegate {
State { State {
name: "alignCenter" name: "alignCenter"
AnchorChanges { AnchorChanges {
target: timelineContainer target: root
anchors.horizontalCenter: parent ? parent.horizontalCenter : undefined anchors.horizontalCenter: parent ? parent.horizontalCenter : undefined
anchors.left: undefined anchors.left: undefined
} }
@@ -112,13 +75,45 @@ QQC2.ItemDelegate {
SectionDelegate { SectionDelegate {
id: sectionDelegate id: sectionDelegate
anchors.left: timelineContainer.left
anchors.right: timelineContainer.right Layout.fillWidth: true
visible: sectionVisible visible: model.showSection
height: visible ? implicitHeight : 0
labelText: model.showSection ? section : "" labelText: model.showSection ? section : ""
} }
QQC2.ItemDelegate {
id: mainContainer
Layout.fillWidth: true
Layout.topMargin: showAuthor ? Kirigami.Units.largeSpacing : (Config.compactLayout ? 1 : Kirigami.Units.smallSpacing)
Layout.leftMargin: Kirigami.Units.smallSpacing
implicitHeight: Math.max(model.showAuthor ? avatar.implicitHeight : 0, bubble.height)
Component.onCompleted: {
if (model.isReply && model.reply === undefined) {
messageEventModel.loadReply(sortedMessageEventModel.mapToSource(sortedMessageEventModel.index(model.index, 0)))
}
}
// show hover actions
onHoveredChanged: {
if (hovered && !Kirigami.Settings.isMobile) {
updateHoverComponent();
}
}
// Show hover actions by updating the global hover component to this delegate
function updateHoverComponent() {
if (hovered && !Kirigami.Settings.isMobile) {
hoverComponent.delegate = root
hoverComponent.bubble = bubble
hoverComponent.event = model
hoverComponent.updateFunction = updateHoverComponent;
}
}
Kirigami.Avatar { Kirigami.Avatar {
id: avatar id: avatar
width: visible || Config.showAvatarInTimeline ? Kirigami.Units.gridUnit * 2 + Kirigami.Units.smallSpacing * 2 : 0 width: visible || Config.showAvatarInTimeline ? Kirigami.Units.gridUnit * 2 + Kirigami.Units.smallSpacing * 2 : 0
@@ -128,11 +123,7 @@ QQC2.ItemDelegate {
bottomInset: Kirigami.Units.smallSpacing bottomInset: Kirigami.Units.smallSpacing
leftInset: Kirigami.Units.smallSpacing leftInset: Kirigami.Units.smallSpacing
rightInset: Kirigami.Units.smallSpacing rightInset: Kirigami.Units.smallSpacing
sourceSize.width: width
sourceSize.height: width
anchors { anchors {
top: sectionDelegate.bottom
topMargin: model.showAuthor ? Kirigami.Units.largeSpacing : (Config.compactLayout ? 1 : Kirigami.Units.smallSpacing)
left: parent.left left: parent.left
leftMargin: Kirigami.Units.smallSpacing leftMargin: Kirigami.Units.smallSpacing
} }
@@ -140,7 +131,7 @@ QQC2.ItemDelegate {
visible: model.showAuthor && visible: model.showAuthor &&
Config.showAvatarInTimeline && Config.showAvatarInTimeline &&
(Config.compactLayout || !showUserMessageOnRight) (Config.compactLayout || !showUserMessageOnRight)
name: model.displayNameForInitials name: model.author.name ?? model.author.displayName
source: visible && model.author.avatarMediaId ? ("image://mxc/" + model.author.avatarMediaId) : "" source: visible && model.author.avatarMediaId ? ("image://mxc/" + model.author.avatarMediaId) : ""
color: model.author.color color: model.author.color
@@ -163,17 +154,16 @@ QQC2.ItemDelegate {
id: bubble id: bubble
topPadding: Config.compactLayout ? Kirigami.Units.smallSpacing / 2 : Kirigami.Units.largeSpacing topPadding: Config.compactLayout ? Kirigami.Units.smallSpacing / 2 : Kirigami.Units.largeSpacing
bottomPadding: Config.compactLayout ? Kirigami.Units.mediumSpacing / 2 : Kirigami.Units.largeSpacing bottomPadding: Config.compactLayout ? Kirigami.Units.mediumSpacing / 2 : Kirigami.Units.largeSpacing
leftPadding: Kirigami.Units.largeSpacing + Kirigami.Units.smallSpacing leftPadding: Config.compactLayout ? 0 : Kirigami.Units.largeSpacing + Kirigami.Units.smallSpacing
rightPadding: Kirigami.Units.largeSpacing + Kirigami.Units.smallSpacing rightPadding: Kirigami.Units.largeSpacing + Kirigami.Units.smallSpacing
hoverEnabled: true hoverEnabled: true
anchors { anchors {
top: avatar.top
leftMargin: Kirigami.Units.smallSpacing leftMargin: Kirigami.Units.smallSpacing
rightMargin: showUserMessageOnRight ? Kirigami.Units.smallSpacing : Kirigami.Units.largeSpacing rightMargin: Kirigami.Units.largeSpacing
} }
// HACK: anchoring didn't reset anchors.right when switching from parent.right to undefined reliably // HACK: anchoring didn't reset anchors.right when switching from parent.right to undefined reliably
width: Config.compactLayout ? timelineContainer.width - (Config.showAvatarInTimeline ? Kirigami.Units.gridUnit * 2 : 0) + Kirigami.Units.largeSpacing * 2 : implicitWidth width: Config.compactLayout ? mainContainer.width - (Config.showAvatarInTimeline ? Kirigami.Units.gridUnit * 2 : 0) + Kirigami.Units.largeSpacing * 2 : implicitWidth
state: showUserMessageOnRight ? "userMessageOnRight" : "userMessageOnLeft" state: showUserMessageOnRight ? "userMessageOnRight" : "userMessageOnLeft"
// states for anchor animations on window resize // states for anchor animations on window resize
@@ -281,7 +271,7 @@ QQC2.ItemDelegate {
color: { color: {
if (model.author.isLocalUser) { if (model.author.isLocalUser) {
return Kirigami.ColorUtils.tintWithAlpha(Kirigami.Theme.backgroundColor, Kirigami.Theme.highlightColor, 0.15) return Kirigami.ColorUtils.tintWithAlpha(Kirigami.Theme.backgroundColor, Kirigami.Theme.highlightColor, 0.15)
} else if (timelineContainer.isHighlighted) { } else if (root.isHighlighted) {
return Kirigami.Theme.positiveBackgroundColor return Kirigami.Theme.positiveBackgroundColor
} else { } else {
return Kirigami.Theme.backgroundColor return Kirigami.Theme.backgroundColor
@@ -289,7 +279,7 @@ QQC2.ItemDelegate {
} }
radius: Kirigami.Units.smallSpacing radius: Kirigami.Units.smallSpacing
shadow.size: Kirigami.Units.smallSpacing shadow.size: Kirigami.Units.smallSpacing
shadow.color: timelineContainer.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) shadow.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)
border.color: Kirigami.ColorUtils.tintWithAlpha(color, Kirigami.Theme.textColor, 0.15) border.color: Kirigami.ColorUtils.tintWithAlpha(color, Kirigami.Theme.textColor, 0.15)
border.width: 1 border.width: 1
@@ -300,28 +290,29 @@ QQC2.ItemDelegate {
} }
} }
Loader { background: Rectangle {
id: loader visible: mainContainer.hovered
anchors { color: Kirigami.ColorUtils.tintWithAlpha(Kirigami.Theme.backgroundColor, Kirigami.Theme.highlightColor, 0.15)
left: bubble.left radius: Kirigami.Units.smallSpacing
right: parent.right
top: bubble.bottom
topMargin: active ? Kirigami.Units.smallSpacing : 0
}
height: active ? item.implicitHeight : 0
active: eventType !== MessageEventModel.State && eventType !== MessageEventModel.Notice && reaction != undefined && reaction.length > 0
visible: active
sourceComponent: ReactionDelegate { }
} }
TapHandler { TapHandler {
acceptedButtons: Qt.RightButton acceptedButtons: Qt.RightButton
acceptedDevices: PointerDevice.Mouse onTapped: root.openContextMenu()
onTapped: timelineContainer.openContextMenu()
} }
TapHandler { TapHandler {
acceptedButtons: Qt.LeftButton acceptedButtons: Qt.LeftButton
onLongPressed: timelineContainer.openContextMenu() onLongPressed: root.openContextMenu()
}
}
ReactionDelegate {
Layout.maximumWidth: delegateWidth - 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: eventType !== MessageEventModel.State && eventType !== MessageEventModel.Notice && reaction != undefined && reaction.length > 0
} }
} }

View File

@@ -14,9 +14,6 @@ import org.kde.neochat 1.0
TimelineContainer { TimelineContainer {
id: videoDelegate id: videoDelegate
onReplyClicked: ListView.view.goToEvent(eventID)
hoverComponent: hoverActions
property bool playOnFinished: false property bool playOnFinished: false
readonly property bool downloaded: progressInfo && progressInfo.completed readonly property bool downloaded: progressInfo && progressInfo.completed

View File

@@ -463,6 +463,7 @@ Kirigami.ScrollablePage {
property var bubble: null property var bubble: null
property var hovered: bubble && bubble.hovered property var hovered: bubble && bubble.hovered
property var visibleDelayed: (hovered || hoverHandler.hovered) && !Kirigami.Settings.isMobile property var visibleDelayed: (hovered || hoverHandler.hovered) && !Kirigami.Settings.isMobile
property var updateFunction
onVisibleDelayedChanged: if (visibleDelayed) { onVisibleDelayedChanged: if (visibleDelayed) {
visible = true; visible = true;
} else { } else {
@@ -482,8 +483,6 @@ Kirigami.ScrollablePage {
visible: false visible: false
property var updateFunction
property alias childWidth: hoverActionsRow.width property alias childWidth: hoverActionsRow.width
property alias childHeight: hoverActionsRow.height property alias childHeight: hoverActionsRow.height