Add a section label at the top which shows the date label of the next section

**Updated**

Add a section label at the top which shows the date label of the next section up. This means that the user will always be able to see the date of all messages on screen.

![image](/uploads/ecbcdc0740877ea0d72e735176353036/image.png)

From the feedback given I've added a background at the top. I also added an underline to the heading which applies both at the top and in the listView since they use the same component. I added it originally for the top because I felt it looked a bit weird having messages appear from behind a heading background the same colour as the listView background.

Note: I know the gaps between messages are not right. I had to set the spacing in the listView to 0 to prevent itemAt returning null. I plan to add it back in as part of the delegate code before it would be merge.

Fixes BUG:454880
This commit is contained in:
James Graham
2022-11-08 19:40:56 +00:00
parent c71beb30f7
commit a4c445d1a5
4 changed files with 137 additions and 63 deletions

View File

@@ -3,15 +3,45 @@
// SPDX-License-Identifier: GPL-3.0-only // SPDX-License-Identifier: GPL-3.0-only
import QtQuick 2.15 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.kirigami 2.15 as Kirigami
Kirigami.Heading { import org.kde.neochat 1.0
level: 4
text: model.showSection ? section : "" QQC2.ItemDelegate {
color: Kirigami.Theme.disabledTextColor id: sectionDelegate
horizontalAlignment: Text.AlignHCenter
verticalAlignment: Text.AlignVCenter property alias labelText: sectionLabel.text
topPadding: Kirigami.Units.largeSpacing * 2 property var maxWidth: Number.POSITIVE_INFINITY
bottomPadding: Kirigami.Units.smallSpacing
topPadding: Kirigami.Units.largeSpacing
bottomPadding: 0 // Note not 0 by default
contentItem: ColumnLayout {
spacing: Kirigami.Units.smallSpacing
Layout.fillWidth: true
Kirigami.Heading {
id: sectionLabel
level: 4
color: Kirigami.Theme.disabledTextColor
horizontalAlignment: Text.AlignHCenter
verticalAlignment: Text.AlignVCenter
Layout.fillWidth: true
Layout.maximumWidth: maxWidth
}
Kirigami.Separator {
Layout.minimumHeight: 2
Layout.fillWidth: true
Layout.maximumWidth: maxWidth
}
}
background: Rectangle {
color: Config.blur ? "transparent" : Kirigami.Theme.backgroundColor
Kirigami.Theme.inherit: false
Kirigami.Theme.colorSet: Kirigami.Theme.Window
}
} }

View File

