Add hack around the timeline never settling just right

This is due to some kind of bug in ListView that never resettles
properly for bottom-to-top views. This can arise when the pinned message
is loaded (because that squishes the view) or the window is resized
(because that also resizes the view.)

We can work around it by assuming the following:
1. The RoomPage knows the window is resizing because it gets its height
changed before TimelineView.
2. The first height change can be a marker to position the view at the
beginning.

This fixes the issue for me, I did the following in order to test this:
* Switch between many rooms, especially ones with a pinned message. Now
all of them start at the bottom as they should.
* Resize the window, ensure that if you scrolled it stays around that
position - otherwise it sticks at the bottom.
This commit is contained in:
Joshua Goins
2026-01-16 16:53:18 -05:00
parent 4c37dcf518
commit 39de4d10e4
2 changed files with 41 additions and 13 deletions

View File

@@ -75,6 +75,12 @@ Kirigami.Page {
focus: true
padding: 0
onHeightChanged: {
// HACK: See TimelineView for the hack details.
// We get the height change here *first* so we are informed this is because of a window resize and not due to the pinned message.
(timelineViewLoader.item as TimelineView).resetViewSettling();
}
actions: [
Kirigami.Action {
id: jitsiMeetingAction

View File

@@ -80,12 +80,30 @@ QQC2.ScrollView {
QQC2.ScrollBar.vertical.interactive: false
/**
* @brief Tell the view to resettle again as needed.
*/
function resetViewSettling() {
_private.viewHasSettled = false;
}
ListView {
id: messageListView
// HACK: Use this instead of atYEnd to handle cases like -643.2 at height of 643 not being counted as "at the beginning"
readonly property bool closeToYEnd: -Math.round(contentY) >= height
onHeightChanged: {
// HACK: Fix a bug where Qt doesn't resettle the view properly when the pinned messages changes our height
// We basically want to resettle back at the start if:
// * The user hasn't scrolled before (obviously) *and* that scroll is actually somewhere other than the beginning
// * This is the first height change
if (!_private.viewHasSettled && (!_private.hasScrolledUpBefore || closeToYEnd)) {
positionViewAtBeginning();
_private.viewHasSettled = true;
}
}
/**
* @brief Whether all unread messages in the timeline are visible.
*/
@@ -141,19 +159,13 @@ QQC2.ScrollView {
}
}
Component.onCompleted: {
positionViewAtBeginning();
}
Connections {
target: messageListView.model.sourceModel.timelineMessageModel
function onModelAboutToBeReset() {
(root.QQC2.ApplicationWindow.window as Main).hoverLinkIndicator.text = "";
_private.hasScrolledUpBefore = false;
}
function onModelResetComplete() {
messageListView.positionViewAtBeginning();
_private.viewHasSettled = false;
}
function onReadMarkerAdded() {
@@ -182,13 +194,20 @@ QQC2.ScrollView {
}
}
onCloseToYEndChanged: if (closeToYEnd && _private.hasScrolledUpBefore) {
if (QQC2.ApplicationWindow.window && (QQC2.ApplicationWindow.window.visibility !== QQC2.ApplicationWindow.Hidden)) {
_private.room.markAllMessagesAsRead();
onAtYEndChanged: {
// Don't care about this until the view has settled first.
if (!_private.viewHasSettled) {
return;
}
if (closeToYEnd && _private.hasScrolledUpBefore) {
if (QQC2.ApplicationWindow.window && (QQC2.ApplicationWindow.window.visibility !== QQC2.ApplicationWindow.Hidden)) {
_private.room.markAllMessagesAsRead();
}
_private.hasScrolledUpBefore = false;
} else if (!closeToYEnd) {
_private.hasScrolledUpBefore = true;
}
_private.hasScrolledUpBefore = false;
} else if (!closeToYEnd) {
_private.hasScrolledUpBefore = true;
}
model: root.messageFilterModel
@@ -371,5 +390,8 @@ QQC2.ScrollView {
// Used to determine if scrolling to the bottom should mark the message as unread
property bool hasScrolledUpBefore: false
// Used to determine if the view has settled and should stop moving
property bool viewHasSettled: false
}
}