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 {
id: audioDelegate
onReplyClicked: ListView.view.goToEvent(eventID)
onOpenContextMenu: openFileContext(model, audioDelegate)
readonly property bool downloaded: model.progressInfo && model.progressInfo.completed
onDownloadedChanged: audio.play()
hoverComponent: hoverActions
innerObject: ColumnLayout {
Layout.fillWidth: true
Layout.maximumWidth: audioDelegate.contentMaxWidth
Audio {

View File

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

View File

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

View File

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

View File

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

View File

@@ -9,15 +9,21 @@ import org.kde.kirigami 2.15 as Kirigami
import org.kde.neochat 1.0
QQC2.ItemDelegate {
id: timelineContainer
ColumnLayout {
id: root
signal openContextMenu
signal openExternally()
signal replyClicked(string eventID)
onReplyClicked: ListView.view.goToEvent(eventID)
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 cardBackground: true
property bool showUserMessageOnRight: Config.showLocalMessagesOnRight && model.author.isLocalUser && !Config.compactLayout
property bool isHighlighted: model.isHighlighted || isTemporaryHighlighted
property bool isTemporaryHighlighted: false
@@ -30,58 +36,15 @@ QQC2.ItemDelegate {
onTriggered: isTemporaryHighlighted = false
}
signal openContextMenu
// 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
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 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)
property bool showUserMessageOnRight: Config.showLocalMessagesOnRight &&
model.author.isLocalUser && !Config.compactLayout
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
}
}
width: delegateWidth
spacing: Kirigami.Units.smallSpacing
state: Config.compactLayout ? "alignLeft" : "alignCenter"
// Align left when in compact mode and center when using bubbles
@@ -89,7 +52,7 @@ QQC2.ItemDelegate {
State {
name: "alignLeft"
AnchorChanges {
target: timelineContainer
target: root
anchors.horizontalCenter: undefined
anchors.left: parent ? parent.left : undefined
}
@@ -97,7 +60,7 @@ QQC2.ItemDelegate {
State {
name: "alignCenter"
AnchorChanges {
target: timelineContainer
target: root
anchors.horizontalCenter: parent ? parent.horizontalCenter : undefined
anchors.left: undefined
}
@@ -112,216 +75,244 @@ QQC2.ItemDelegate {
SectionDelegate {
id: sectionDelegate
anchors.left: timelineContainer.left
anchors.right: timelineContainer.right
visible: sectionVisible
height: visible ? implicitHeight : 0
Layout.fillWidth: true
visible: model.showSection
labelText: model.showSection ? section : ""
}
Kirigami.Avatar {
id: avatar
width: visible || Config.showAvatarInTimeline ? Kirigami.Units.gridUnit * 2 + Kirigami.Units.smallSpacing * 2 : 0
height: width
padding: Kirigami.Units.smallSpacing
topInset: Kirigami.Units.smallSpacing
bottomInset: Kirigami.Units.smallSpacing
leftInset: Kirigami.Units.smallSpacing
rightInset: Kirigami.Units.smallSpacing
sourceSize.width: width
sourceSize.height: width
anchors {
top: sectionDelegate.bottom
topMargin: model.showAuthor ? Kirigami.Units.largeSpacing : (Config.compactLayout ? 1 : Kirigami.Units.smallSpacing)
left: parent.left
leftMargin: Kirigami.Units.smallSpacing
}
QQC2.ItemDelegate {
id: mainContainer
visible: model.showAuthor &&
Config.showAvatarInTimeline &&
(Config.compactLayout || !showUserMessageOnRight)
name: model.displayNameForInitials
source: visible && model.author.avatarMediaId ? ("image://mxc/" + model.author.avatarMediaId) : ""
color: model.author.color
Layout.fillWidth: true
Layout.topMargin: showAuthor ? Kirigami.Units.largeSpacing : (Config.compactLayout ? 1 : Kirigami.Units.smallSpacing)
Layout.leftMargin: Kirigami.Units.smallSpacing
MouseArea {
anchors.fill: parent
onClicked: {
userDetailDialog.createObject(QQC2.ApplicationWindow.overlay, {
room: currentRoom,
user: author.object,
displayName: author.displayName,
avatarMediaId: author.avatarMediaId,
avatarUrl: author.avatarUrl
}).open();
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)))
}
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: Kirigami.Units.largeSpacing + Kirigami.Units.smallSpacing
rightPadding: Kirigami.Units.largeSpacing + Kirigami.Units.smallSpacing
hoverEnabled: true
anchors {
top: avatar.top
leftMargin: Kirigami.Units.smallSpacing
rightMargin: showUserMessageOnRight ? Kirigami.Units.smallSpacing : Kirigami.Units.largeSpacing
// show hover actions
onHoveredChanged: {
if (hovered && !Kirigami.Settings.isMobile) {
updateHoverComponent();
}
}
// 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
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
// 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 {
id: avatar
width: visible || Config.showAvatarInTimeline ? Kirigami.Units.gridUnit * 2 + Kirigami.Units.smallSpacing * 2 : 0
height: width
padding: Kirigami.Units.smallSpacing
topInset: Kirigami.Units.smallSpacing
bottomInset: Kirigami.Units.smallSpacing
leftInset: Kirigami.Units.smallSpacing
rightInset: Kirigami.Units.smallSpacing
anchors {
left: parent.left
leftMargin: Kirigami.Units.smallSpacing
}
visible: model.showAuthor &&
Config.showAvatarInTimeline &&
(Config.compactLayout || !showUserMessageOnRight)
name: model.author.name ?? model.author.displayName
source: visible && model.author.avatarMediaId ? ("image://mxc/" + model.author.avatarMediaId) : ""
color: model.author.color
MouseArea {
anchors.fill: parent
onClicked: {
userDetailDialog.createObject(QQC2.ApplicationWindow.overlay, {
room: currentRoom,
user: author.object,
displayName: author.displayName,
avatarMediaId: author.avatarMediaId,
avatarUrl: author.avatarUrl
}).open();
}
},
State {
name: "userMessageOnLeft"
AnchorChanges {
target: bubble
anchors.left: avatar.right
anchors.right: undefined
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 {
leftMargin: Kirigami.Units.smallSpacing
rightMargin: Kirigami.Units.largeSpacing
}
// HACK: anchoring didn't reset anchors.right when switching from parent.right to undefined reliably
width: Config.compactLayout ? 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: ColumnLayout {
id: column
spacing: Kirigami.Units.smallSpacing
RowLayout {
id: rowLayout
transitions: [
Transition {
AnchorAnimation{duration: Kirigami.Units.longDuration; easing.type: Easing.OutCubic}
}
]
contentItem: ColumnLayout {
id: column
spacing: Kirigami.Units.smallSpacing
visible: model.showAuthor && !isEmote
RowLayout {
id: rowLayout
QQC2.Label {
id: nameLabel
spacing: Kirigami.Units.smallSpacing
visible: model.showAuthor && !isEmote
Layout.maximumWidth: contentMaxWidth - timeLabel.implicitWidth - rowLayout.spacing
QQC2.Label {
id: nameLabel
text: visible ? author.displayName : ""
textFormat: Text.PlainText
font.weight: Font.Bold
color: author.color
elide: Text.ElideRight
MouseArea {
anchors.fill: parent
cursorShape: Qt.PointingHandCursor
onClicked: {
userDetailDialog.createObject(QQC2.ApplicationWindow.overlay, {
room: currentRoom,
user: author.object,
displayName: author.displayName,
avatarMediaId: author.avatarMediaId,
avatarUrl: author.avatarUrl
}).open();
Layout.maximumWidth: contentMaxWidth - timeLabel.implicitWidth - rowLayout.spacing
text: visible ? author.displayName : ""
textFormat: Text.PlainText
font.weight: Font.Bold
color: author.color
elide: Text.ElideRight
MouseArea {
anchors.fill: parent
cursorShape: Qt.PointingHandCursor
onClicked: {
userDetailDialog.createObject(QQC2.ApplicationWindow.overlay, {
room: currentRoom,
user: author.object,
displayName: author.displayName,
avatarMediaId: author.avatarMediaId,
avatarUrl: author.avatarUrl
}).open();
}
}
}
QQC2.Label {
id: timeLabel
text: visible ? time.toLocaleTimeString(Qt.locale(), Locale.ShortFormat) : ""
color: Kirigami.Theme.disabledTextColor
QQC2.ToolTip.visible: hoverHandler.hovered
QQC2.ToolTip.text: time.toLocaleString(Qt.locale(), Locale.LongFormat)
QQC2.ToolTip.delay: Kirigami.Units.toolTipDelay
HoverHandler {
id: hoverHandler
}
}
}
QQC2.Label {
id: timeLabel
Loader {
id: replyLoader
text: visible ? time.toLocaleTimeString(Qt.locale(), Locale.ShortFormat) : ""
color: Kirigami.Theme.disabledTextColor
QQC2.ToolTip.visible: hoverHandler.hovered
QQC2.ToolTip.text: time.toLocaleString(Qt.locale(), Locale.LongFormat)
QQC2.ToolTip.delay: Kirigami.Units.toolTipDelay
Layout.maximumWidth: contentMaxWidth
HoverHandler {
id: hoverHandler
active: model.reply !== undefined
visible: active
sourceComponent: ReplyComponent {
name: currentRoom.htmlSafeMemberName(reply.author.id)
avatar: reply.author.avatarMediaId ? ("image://mxc/" + reply.author.avatarMediaId) : ""
color: reply.author.color
}
Connections {
target: replyLoader.item
function onReplyClicked() {
replyClicked(reply.eventId)
}
}
}
}
Loader {
id: replyLoader
Layout.maximumWidth: contentMaxWidth
background: Item {
Kirigami.ShadowedRectangle {
id: bubbleBackground
visible: cardBackground && !Config.compactLayout
anchors.fill: parent
color: {
if (model.author.isLocalUser) {
return Kirigami.ColorUtils.tintWithAlpha(Kirigami.Theme.backgroundColor, Kirigami.Theme.highlightColor, 0.15)
} else if (root.isHighlighted) {
return Kirigami.Theme.positiveBackgroundColor
} else {
return Kirigami.Theme.backgroundColor
}
}
radius: Kirigami.Units.smallSpacing
shadow.size: Kirigami.Units.smallSpacing
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.width: 1
active: model.reply !== undefined
visible: active
sourceComponent: ReplyComponent {
name: currentRoom.htmlSafeMemberName(reply.author.id)
avatar: reply.author.avatarMediaId ? ("image://mxc/" + reply.author.avatarMediaId) : ""
color: reply.author.color
}
Connections {
target: replyLoader.item
function onReplyClicked() {
replyClicked(reply.eventId)
Behavior on color {
ColorAnimation {target: bubbleBackground; duration: Kirigami.Units.veryLongDuration; easing.type: Easing.InOutCubic}
}
}
}
}
background: Item {
Kirigami.ShadowedRectangle {
id: bubbleBackground
visible: cardBackground && !Config.compactLayout
anchors.fill: parent
color: {
if (model.author.isLocalUser) {
return Kirigami.ColorUtils.tintWithAlpha(Kirigami.Theme.backgroundColor, Kirigami.Theme.highlightColor, 0.15)
} else if (timelineContainer.isHighlighted) {
return Kirigami.Theme.positiveBackgroundColor
} else {
return Kirigami.Theme.backgroundColor
}
}
radius: 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)
border.color: Kirigami.ColorUtils.tintWithAlpha(color, Kirigami.Theme.textColor, 0.15)
border.width: 1
background: Rectangle {
visible: mainContainer.hovered
color: Kirigami.ColorUtils.tintWithAlpha(Kirigami.Theme.backgroundColor, Kirigami.Theme.highlightColor, 0.15)
radius: Kirigami.Units.smallSpacing
}
Behavior on color {
ColorAnimation {target: bubbleBackground; duration: Kirigami.Units.veryLongDuration; easing.type: Easing.InOutCubic}
}
}
TapHandler {
acceptedButtons: Qt.RightButton
onTapped: root.openContextMenu()
}
TapHandler {
acceptedButtons: Qt.LeftButton
onLongPressed: root.openContextMenu()
}
}
Loader {
id: loader
anchors {
left: bubble.left
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 { }
}
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
TapHandler {
acceptedButtons: Qt.RightButton
acceptedDevices: PointerDevice.Mouse
onTapped: timelineContainer.openContextMenu()
}
TapHandler {
acceptedButtons: Qt.LeftButton
onLongPressed: timelineContainer.openContextMenu()
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 {
id: videoDelegate
onReplyClicked: ListView.view.goToEvent(eventID)
hoverComponent: hoverActions
property bool playOnFinished: false
readonly property bool downloaded: progressInfo && progressInfo.completed

View File

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