@@ -11,13 +11,14 @@ import org.kde.neochat 1.0
Control { Control {
id: stateDelegate id: stateDelegate
readonly property bool sectionVisible: model.showSection
// extraWidth defines how the delegate can grow after the listView gets very wide // extraWidth defines how the delegate can grow after the listView gets very wide
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 delegateMaxWidth: Config.compactLayout ? messageListView.width: Math.min(messageListView.width, Kirigami.Units.gridUnit * 40 + extraWidth) readonly property int delegateMaxWidth: Config.compactLayout ? messageListView.width: Math.min(messageListView.width, Kirigami.Units.gridUnit * 40 + extraWidth)
width: delegateMaxWidth width: delegateMaxWidth
// anchors.leftMargin: Kirigami.Units.largeSpacing
// anchors.rightMargin: Kirigami.Units.largeSpacing
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
@@ -46,56 +47,60 @@ Control {
} }
] ]
height: sectionDelegate.height + rowLayout.height height: columnLayout.implicitHeight + columnLayout.anchors.topMargin
SectionDelegate {
id: sectionDelegate ColumnLayout {
id: columnLayout
spacing: sectionVisible ? Kirigami.Units.largeSpacing : 0
anchors.top: parent.top anchors.top: parent.top
anchors.topMargin: sectionVisible ? 0 : Kirigami.Units.largeSpacing
anchors.left: parent.left anchors.left: parent.left
anchors.right: parent.right anchors.right: parent.right
visible: model.showSection
height: visible ? implicitHeight : 0
}
RowLayout { SectionDelegate {
id: rowLayout id: sectionDelegate
height: label.contentHeight Layout.fillWidth: true
anchors.bottom: parent.bottom visible: sectionVisible
anchors.left: parent.left labelText: sectionVisible ? section : ""
anchors.right: parent.right
anchors.leftMargin: Kirigami.Units.gridUnit * 1.5 + Kirigami.Units.smallSpacing + (Config.compactLayout ? Kirigami.Units.largeSpacing * 1.25 : 0)
anchors.rightMargin: Kirigami.Units.largeSpacing
Kirigami.Avatar {
id: icon
Layout.preferredWidth: Kirigami.Units.iconSizes.small
Layout.preferredHeight: Kirigami.Units.iconSizes.small
Layout.alignment: Qt.AlignTop
name: model.displayNameForInitials
source: author.avatarMediaId ? ("image://mxc/" + author.avatarMediaId) : ""
color: author.color
Component {
id: userDetailDialog
UserDetailDialog {}
}
MouseArea {
anchors.fill: parent
onClicked: userDetailDialog.createObject(ApplicationWindow.overlay, {room: currentRoom, user: author.object, displayName: author.displayName, avatarMediaId: author.avatarMediaId, avatarUrl: author.avatarUrl}).open()
}
} }
Label { RowLayout {
id: label id: rowLayout
Layout.alignment: Qt.AlignVCenter implicitHeight: label.contentHeight
Layout.fillWidth: true Layout.fillWidth: true
Layout.preferredHeight: icon.height Layout.leftMargin: Kirigami.Units.gridUnit * 1.5 + Kirigami.Units.smallSpacing * 1.5 + (Config.compactLayout ? Kirigami.Units.largeSpacing * 1.25 : 0)
wrapMode: Text.WordWrap Layout.rightMargin: Kirigami.Units.largeSpacing
textFormat: Text.RichText
text: `<style>a {text-decoration: none;}</style><a href="https://matrix.to/#/${author.id}" style="color: ${author.color}">${model.authorDisplayName}</a> ${aggregateDisplay}` Kirigami.Avatar {
onLinkActivated: userDetailDialog.createObject(ApplicationWindow.overlay, {room: currentRoom, user: author.object, displayName: author.displayName, avatarMediaId: author.avatarMediaId, avatarUrl: author.avatarUrl}).open() id: icon
Layout.preferredWidth: Kirigami.Units.iconSizes.small
Layout.preferredHeight: Kirigami.Units.iconSizes.small
name: author.displayName
source: author.avatarMediaId ? ("image://mxc/" + author.avatarMediaId) : ""
color: author.color
Component {
id: userDetailDialog
UserDetailDialog {}
}
MouseArea {
anchors.fill: parent
onClicked: userDetailDialog.createObject(ApplicationWindow.overlay, {room: currentRoom, user: author.object, displayName: author.displayName, avatarMediaId: author.avatarMediaId, avatarUrl: author.avatarUrl}).open()
}
}
Label {
id: label
Layout.alignment: Qt.AlignVCenter
Layout.fillWidth: true
wrapMode: Text.WordWrap
textFormat: Text.RichText
text: `<style>a {text-decoration: none;}</style><a href="https://matrix.to/#/${author.id}" style="color: ${author.color}">${currentRoom.htmlSafeMemberName(author.id)}</a> ${aggregateDisplay}`
onLinkActivated: userDetailDialog.createObject(ApplicationWindow.overlay, {room: currentRoom, user: author.object, displayName: author.displayName, avatarMediaId: author.avatarMediaId, avatarUrl: author.avatarUrl}).open()
}
} }
} }
} }

View File

