From 88ef7e23a13d241cbe7404e424d26d267b8d60af Mon Sep 17 00:00:00 2001 From: Rello Date: Tue, 4 Aug 2026 22:06:42 +0200 Subject: [PATCH] fix(tray): scroll to top in the activity window on opening - reopening will ensure tha the list is scrolled to the top - before, the scroll location was memorized and and new notifications or conflicts were missed Assisted-by: Codex:GPT-5 Signed-off-by: Rello --- src/gui/activity/qml/ActivitiesWindow.qml | 3 + src/gui/activity/qml/ActivityList.qml | 6 + test/CMakeLists.txt | 1 + test/testactivitieswindow.cpp | 132 ++++++++++++++++++++++ 4 files changed, 142 insertions(+) create mode 100644 test/testactivitieswindow.cpp diff --git a/src/gui/activity/qml/ActivitiesWindow.qml b/src/gui/activity/qml/ActivitiesWindow.qml index 7e1a778ccb26b..e3dc563e641bc 100644 --- a/src/gui/activity/qml/ActivitiesWindow.qml +++ b/src/gui/activity/qml/ActivitiesWindow.qml @@ -26,6 +26,7 @@ WizardStyledWindow { minimumHeight: Style.wizardStandaloneWindowMinimumHeight function resetActivityList() { + activityList.positionAtTop() newActivitiesButtonLoader.active = false } @@ -84,6 +85,7 @@ WizardStyledWindow { ActivityList { id: activityList + objectName: "activityList" anchors.fill: parent activeFocusOnTab: true @@ -109,6 +111,7 @@ WizardStyledWindow { Loader { id: newActivitiesButtonLoader + objectName: "newActivitiesButtonLoader" anchors.top: activityList.top anchors.topMargin: Style.smallSpacing diff --git a/src/gui/activity/qml/ActivityList.qml b/src/gui/activity/qml/ActivityList.qml index 37dee50d50139..6c739069af892 100644 --- a/src/gui/activity/qml/ActivityList.qml +++ b/src/gui/activity/qml/ActivityList.qml @@ -27,6 +27,11 @@ ScrollView { scrollingToTop = true } + function positionAtTop() { + scrollingToTop = false + activityList.positionViewAtBeginning() + } + signal openFile(string filePath) signal activityItemClicked(int index) @@ -52,6 +57,7 @@ ScrollView { ListView { id: activityList + objectName: "activityListView" Accessible.role: Accessible.List Accessible.name: qsTr("Activity list") diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt index e04a968cc79d9..514b2b39e0b19 100644 --- a/test/CMakeLists.txt +++ b/test/CMakeLists.txt @@ -105,6 +105,7 @@ nextcloud_add_test(TrayAccountMenuPolicy) nextcloud_add_test(UnifiedSearchListmodel) nextcloud_add_test(ActivityListModel) nextcloud_add_test(SortedActivityListModel) +nextcloud_add_test(ActivitiesWindow) nextcloud_add_test(ActivityData) add_test(NAME ActivityFileMenuQmlTest COMMAND Qt6::qmltestrunner diff --git a/test/testactivitieswindow.cpp b/test/testactivitieswindow.cpp new file mode 100644 index 0000000000000..a9665095a45ad --- /dev/null +++ b/test/testactivitieswindow.cpp @@ -0,0 +1,132 @@ +/* + * SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: CC0-1.0 + * + * This software is in the public domain, furnished "as is", without technical + * support, and with no warranty, express or implied, as to its usefulness for + * any purpose. + */ + +#include "account.h" +#include "accountmanager.h" +#include "accountstate.h" +#include "activity/activitylistmodel.h" +#include "activity/sortedactivitylistmodel.h" +#include "activity/syncstatussummary.h" +#include "activitylistmodeltestutils.h" +#include "systray.h" +#include "syncenginetestutils.h" +#include "wheelhandler.h" + +#include +#include +#include +#include +#include +#include +#include + +using namespace ActivityListModelTestUtils; + +class TestActivitiesWindow : public QObject +{ + Q_OBJECT + +public: + TestActivitiesWindow() + { + Q_INIT_RESOURCE(resources); + Q_INIT_RESOURCE(theme); + } + + ~TestActivitiesWindow() override + { + if (accountState) { + OCC::AccountManager::instance()->deleteAccount(accountState.data()); + } + } + +private slots: + void initTestCase() + { + QStandardPaths::setTestModeEnabled(true); + + qmlRegisterType("com.nextcloud.desktopclient", 1, 0, "SyncStatusSummary"); + qmlRegisterType("com.nextcloud.desktopclient", 1, 0, "SortedActivityListModel"); + qmlRegisterType("com.nextcloud.desktopclient", 1, 0, "WheelHandler"); + qmlRegisterSingletonInstance("com.nextcloud.desktopclient", 1, 0, "Systray", OCC::Systray::instance()); + + fakeQnam.reset(new FakeQNAM({})); + account = OCC::Account::create(); + account->setCredentials(new FakeCredentials{fakeQnam.data()}); + account->setUrl(QUrl(QStringLiteral("https://example.com"))); + accountState = OCC::AccountManager::instance()->addAccount(account); + QVERIFY(accountState); + } + + void testOpeningResetsViewportWithoutInterruptingLiveUpdates() + { + TestingALM activityModel; + activityModel.setAccountState(accountState.data()); + + const auto accountName = account->displayName(); + for (auto id = 1; id <= 40; ++id) { + const auto activity = exampleNotificationActivity(accountName, id); + activityModel.addNotificationToActivityList(activity); + } + + QQmlApplicationEngine engine; + engine.addImportPath(QStringLiteral("qrc:/qml/theme")); + + QQmlComponent component(&engine, QUrl(QStringLiteral("qrc:/qml/src/gui/activity/qml/ActivitiesWindow.qml"))); + QVERIFY2(component.isReady(), qPrintable(component.errorString())); + + QScopedPointer object(component.createWithInitialProperties({ + {QStringLiteral("activityModel"), QVariant::fromValue(&activityModel)}, + })); + QVERIFY2(object, qPrintable(component.errorString())); + + const auto window = qobject_cast(object.data()); + QVERIFY(window); + + window->show(); + QTRY_VERIFY(window->isVisible()); + + const auto activityList = window->findChild(QStringLiteral("activityList")); + const auto activityListView = window->findChild(QStringLiteral("activityListView")); + const auto newActivitiesButtonLoader = window->findChild(QStringLiteral("newActivitiesButtonLoader")); + QVERIFY(activityList); + QVERIFY(activityListView); + QVERIFY(newActivitiesButtonLoader); + QTRY_VERIFY(activityListView->property("contentHeight").toReal() > activityListView->property("height").toReal()); + + QVERIFY(QMetaObject::invokeMethod(activityListView, "positionViewAtEnd")); + QTRY_VERIFY(!activityList->property("atYBeginning").toBool()); + + window->hide(); + QTRY_VERIFY(!window->isVisible()); + window->show(); + QTRY_VERIFY(window->isVisible()); + QTRY_VERIFY(activityList->property("atYBeginning").toBool()); + + QVERIFY(QMetaObject::invokeMethod(activityListView, "positionViewAtEnd")); + QTRY_VERIFY(!activityList->property("atYBeginning").toBool()); + + auto interactiveActivity = exampleNotificationActivity(accountName, 41); + OCC::ActivityLink action; + action._verb = QByteArrayLiteral("POST"); + interactiveActivity._links.push_back(action); + activityModel.addNotificationToActivityList(interactiveActivity); + + QTRY_VERIFY(!activityList->property("atYBeginning").toBool()); + QTRY_VERIFY(newActivitiesButtonLoader->property("active").toBool()); + } + +private: + QScopedPointer fakeQnam; + OCC::AccountPtr account; + QPointer accountState; +}; + +QTEST_MAIN(TestActivitiesWindow) +#include "testactivitieswindow.moc"