From 280f316b27ece825eee4e7c776177ef09eb15304 Mon Sep 17 00:00:00 2001 From: Nicolas Werner Date: Wed, 18 Oct 2023 22:33:23 +0200 Subject: [PATCH] Try out scrollview for timeline --- resources/qml/CommunitiesList.qml | 6 +- resources/qml/MatrixText.qml | 3 - resources/qml/MessageView.qml | 547 +++++++++++++------------- resources/qml/ReplyPopup.qml | 2 +- resources/qml/dialogs/AliasEditor.qml | 8 +- src/timeline/TimelineFilter.cpp | 22 +- 6 files changed, 293 insertions(+), 295 deletions(-) diff --git a/resources/qml/CommunitiesList.qml b/resources/qml/CommunitiesList.qml index 81f0640e..37a44163 100644 --- a/resources/qml/CommunitiesList.qml +++ b/resources/qml/CommunitiesList.qml @@ -143,11 +143,11 @@ Page { enabled: false height: avatarSize roomid: model.id - textColor: model.avatarUrl.startsWith(":/") ? communityItem.unimportantText : communityItem.importantText + textColor: model.avatarUrl?.startsWith(":/") == true ? communityItem.unimportantText : communityItem.importantText url: { - if (model.avatarUrl.startsWith("mxc://")) + if (model.avatarUrl?.startsWith("mxc://") == true) return model.avatarUrl.replace("mxc://", "image://MxcImage/"); - else if (model.avatarUrl.length > 0) + else if ((model.avatarUrl?.length ?? 0) > 0) return model.avatarUrl; else return ""; diff --git a/resources/qml/MatrixText.qml b/resources/qml/MatrixText.qml index c804e72e..bdc0cb6b 100644 --- a/resources/qml/MatrixText.qml +++ b/resources/qml/MatrixText.qml @@ -2,9 +2,6 @@ // // 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.Controls import im.nheko diff --git a/resources/qml/MessageView.qml b/resources/qml/MessageView.qml index 98b9748a..a8e2a4e6 100644 --- a/resources/qml/MessageView.qml +++ b/resources/qml/MessageView.qml @@ -31,334 +31,329 @@ Item { target: MainWindow } - ScrollBar { - id: scrollbar - anchors.bottom: parent.bottom - anchors.right: parent.right - 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 + ScrollView { + id: scrollView anchors.fill: parent - anchors.rightMargin: scrollbar.interactive ? scrollbar.width : 0 - // reuseItems still has a few bugs, see https://bugreports.qt.io/browse/QTBUG-95105 https://bugreports.qt.io/browse/QTBUG-95107 - //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 + ListView { + id: chat - Component { - id: defaultMessageStyle + property int delegateMaxWidth: ((Settings.timelineMaxWidth > 100 && Settings.timelineMaxWidth < chatRoot.availableWidth) ? Settings.timelineMaxWidth : chatRoot.availableWidth) - chatRoot.padding * 2 - scrollView.effectiveScrollBarWidth + readonly property alias filteringInProgress: filteredTimeline.filteringInProgress - TimelineDefaultMessageStyle { - messageActions: messageActionsC - messageContextMenu: messageContextMenuC - replyContextMenu: replyContextMenuC - scrolledToThis: eventId === room.scrollTarget && (y + height > chat.y + chat.contentY && y < chat.y + chat.height + chat.contentY) + // reuseItems still has a few bugs, see https://bugreports.qt.io/browse/QTBUG-95105 https://bugreports.qt.io/browse/QTBUG-95107 + //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 { + 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 { - id: bubbleMessageStyle + Component { + id: bubbleMessageStyle - TimelineBubbleMessageStyle { - messageActions: messageActionsC - messageContextMenu: messageContextMenuC - replyContextMenu: replyContextMenuC - scrolledToThis: eventId === room.scrollTarget && (y + height > chat.y + chat.contentY && y < chat.y + chat.height + chat.contentY) + TimelineBubbleMessageStyle { + messageActions: messageActionsC + messageContextMenu: messageContextMenuC + replyContextMenu: replyContextMenuC + scrolledToThis: eventId === room.scrollTarget && (y + height > chat.y + chat.contentY && y < chat.y + chat.height + chat.contentY) + } } - } - delegate: Settings.bubbles ? bubbleMessageStyle : defaultMessageStyle - footer: Item { - 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 + delegate: Settings.bubbles ? bubbleMessageStyle : defaultMessageStyle + footer: Item { + anchors.horizontalCenter: parent.horizontalCenter anchors.margins: Nheko.paddingLarge - foreground: palette.mid - running: (room && room.paginationInProgress) || chat.filteringInProgress - z: 3 - } - } + // hacky, but works + height: loadingSpinner.height + 2 * Nheko.paddingLarge + visible: (room && room.paginationInProgress) || chat.filteringInProgress - Window.onActiveChanged: readTimer.running = Window.active - onCountChanged: { - // Mark timeline as read - if (atYEnd && room) + Spinner { + id: loadingSpinner + + 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; - } - - 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 { - model: Settings.recentReactions - visible: room ? room.permissions.canSend(MtxEvent.Reaction) : false + property Item attached: null + // use comma to update on scroll + property alias model: row.model - delegate: AbstractButton { - id: button + 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 - property color buttonTextColor: palette.buttonText - property color highlightColor: palette.highlight - required property string modelData - property bool showImage: modelData.startsWith("mxc://") + background: Rectangle { + border.color: palette.buttonText + border.width: 1 + color: palette.window + radius: padding + } + contentItem: RowLayout { + id: row - //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 + property var model + + spacing: messageActionsC.padding + + Repeater { + model: Settings.recentReactions + 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: { - 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: { - if (row.model.isEditable) + if (row.model.isEditable) room.edit = row.model.eventId; + } } - } - ImageButton { - id: reactButton + ImageButton { + id: reactButton - ToolTip.delay: Nheko.tooltipDelay - ToolTip.text: qsTr("React") - ToolTip.visible: hovered - hoverEnabled: true - image: ":/icons/icons/ui/smile-add.svg" - visible: room ? room.permissions.canSend(MtxEvent.Reaction) : false - width: 16 + ToolTip.delay: Nheko.tooltipDelay + ToolTip.text: qsTr("React") + ToolTip.visible: hovered + hoverEnabled: true + image: ":/icons/icons/ui/smile-add.svg" + visible: room ? room.permissions.canSend(MtxEvent.Reaction) : false + 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 : ""; room.input.reaction(event_id, plaintext); TimelineManager.focusMessageInput(); }) - } - ImageButton { - ToolTip.delay: Nheko.tooltipDelay - ToolTip.text: (row.model && row.model.threadId) ? qsTr("Reply in thread") : qsTr("New thread") - ToolTip.visible: hovered - hoverEnabled: true - 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 - width: 16 + } + ImageButton { + ToolTip.delay: Nheko.tooltipDelay + ToolTip.text: (row.model && row.model.threadId) ? qsTr("Reply in thread") : qsTr("New thread") + ToolTip.visible: hovered + hoverEnabled: true + 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 + width: 16 - onClicked: room.thread = (row.model.threadId || row.model.eventId) - } - ImageButton { - ToolTip.delay: Nheko.tooltipDelay - ToolTip.text: qsTr("Reply") - ToolTip.visible: hovered - hoverEnabled: true - image: ":/icons/icons/ui/reply.svg" - visible: room ? room.permissions.canSend(MtxEvent.TextMessage) : false - width: 16 + onClicked: room.thread = (row.model.threadId || row.model.eventId) + } + ImageButton { + ToolTip.delay: Nheko.tooltipDelay + ToolTip.text: qsTr("Reply") + ToolTip.visible: hovered + hoverEnabled: true + image: ":/icons/icons/ui/reply.svg" + visible: room ? room.permissions.canSend(MtxEvent.TextMessage) : false + width: 16 - onClicked: room.reply = row.model.eventId - } - ImageButton { - ToolTip.delay: Nheko.tooltipDelay - ToolTip.text: qsTr("Go to message") - ToolTip.visible: hovered - buttonTextColor: palette.buttonText - hoverEnabled: true - image: ":/icons/icons/ui/go-to.svg" - visible: !!row.model && filteredTimeline.filterByContent - width: 16 + onClicked: room.reply = row.model.eventId + } + ImageButton { + ToolTip.delay: Nheko.tooltipDelay + ToolTip.text: qsTr("Go to message") + ToolTip.visible: hovered + buttonTextColor: palette.buttonText + hoverEnabled: true + image: ":/icons/icons/ui/go-to.svg" + visible: !!row.model && filteredTimeline.filterByContent + width: 16 - onClicked: { - topBar.searchString = ""; - room.showEvent(row.model.eventId); + onClicked: { + topBar.searchString = ""; + 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 - 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) + onActivated: { + chat.contentY = chat.contentY - chat.height * 0.9; + chat.returnToBounds(); } } - } - Shortcut { - sequence: StandardKey.MoveToPreviousPage + Shortcut { + sequence: StandardKey.MoveToNextPage - onActivated: { - chat.contentY = chat.contentY - chat.height * 0.9; - chat.returnToBounds(); + onActivated: { + chat.contentY = chat.contentY + chat.height * 0.9; + chat.returnToBounds(); + } } - } - Shortcut { - sequence: StandardKey.MoveToNextPage + Shortcut { + sequence: StandardKey.Cancel - onActivated: { - chat.contentY = chat.contentY + chat.height * 0.9; - chat.returnToBounds(); - } - } - Shortcut { - sequence: StandardKey.Cancel - - onActivated: { - if (room.input.uploads.length > 0) + onActivated: { + if (room.input.uploads.length > 0) room.input.declineUploads(); - else if (room.reply) + else if (room.reply) room.reply = undefined; - else if (room.edit) + else if (room.edit) room.edit = undefined; - else + else room.thread = undefined; - 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); + TimelineManager.focusMessageInput(); } } - } - Shortcut { - sequence: "Ctrl+E" - onActivated: { - room.edit = room.reply; + // 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) } - } - Timer { - id: readTimer + Shortcut { + sequence: "Alt+Down" - 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 - onTriggered: { - if (room) + 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: { + room.edit = room.reply; + } + } + Timer { + id: readTimer + + interval: 1000 + + // force current read index to update + onTriggered: { + if (room) room.setCurrentIndex(room.currentIndex); + } } } } + Platform.Menu { id: messageContextMenuC @@ -641,7 +636,7 @@ Item { anchors { bottom: parent.bottom bottomMargin: Nheko.paddingMedium + (fullWidth - width) / 2 - right: scrollbar.left + right: parent.left rightMargin: Nheko.paddingMedium + (fullWidth - width) / 2 } Image { diff --git a/resources/qml/ReplyPopup.qml b/resources/qml/ReplyPopup.qml index 5657c08a..17ce7ee4 100644 --- a/resources/qml/ReplyPopup.qml +++ b/resources/qml/ReplyPopup.qml @@ -29,7 +29,7 @@ Rectangle { anchors.rightMargin: replyPopup.width < 450 ? 2 * (22 + 16) : 3 * (22 + 16) anchors.top: parent.top anchors.topMargin: Nheko.paddingSmall - eventId: room.reply ?? "" + eventId: room?.reply ?? "" userColor: TimelineManager.userColor(modelData.userId, palette.window) visible: room && room.reply maxWidth: parent.width - anchors.leftMargin - anchors.rightMargin diff --git a/resources/qml/dialogs/AliasEditor.qml b/resources/qml/dialogs/AliasEditor.qml index c49ad321..df2938ef 100644 --- a/resources/qml/dialogs/AliasEditor.qml +++ b/resources/qml/dialogs/AliasEditor.qml @@ -4,10 +4,10 @@ import ".." import "../components" -import QtQuick 2.12 -import QtQuick.Controls 2.5 -import QtQuick.Layouts 1.3 -import im.nheko 1.0 +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts +import im.nheko ApplicationWindow { diff --git a/src/timeline/TimelineFilter.cpp b/src/timeline/TimelineFilter.cpp index 6f2f9e7a..c2d9e31b 100644 --- a/src/timeline/TimelineFilter.cpp +++ b/src/timeline/TimelineFilter.cpp @@ -163,14 +163,20 @@ TimelineFilter::setSource(TimelineModel *s) this->setSourceModel(s); - connect(s, &TimelineModel::currentIndexChanged, this, &TimelineFilter::currentIndexChanged); - connect( - s, &TimelineModel::fetchedMore, this, &TimelineFilter::fetchAgain, Qt::QueuedConnection); - connect(s, - &TimelineModel::dataChanged, - this, - &TimelineFilter::sourceDataChanged, - Qt::QueuedConnection); + if (s) { + connect( + s, &TimelineModel::currentIndexChanged, this, &TimelineFilter::currentIndexChanged); + connect(s, + &TimelineModel::fetchedMore, + this, + &TimelineFilter::fetchAgain, + Qt::QueuedConnection); + connect(s, + &TimelineModel::dataChanged, + this, + &TimelineFilter::sourceDataChanged, + Qt::QueuedConnection); + } // reset the search index a second time just to be safe. incrementalSearchIndex = 0;