@@ -14,6 +14,8 @@ QQC2.ItemDelegate {
default property alias innerObject : column.children default property alias innerObject : column.children
// readonly property bool failed: marks == EventStatus.SendingFailed // readonly property bool failed: marks == EventStatus.SendingFailed
readonly property bool sectionVisible: model.showSection
property bool isEmote: false property bool isEmote: false
property bool cardBackground: true property bool cardBackground: true
property bool isHighlighted: model.isHighlighted || isTemporaryHighlighted property bool isHighlighted: model.isHighlighted || isTemporaryHighlighted
@@ -55,7 +57,7 @@ QQC2.ItemDelegate {
leftInset: Kirigami.Units.smallSpacing leftInset: Kirigami.Units.smallSpacing
rightInset: Kirigami.Units.smallSpacing rightInset: Kirigami.Units.smallSpacing
width: delegateMaxWidth width: delegateMaxWidth
height: sectionDelegate.height + Math.max(model.showAuthor ? avatar.height : 0, bubble.implicitHeight) + loader.height + (showAuthor ? Kirigami.Units.largeSpacing : (Config.compactLayout ? 1 : Kirigami.Units.smallSpacing)) height: sectionDelegate.height + Math.max(model.showAuthor ? avatar.height : 0, bubble.implicitHeight) + loader.height + loader.anchors.topMargin + avatar.anchors.topMargin
background: Rectangle { background: Rectangle {
visible: timelineContainer.hovered visible: timelineContainer.hovered
color: Kirigami.ColorUtils.tintWithAlpha(Kirigami.Theme.backgroundColor, Kirigami.Theme.highlightColor, 0.15) color: Kirigami.ColorUtils.tintWithAlpha(Kirigami.Theme.backgroundColor, Kirigami.Theme.highlightColor, 0.15)
@@ -110,11 +112,11 @@ QQC2.ItemDelegate {
SectionDelegate { SectionDelegate {
id: sectionDelegate id: sectionDelegate
width: parent.width anchors.left: timelineContainer.left
anchors.left: avatar.left anchors.right: timelineContainer.right
anchors.leftMargin: Kirigami.Units.smallSpacing visible: sectionVisible
visible: model.showSection
height: visible ? implicitHeight : 0 height: visible ? implicitHeight : 0
labelText: model.showSection ? section : ""
} }
Kirigami.Avatar { Kirigami.Avatar {
@@ -304,10 +306,9 @@ QQC2.ItemDelegate {
left: bubble.left left: bubble.left
right: parent.right right: parent.right
top: bubble.bottom top: bubble.bottom
topMargin: active && Config.compactLayout ? 0 : Kirigami.Units.smallSpacing topMargin: active ? Kirigami.Units.smallSpacing : 0
} }
height: active ? item.implicitHeight : 0 height: active ? item.implicitHeight : 0
//Layout.bottomMargin: readMarker ? Kirigami.Units.smallSpacing : 0
active: eventType !== MessageEventModel.State && eventType !== MessageEventModel.Notice && reaction != undefined && reaction.length > 0 active: eventType !== MessageEventModel.State && eventType !== MessageEventModel.Notice && reaction != undefined && reaction.length > 0
visible: active visible: active
sourceComponent: ReactionDelegate { } sourceComponent: ReactionDelegate { }

View File

@@ -187,8 +187,12 @@ Kirigami.ScrollablePage {
readonly property int largestVisibleIndex: count > 0 ? indexAt(contentX + (width / 2), contentY + height - 1) : -1 readonly property int largestVisibleIndex: count > 0 ? indexAt(contentX + (width / 2), contentY + height - 1) : -1
readonly property bool isLoaded: page.width * page.height > 10 readonly property bool isLoaded: page.width * page.height > 10
// Spacing needs to be zero or the top sectionLabel overlay will be disrupted.
// This is because itemAt returns null in the spaces.
// All spacing should be handled by the delegates themselves
spacing: 0 spacing: 0
// Ensures that the top item is not covered by sectionBanner if the page is scrolled all the way up
// topMargin: sectionBanner.height
verticalLayoutDirection: ListView.BottomToTop verticalLayoutDirection: ListView.BottomToTop
highlightMoveDuration: 500 highlightMoveDuration: 500
@@ -226,6 +230,40 @@ Kirigami.ScrollablePage {
hasScrolledUpBefore = true; hasScrolledUpBefore = true;
} }
// 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.
// 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.
section.property: "section"
readonly property var sectionBannerItem: contentHeight >= height ? itemAtIndex(sectionBannerIndex()) : undefined
function sectionBannerIndex() {
let center = messageListView.x + messageListView.width / 2;
let yStart = messageListView.y + messageListView.contentY;
let index = -1
let i = 0
while (index === -1 && i < 100) {
index = messageListView.indexAt(center, yStart + i);
i++;
}
return index
}
footer: SectionDelegate {
id: sectionBanner
anchors.left: parent.left
anchors.leftMargin: messageListView.sectionBannerItem ? messageListView.sectionBannerItem.x : 0
anchors.right: parent.right
maxWidth: messageListView.sectionBannerItem ? messageListView.sectionBannerItem.width - Kirigami.Units.largeSpacing * 2 : 0
z: 3
visible: messageListView.sectionBannerItem && messageListView.sectionBannerItem.ListView.section != ""
labelText: messageListView.sectionBannerItem ? messageListView.sectionBannerItem.ListView.section : ""
}
footerPositioning: ListView.OverlayHeader
QQC2.Popup { QQC2.Popup {
anchors.centerIn: parent anchors.centerIn: parent
@@ -589,7 +627,7 @@ Kirigami.ScrollablePage {
let center = messageListView.x + messageListView.width / 2; let center = messageListView.x + messageListView.width / 2;
let index = -1 let index = -1
let i = 0 let i = 0
while(index === -1 && i < 100) { while (index === -1 && i < 100) {
index = messageListView.indexAt(center, messageListView.y + messageListView.contentY + i); index = messageListView.indexAt(center, messageListView.y + messageListView.contentY + i);
i++; i++;
} }
@@ -600,7 +638,7 @@ Kirigami.ScrollablePage {
let center = messageListView.x + messageListView.width / 2; let center = messageListView.x + messageListView.width / 2;
let index = -1 let index = -1
let i = 0 let i = 0
while(index === -1 && i < 100) { while (index === -1 && i < 100) {
index = messageListView.indexAt(center, messageListView.y + messageListView.contentY + messageListView.height - i); index = messageListView.indexAt(center, messageListView.y + messageListView.contentY + messageListView.height - i);
i++ i++
} }