Try out scrollview for timeline

This commit is contained in:
Nicolas Werner 2023-10-18 22:33:23 +02:00
parent fab7805610
commit 280f316b27
No known key found for this signature in database
GPG key ID: C8D75E610773F2D9
6 changed files with 293 additions and 295 deletions

View file

@ -143,11 +143,11 @@ Page {
enabled: false enabled: false
height: avatarSize height: avatarSize
roomid: model.id roomid: model.id
textColor: model.avatarUrl.startsWith(":/") ? communityItem.unimportantText : communityItem.importantText textColor: model.avatarUrl?.startsWith(":/") == true ? communityItem.unimportantText : communityItem.importantText
url: { url: {
if (model.avatarUrl.startsWith("mxc://")) if (model.avatarUrl?.startsWith("mxc://") == true)
return model.avatarUrl.replace("mxc://", "image://MxcImage/"); return model.avatarUrl.replace("mxc://", "image://MxcImage/");
else if (model.avatarUrl.length > 0) else if ((model.avatarUrl?.length ?? 0) > 0)
return model.avatarUrl; return model.avatarUrl;
else else
return ""; return "";

View file

@ -2,9 +2,6 @@
// //
// SPDX-License-Identifier: GPL-3.0-or-later // SPDX-License-Identifier: GPL-3.0-or-later
// TODO: using any Qt 6 API version will screw up the reply text color. We need to
// figure out a more permanent fix than just importing the old version.
//import QtQuick 2.15
import QtQuick import QtQuick
import QtQuick.Controls import QtQuick.Controls
import im.nheko import im.nheko

View file

@ -31,334 +31,329 @@ Item {
target: MainWindow target: MainWindow
} }
ScrollBar {
id: scrollbar
anchors.bottom: parent.bottom ScrollView {
anchors.right: parent.right id: scrollView
anchors.top: parent.top
parent: chat.parent
}
ListView {
id: chat
property int delegateMaxWidth: ((Settings.timelineMaxWidth > 100 && Settings.timelineMaxWidth < chatRoot.availableWidth) ? Settings.timelineMaxWidth : chatRoot.availableWidth) - chatRoot.padding * 2 - (scrollbar.interactive ? scrollbar.width : 0)
readonly property alias filteringInProgress: filteredTimeline.filteringInProgress
ScrollBar.vertical: scrollbar
anchors.fill: parent anchors.fill: parent
anchors.rightMargin: scrollbar.interactive ? scrollbar.width : 0 ListView {
// reuseItems still has a few bugs, see https://bugreports.qt.io/browse/QTBUG-95105 https://bugreports.qt.io/browse/QTBUG-95107 id: chat
//onModelChanged: if (room) room.sendReset()
//reuseItems: true
boundsBehavior: Flickable.StopAtBounds
displayMarginBeginning: height / 4
displayMarginEnd: height / 4
model: (filteredTimeline.filterByThread || filteredTimeline.filterByContent) ? filteredTimeline : room
//pixelAligned: true
spacing: 2
verticalLayoutDirection: ListView.BottomToTop
Component { property int delegateMaxWidth: ((Settings.timelineMaxWidth > 100 && Settings.timelineMaxWidth < chatRoot.availableWidth) ? Settings.timelineMaxWidth : chatRoot.availableWidth) - chatRoot.padding * 2 - scrollView.effectiveScrollBarWidth
id: defaultMessageStyle readonly property alias filteringInProgress: filteredTimeline.filteringInProgress
TimelineDefaultMessageStyle { // reuseItems still has a few bugs, see https://bugreports.qt.io/browse/QTBUG-95105 https://bugreports.qt.io/browse/QTBUG-95107
messageActions: messageActionsC //onModelChanged: if (room) room.sendReset()
messageContextMenu: messageContextMenuC //reuseItems: true
replyContextMenu: replyContextMenuC boundsBehavior: Flickable.StopAtBounds
scrolledToThis: eventId === room.scrollTarget && (y + height > chat.y + chat.contentY && y < chat.y + chat.height + chat.contentY) displayMarginBeginning: height / 4
displayMarginEnd: height / 4
model: (filteredTimeline.filterByThread || filteredTimeline.filterByContent) ? filteredTimeline : room
//pixelAligned: true
spacing: 2
verticalLayoutDirection: ListView.BottomToTop
Component {
id: defaultMessageStyle
TimelineDefaultMessageStyle {
messageActions: messageActionsC
messageContextMenu: messageContextMenuC
replyContextMenu: replyContextMenuC
scrolledToThis: eventId === room.scrollTarget && (y + height > chat.y + chat.contentY && y < chat.y + chat.height + chat.contentY)
}
} }
} Component {
Component { id: bubbleMessageStyle
id: bubbleMessageStyle
TimelineBubbleMessageStyle { TimelineBubbleMessageStyle {
messageActions: messageActionsC messageActions: messageActionsC
messageContextMenu: messageContextMenuC messageContextMenu: messageContextMenuC
replyContextMenu: replyContextMenuC replyContextMenu: replyContextMenuC
scrolledToThis: eventId === room.scrollTarget && (y + height > chat.y + chat.contentY && y < chat.y + chat.height + chat.contentY) scrolledToThis: eventId === room.scrollTarget && (y + height > chat.y + chat.contentY && y < chat.y + chat.height + chat.contentY)
}
} }
}
delegate: Settings.bubbles ? bubbleMessageStyle : defaultMessageStyle delegate: Settings.bubbles ? bubbleMessageStyle : defaultMessageStyle
footer: Item { footer: Item {
anchors.horizontalCenter: parent.horizontalCenter anchors.horizontalCenter: parent.horizontalCenter
anchors.margins: Nheko.paddingLarge
// hacky, but works
height: loadingSpinner.height + 2 * Nheko.paddingLarge
visible: (room && room.paginationInProgress) || chat.filteringInProgress
Spinner {
id: loadingSpinner
anchors.centerIn: parent
anchors.margins: Nheko.paddingLarge anchors.margins: Nheko.paddingLarge
foreground: palette.mid // hacky, but works
running: (room && room.paginationInProgress) || chat.filteringInProgress height: loadingSpinner.height + 2 * Nheko.paddingLarge
z: 3 visible: (room && room.paginationInProgress) || chat.filteringInProgress
}
}
Window.onActiveChanged: readTimer.running = Window.active Spinner {
onCountChanged: { id: loadingSpinner
// Mark timeline as read
if (atYEnd && room) anchors.centerIn: parent
anchors.margins: Nheko.paddingLarge
foreground: palette.mid
running: (room && room.paginationInProgress) || chat.filteringInProgress
z: 3
}
}
Window.onActiveChanged: readTimer.running = Window.active
onCountChanged: {
// Mark timeline as read
if (atYEnd && room)
model.currentIndex = 0; model.currentIndex = 0;
}
TimelineFilter {
id: filteredTimeline
filterByContent: chatRoot.searchString
filterByThread: room ? room.thread : ""
source: room
}
Control {
id: messageActionsC
property Item attached: null
// use comma to update on scroll
property alias model: row.model
hoverEnabled: true
padding: Nheko.paddingSmall
visible: Settings.buttonsInTimeline && !!attached && (attached.hovered || hovered)
z: 10
parent: chat.contentItem
anchors.bottom: attached?.top
anchors.right: attached?.right
background: Rectangle {
border.color: palette.buttonText
border.width: 1
color: palette.window
radius: padding
} }
contentItem: RowLayout {
id: row
property var model TimelineFilter {
id: filteredTimeline
spacing: messageActionsC.padding filterByContent: chatRoot.searchString
filterByThread: room ? room.thread : ""
source: room
}
Control {
id: messageActionsC
Repeater { property Item attached: null
model: Settings.recentReactions // use comma to update on scroll
visible: room ? room.permissions.canSend(MtxEvent.Reaction) : false property alias model: row.model
delegate: AbstractButton { hoverEnabled: true
id: button padding: Nheko.paddingSmall
visible: Settings.buttonsInTimeline && !!attached && (attached.hovered || hovered)
z: 10
parent: chat.contentItem
anchors.bottom: attached?.top
anchors.right: attached?.right
property color buttonTextColor: palette.buttonText background: Rectangle {
property color highlightColor: palette.highlight border.color: palette.buttonText
required property string modelData border.width: 1
property bool showImage: modelData.startsWith("mxc://") color: palette.window
radius: padding
}
contentItem: RowLayout {
id: row
//Layout.preferredHeight: fontMetrics.height property var model
Layout.alignment: Qt.AlignBottom
focusPolicy: Qt.NoFocus spacing: messageActionsC.padding
height: showImage ? 16 : buttonText.implicitHeight
implicitHeight: showImage ? 16 : buttonText.implicitHeight Repeater {
implicitWidth: showImage ? 16 : buttonText.implicitWidth model: Settings.recentReactions
width: showImage ? 16 : buttonText.implicitWidth visible: room ? room.permissions.canSend(MtxEvent.Reaction) : false
delegate: AbstractButton {
id: button
property color buttonTextColor: palette.buttonText
property color highlightColor: palette.highlight
required property string modelData
property bool showImage: modelData.startsWith("mxc://")
//Layout.preferredHeight: fontMetrics.height
Layout.alignment: Qt.AlignBottom
focusPolicy: Qt.NoFocus
height: showImage ? 16 : buttonText.implicitHeight
implicitHeight: showImage ? 16 : buttonText.implicitHeight
implicitWidth: showImage ? 16 : buttonText.implicitWidth
width: showImage ? 16 : buttonText.implicitWidth
onClicked: {
room.input.reaction(row.model.eventId, modelData);
TimelineManager.focusMessageInput();
}
Label {
id: buttonText
anchors.centerIn: parent
color: button.hovered ? button.highlightColor : button.buttonTextColor
font.family: Settings.emojiFont
horizontalAlignment: Text.AlignHCenter
padding: 0
text: button.modelData
verticalAlignment: Text.AlignVCenter
visible: !button.showImage
}
Image {
id: buttonImg
// Workaround, can't get icon.source working for now...
anchors.fill: parent
fillMode: Image.PreserveAspectFit
source: button.showImage ? (button.modelData.replace("mxc://", "image://MxcImage/") + "?scale") : ""
sourceSize.height: button.height
sourceSize.width: button.width
}
NhekoCursorShape {
anchors.fill: parent
cursorShape: Qt.PointingHandCursor
}
Ripple {
color: Qt.rgba(buttonTextColor.r, buttonTextColor.g, buttonTextColor.b, 0.5)
}
}
}
ImageButton {
ToolTip.delay: Nheko.tooltipDelay
ToolTip.text: qsTr("Edit")
ToolTip.visible: hovered
buttonTextColor: palette.buttonText
hoverEnabled: true
image: ":/icons/icons/ui/edit.svg"
visible: !!row.model && row.model.isEditable
width: 16
onClicked: { onClicked: {
room.input.reaction(row.model.eventId, modelData); if (row.model.isEditable)
TimelineManager.focusMessageInput();
}
Label {
id: buttonText
anchors.centerIn: parent
color: button.hovered ? button.highlightColor : button.buttonTextColor
font.family: Settings.emojiFont
horizontalAlignment: Text.AlignHCenter
padding: 0
text: button.modelData
verticalAlignment: Text.AlignVCenter
visible: !button.showImage
}
Image {
id: buttonImg
// Workaround, can't get icon.source working for now...
anchors.fill: parent
fillMode: Image.PreserveAspectFit
source: button.showImage ? (button.modelData.replace("mxc://", "image://MxcImage/") + "?scale") : ""
sourceSize.height: button.height
sourceSize.width: button.width
}
NhekoCursorShape {
anchors.fill: parent
cursorShape: Qt.PointingHandCursor
}
Ripple {
color: Qt.rgba(buttonTextColor.r, buttonTextColor.g, buttonTextColor.b, 0.5)
}
}
}
ImageButton {
ToolTip.delay: Nheko.tooltipDelay
ToolTip.text: qsTr("Edit")
ToolTip.visible: hovered
buttonTextColor: palette.buttonText
hoverEnabled: true
image: ":/icons/icons/ui/edit.svg"
visible: !!row.model && row.model.isEditable
width: 16
onClicked: {
if (row.model.isEditable)
room.edit = row.model.eventId; room.edit = row.model.eventId;
}
} }
} ImageButton {
ImageButton { id: reactButton
id: reactButton
ToolTip.delay: Nheko.tooltipDelay ToolTip.delay: Nheko.tooltipDelay
ToolTip.text: qsTr("React") ToolTip.text: qsTr("React")
ToolTip.visible: hovered ToolTip.visible: hovered
hoverEnabled: true hoverEnabled: true
image: ":/icons/icons/ui/smile-add.svg" image: ":/icons/icons/ui/smile-add.svg"
visible: room ? room.permissions.canSend(MtxEvent.Reaction) : false visible: room ? room.permissions.canSend(MtxEvent.Reaction) : false
width: 16 width: 16
onClicked: emojiPopup.visible ? emojiPopup.close() : emojiPopup.show(reactButton, room.roomId, function (plaintext, markdown) { onClicked: emojiPopup.visible ? emojiPopup.close() : emojiPopup.show(reactButton, room.roomId, function (plaintext, markdown) {
var event_id = row.model ? row.model.eventId : ""; var event_id = row.model ? row.model.eventId : "";
room.input.reaction(event_id, plaintext); room.input.reaction(event_id, plaintext);
TimelineManager.focusMessageInput(); TimelineManager.focusMessageInput();
}) })
} }
ImageButton { ImageButton {
ToolTip.delay: Nheko.tooltipDelay ToolTip.delay: Nheko.tooltipDelay
ToolTip.text: (row.model && row.model.threadId) ? qsTr("Reply in thread") : qsTr("New thread") ToolTip.text: (row.model && row.model.threadId) ? qsTr("Reply in thread") : qsTr("New thread")
ToolTip.visible: hovered ToolTip.visible: hovered
hoverEnabled: true hoverEnabled: true
image: (row.model && row.model.threadId) ? ":/icons/icons/ui/thread.svg" : ":/icons/icons/ui/new-thread.svg" image: (row.model && row.model.threadId) ? ":/icons/icons/ui/thread.svg" : ":/icons/icons/ui/new-thread.svg"
visible: room ? room.permissions.canSend(MtxEvent.TextMessage) : false visible: room ? room.permissions.canSend(MtxEvent.TextMessage) : false
width: 16 width: 16
onClicked: room.thread = (row.model.threadId || row.model.eventId) onClicked: room.thread = (row.model.threadId || row.model.eventId)
} }
ImageButton { ImageButton {
ToolTip.delay: Nheko.tooltipDelay ToolTip.delay: Nheko.tooltipDelay
ToolTip.text: qsTr("Reply") ToolTip.text: qsTr("Reply")
ToolTip.visible: hovered ToolTip.visible: hovered
hoverEnabled: true hoverEnabled: true
image: ":/icons/icons/ui/reply.svg" image: ":/icons/icons/ui/reply.svg"
visible: room ? room.permissions.canSend(MtxEvent.TextMessage) : false visible: room ? room.permissions.canSend(MtxEvent.TextMessage) : false
width: 16 width: 16
onClicked: room.reply = row.model.eventId onClicked: room.reply = row.model.eventId
} }
ImageButton { ImageButton {
ToolTip.delay: Nheko.tooltipDelay ToolTip.delay: Nheko.tooltipDelay
ToolTip.text: qsTr("Go to message") ToolTip.text: qsTr("Go to message")
ToolTip.visible: hovered ToolTip.visible: hovered
buttonTextColor: palette.buttonText buttonTextColor: palette.buttonText
hoverEnabled: true hoverEnabled: true
image: ":/icons/icons/ui/go-to.svg" image: ":/icons/icons/ui/go-to.svg"
visible: !!row.model && filteredTimeline.filterByContent visible: !!row.model && filteredTimeline.filterByContent
width: 16 width: 16
onClicked: { onClicked: {
topBar.searchString = ""; topBar.searchString = "";
room.showEvent(row.model.eventId); room.showEvent(row.model.eventId);
}
}
ImageButton {
id: optionsButton
ToolTip.delay: Nheko.tooltipDelay
ToolTip.text: qsTr("Options")
ToolTip.visible: hovered
hoverEnabled: true
image: ":/icons/icons/ui/options.svg"
width: 16
onClicked: messageContextMenuC.show(row.model.eventId, row.model.threadId, row.model.type, row.model.isSender, row.model.isEncrypted, row.model.isEditable, "", row.model.body, optionsButton)
} }
} }
ImageButton { }
id: optionsButton Shortcut {
sequence: StandardKey.MoveToPreviousPage
ToolTip.delay: Nheko.tooltipDelay onActivated: {
ToolTip.text: qsTr("Options") chat.contentY = chat.contentY - chat.height * 0.9;
ToolTip.visible: hovered chat.returnToBounds();
hoverEnabled: true
image: ":/icons/icons/ui/options.svg"
width: 16
onClicked: messageContextMenuC.show(row.model.eventId, row.model.threadId, row.model.type, row.model.isSender, row.model.isEncrypted, row.model.isEditable, "", row.model.body, optionsButton)
} }
} }
} Shortcut {
Shortcut { sequence: StandardKey.MoveToNextPage
sequence: StandardKey.MoveToPreviousPage
onActivated: { onActivated: {
chat.contentY = chat.contentY - chat.height * 0.9; chat.contentY = chat.contentY + chat.height * 0.9;
chat.returnToBounds(); chat.returnToBounds();
}
} }
} Shortcut {
Shortcut { sequence: StandardKey.Cancel
sequence: StandardKey.MoveToNextPage
onActivated: { onActivated: {
chat.contentY = chat.contentY + chat.height * 0.9; if (room.input.uploads.length > 0)
chat.returnToBounds();
}
}
Shortcut {
sequence: StandardKey.Cancel
onActivated: {
if (room.input.uploads.length > 0)
room.input.declineUploads(); room.input.declineUploads();
else if (room.reply) else if (room.reply)
room.reply = undefined; room.reply = undefined;
else if (room.edit) else if (room.edit)
room.edit = undefined; room.edit = undefined;
else else
room.thread = undefined; room.thread = undefined;
TimelineManager.focusMessageInput(); TimelineManager.focusMessageInput();
}
}
// These shortcuts use the room timeline because switching to threads and out is annoying otherwise.
// Better solution welcome.
Shortcut {
sequence: "Alt+Up"
onActivated: room.reply = room.indexToId(room.reply ? room.idToIndex(room.reply) + 1 : 0)
}
Shortcut {
sequence: "Alt+Down"
onActivated: {
var idx = room.reply ? room.idToIndex(room.reply) - 1 : -1;
room.reply = idx >= 0 ? room.indexToId(idx) : null;
}
}
Shortcut {
sequence: "Alt+F"
onActivated: {
if (room.reply) {
var forwardMess = forwardCompleterComponent.createObject(timelineRoot);
forwardMess.setMessageEventId(room.reply);
forwardMess.open();
room.reply = null;
timelineRoot.destroyOnClose(forwardMess);
} }
} }
}
Shortcut {
sequence: "Ctrl+E"
onActivated: { // These shortcuts use the room timeline because switching to threads and out is annoying otherwise.
room.edit = room.reply; // Better solution welcome.
Shortcut {
sequence: "Alt+Up"
onActivated: room.reply = room.indexToId(room.reply ? room.idToIndex(room.reply) + 1 : 0)
} }
} Shortcut {
Timer { sequence: "Alt+Down"
id: readTimer
interval: 1000 onActivated: {
var idx = room.reply ? room.idToIndex(room.reply) - 1 : -1;
room.reply = idx >= 0 ? room.indexToId(idx) : null;
}
}
Shortcut {
sequence: "Alt+F"
// force current read index to update onActivated: {
onTriggered: { if (room.reply) {
if (room) var forwardMess = forwardCompleterComponent.createObject(timelineRoot);
forwardMess.setMessageEventId(room.reply);
forwardMess.open();
room.reply = null;
timelineRoot.destroyOnClose(forwardMess);
}
}
}
Shortcut {
sequence: "Ctrl+E"
onActivated: {
room.edit = room.reply;
}
}
Timer {
id: readTimer
interval: 1000
// force current read index to update
onTriggered: {
if (room)
room.setCurrentIndex(room.currentIndex); room.setCurrentIndex(room.currentIndex);
}
} }
} }
} }
Platform.Menu { Platform.Menu {
id: messageContextMenuC id: messageContextMenuC
@ -641,7 +636,7 @@ Item {
anchors { anchors {
bottom: parent.bottom bottom: parent.bottom
bottomMargin: Nheko.paddingMedium + (fullWidth - width) / 2 bottomMargin: Nheko.paddingMedium + (fullWidth - width) / 2
right: scrollbar.left right: parent.left
rightMargin: Nheko.paddingMedium + (fullWidth - width) / 2 rightMargin: Nheko.paddingMedium + (fullWidth - width) / 2
} }
Image { Image {

View file

@ -29,7 +29,7 @@ Rectangle {
anchors.rightMargin: replyPopup.width < 450 ? 2 * (22 + 16) : 3 * (22 + 16) anchors.rightMargin: replyPopup.width < 450 ? 2 * (22 + 16) : 3 * (22 + 16)
anchors.top: parent.top anchors.top: parent.top
anchors.topMargin: Nheko.paddingSmall anchors.topMargin: Nheko.paddingSmall
eventId: room.reply ?? "" eventId: room?.reply ?? ""
userColor: TimelineManager.userColor(modelData.userId, palette.window) userColor: TimelineManager.userColor(modelData.userId, palette.window)
visible: room && room.reply visible: room && room.reply
maxWidth: parent.width - anchors.leftMargin - anchors.rightMargin maxWidth: parent.width - anchors.leftMargin - anchors.rightMargin

View file

@ -4,10 +4,10 @@
import ".." import ".."
import "../components" import "../components"
import QtQuick 2.12 import QtQuick
import QtQuick.Controls 2.5 import QtQuick.Controls
import QtQuick.Layouts 1.3 import QtQuick.Layouts
import im.nheko 1.0 import im.nheko
ApplicationWindow { ApplicationWindow {

View file

@ -163,14 +163,20 @@ TimelineFilter::setSource(TimelineModel *s)
this->setSourceModel(s); this->setSourceModel(s);
connect(s, &TimelineModel::currentIndexChanged, this, &TimelineFilter::currentIndexChanged); if (s) {
connect( connect(
s, &TimelineModel::fetchedMore, this, &TimelineFilter::fetchAgain, Qt::QueuedConnection); s, &TimelineModel::currentIndexChanged, this, &TimelineFilter::currentIndexChanged);
connect(s, connect(s,
&TimelineModel::dataChanged, &TimelineModel::fetchedMore,
this, this,
&TimelineFilter::sourceDataChanged, &TimelineFilter::fetchAgain,
Qt::QueuedConnection); Qt::QueuedConnection);
connect(s,
&TimelineModel::dataChanged,
this,
&TimelineFilter::sourceDataChanged,
Qt::QueuedConnection);
}
// reset the search index a second time just to be safe. // reset the search index a second time just to be safe.
incrementalSearchIndex = 0; incrementalSearchIndex = 0;