Add a sortedactivitylistmodel that automatically handles sorting of activities
authorClaudio Cambra <claudio.cambra@gmail.com>
Tue, 13 Sep 2022 23:03:56 +0000 (01:03 +0200)
committerClaudio Cambra <claudio.cambra@gmail.com>
Mon, 26 Sep 2022 16:18:28 +0000 (18:18 +0200)
Signed-off-by: Claudio Cambra <claudio.cambra@gmail.com>
12 files changed:
src/gui/CMakeLists.txt
src/gui/owncloudgui.cpp
src/gui/tray/ActivityItem.qml
src/gui/tray/ActivityList.qml
src/gui/tray/activitydata.h
src/gui/tray/activitylistmodel.cpp
src/gui/tray/activitylistmodel.h
src/gui/tray/notificationhandler.cpp
src/gui/tray/sortedactivitylistmodel.cpp [new file with mode: 0644]
src/gui/tray/sortedactivitylistmodel.h [new file with mode: 0644]
src/gui/tray/usermodel.cpp
src/libsync/syncfileitem.h

index 15313219e7f64646455426fde1c78b45963987eb..0258ff41bc1631d5887cbd39c8ecf674781484fe 100644 (file)
@@ -207,6 +207,8 @@ set(client_SRCS
     tray/usermodel.cpp
     tray/notificationhandler.h
     tray/notificationhandler.cpp
+    tray/sortedactivitylistmodel.h
+    tray/sortedactivitylistmodel.cpp
     creds/credentialsfactory.h
     tray/talkreply.cpp
     creds/credentialsfactory.cpp
index 17ba100db93b47b5b97fd34af0e293447ae18efd..ab96fe8315e08cc84f775daeed2c969da8bab306 100644 (file)
@@ -34,6 +34,7 @@
 #include "wheelhandler.h"
 #include "common/syncjournalfilerecord.h"
 #include "creds/abstractcredentials.h"
+#include "tray/sortedactivitylistmodel.h"
 #include "tray/syncstatussummary.h"
 #include "tray/unifiedsearchresultslistmodel.h"
 
@@ -121,6 +122,7 @@ ownCloudGui::ownCloudGui(Application *parent)
     qmlRegisterType<UserStatusSelectorModel>("com.nextcloud.desktopclient", 1, 0, "UserStatusSelectorModel");
     qmlRegisterType<ActivityListModel>("com.nextcloud.desktopclient", 1, 0, "ActivityListModel");
     qmlRegisterType<FileActivityListModel>("com.nextcloud.desktopclient", 1, 0, "FileActivityListModel");
+    qmlRegisterType<SortedActivityListModel>("com.nextcloud.desktopclient", 1, 0, "SortedActivityListModel");
     qmlRegisterType<WheelHandler>("com.nextcloud.desktopclient", 1, 0, "WheelHandler");
     qmlRegisterType<CallStateChecker>("com.nextcloud.desktopclient", 1, 0, "CallStateChecker");
 
@@ -128,6 +130,8 @@ ownCloudGui::ownCloudGui(Application *parent)
     qmlRegisterUncreatableType<UserStatus>("com.nextcloud.desktopclient", 1, 0, "UserStatus", "Access to Status enum");
 
     qRegisterMetaTypeStreamOperators<Emoji>();
+
+    qRegisterMetaType<ActivityListModel *>("ActivityListModel*");
     qRegisterMetaType<UnifiedSearchResultsListModel *>("UnifiedSearchResultsListModel*");
     qRegisterMetaType<UserStatus>("UserStatus");
 
index 4c521476225bd114dc439a8b5b6a043d48d8d1e4..30c122238c84771d2b9699749eb5b7f49baeb562 100644 (file)
@@ -55,7 +55,7 @@ ItemDelegate {
 
             onShareButtonClicked: Systray.openShareDialog(model.displayPath, model.path)
 
-            onDismissButtonClicked: activityModel.slotTriggerDismiss(model.index)
+            onDismissButtonClicked: activityModel.slotTriggerDismiss(model.activityIndex)
         }
 
         Loader {
@@ -69,7 +69,7 @@ ItemDelegate {
 
             sourceComponent: TalkReplyTextField {
                 onSendReply: {
-                    UserModel.currentUser.sendReplyMessage(model.index, model.conversationToken, reply, model.messageId);
+                    UserModel.currentUser.sendReplyMessage(model.activityIndex, model.conversationToken, reply, model.messageId);
                     talkReplyTextFieldLoader.visible = false;
                 }
             }
index a7c3363dbf7d7a1424f795c0b80a543f0e2024bc..b108f3cca03221a7689be3619941a9bb88ee1aed 100644 (file)
@@ -6,7 +6,7 @@ import Style 1.0
 
 ScrollView {
     id: controlRoot
-    property alias model: activityList.model
+    property alias model: sortedActivityList.activityListModel
 
     property bool isFileActivityList: false
 
@@ -48,6 +48,11 @@ ScrollView {
         preferredHighlightBegin: 0
         preferredHighlightEnd: controlRoot.height
 
+        model: NC.SortedActivityListModel {
+            id: sortedActivityList
+            activityListModel: controlRoot.model
+        }
+
         delegate: ActivityItem {
             isFileActivityList: controlRoot.isFileActivityList
             width: activityList.contentWidth
@@ -73,7 +78,7 @@ ScrollView {
                 if (model.isCurrentUserFileActivity && model.openablePath) {
                     openFile("file://" + model.openablePath);
                 } else {
-                    activityItemClicked(model.index)
+                    activityItemClicked(model.activityIndex)
                 }
             }
         }
index a6fe46651a2e9c59ff8e95bbf625e2d105ae2ba4..1a04049dd442c5763a5c220a2684d92024ca02b4 100644 (file)
@@ -92,11 +92,14 @@ class Activity
 public:
     using Identifier = QPair<qlonglong, QString>;
 
+    // Note that these are in the order we want to present them in the model!
     enum Type {
-        ActivityType,
+        DummyFetchingActivityType,
         NotificationType,
         SyncResultType,
-        SyncFileItemType
+        SyncFileItemType,
+        ActivityType,
+        DummyMoreActivitiesAvailableType,
     };
 
     static Activity fromActivityJson(const QJsonObject &json, const AccountPtr account);
@@ -144,7 +147,8 @@ public:
     QVector<PreviewData> _previews;
 
     // Stores information about the error
-    int _status;
+    SyncFileItem::Status _syncFileItemStatus;
+    SyncResult::Status _syncResultStatus;
 
     QVector<ActivityLink> _links;
     /**
index 131a3dbc86a61fa78f1acd671df00912822453c7..65a6d7ce300d67e2f40a27542ffa9e7d82e43c1f 100644 (file)
@@ -84,6 +84,7 @@ QHash<int, QByteArray> ActivityListModel::roleNames() const
     roles[TalkNotificationMessageIdRole] = "messageId";
     roles[TalkNotificationMessageSentRole] = "messageSent";
     roles[TalkNotificationUserAvatarRole] = "userAvatar";
+    roles[ActivityIndexRole] = "activityIndex";
     roles[ActivityRole] = "activity";
 
     return roles;
@@ -222,21 +223,21 @@ QVariant ActivityListModel::data(const QModelIndex &index, int role) const
             colorIconPath.append("state-error.svg");
             return colorIconPath;
         } else if (a._type == Activity::SyncFileItemType) {
-            if (a._status == SyncFileItem::NormalError
-                || a._status == SyncFileItem::FatalError
-                || a._status == SyncFileItem::DetailError
-                || a._status == SyncFileItem::BlacklistedError) {
+            if (a._syncFileItemStatus == SyncFileItem::NormalError
+                || a._syncFileItemStatus == SyncFileItem::FatalError
+                || a._syncFileItemStatus == SyncFileItem::DetailError
+                || a._syncFileItemStatus == SyncFileItem::BlacklistedError) {
                 colorIconPath.append("state-error.svg");
                 return colorIconPath;
-            } else if (a._status == SyncFileItem::SoftError
-                || a._status == SyncFileItem::Conflict
-                || a._status == SyncFileItem::Restoration
-                || a._status == SyncFileItem::FileLocked
-                || a._status == SyncFileItem::FileNameInvalid
-                || a._status == SyncFileItem::FileNameClash) {
+            } else if (a._syncFileItemStatus == SyncFileItem::SoftError
+                || a._syncFileItemStatus == SyncFileItem::Conflict
+                || a._syncFileItemStatus == SyncFileItem::Restoration
+                || a._syncFileItemStatus == SyncFileItem::FileLocked
+                || a._syncFileItemStatus == SyncFileItem::FileNameInvalid
+                || a._syncFileItemStatus == SyncFileItem::FileNameClash) {
                 colorIconPath.append("state-warning.svg");
                 return colorIconPath;
-            } else if (a._status == SyncFileItem::FileIgnored) {
+            } else if (a._syncFileItemStatus == SyncFileItem::FileIgnored) {
                 colorIconPath.append("state-info.svg");
                 return colorIconPath;
             } else {
@@ -301,6 +302,8 @@ QVariant ActivityListModel::data(const QModelIndex &index, int role) const
     case ActionRole: {
         switch (a._type) {
         case Activity::ActivityType:
+        case Activity::DummyFetchingActivityType:
+        case Activity::DummyMoreActivitiesAvailableType:
             return "Activity";
         case Activity::NotificationType:
             return "Notification";
@@ -339,7 +342,11 @@ QVariant ActivityListModel::data(const QModelIndex &index, int role) const
     case DisplayActions:
         return _displayActions;
     case ShareableRole:
-        return !data(index, PathRole).toString().isEmpty() && a._objectType == QStringLiteral("files") && _displayActions && a._fileAction != "file_deleted" && a._status != SyncFileItem::FileIgnored;
+        return !data(index, PathRole).toString().isEmpty() &&
+                a._objectType == QStringLiteral("files") &&
+                _displayActions &&
+                a._fileAction != "file_deleted" &&
+                a._syncFileItemStatus != SyncFileItem::FileIgnored;
     case IsCurrentUserFileActivityRole:
         return a._isCurrentUserFileActivity;
     case ThumbnailRole: {
@@ -362,6 +369,8 @@ QVariant ActivityListModel::data(const QModelIndex &index, int role) const
         return replyMessageSent(a);
     case TalkNotificationUserAvatarRole:
         return a._talkNotificationData.userAvatar;
+    case ActivityIndexRole:
+        return index.row();
     case ActivityRole:
         return QVariant::fromValue(a);
     }
@@ -468,7 +477,7 @@ void ActivityListModel::appendMoreActivitiesAvailableEntry()
         && _finalList.last()._objectType != moreActivitiesEntryObjectType) {
 
         Activity a;
-        a._type = Activity::ActivityType;
+        a._type = Activity::DummyMoreActivitiesAvailableType;
         a._accName = _accountState->account()->displayName();
         a._id = -1;
         a._objectType = moreActivitiesEntryObjectType;
@@ -488,7 +497,7 @@ void ActivityListModel::insertOrRemoveDummyFetchingActivity()
     const QString dummyFetchingActivityObjectType = QLatin1String("dummy_fetching_activity");
 
     if (_currentlyFetching && _finalList.isEmpty()) {
-        _dummyFetchingActivities._type = Activity::ActivityType;
+        _dummyFetchingActivities._type = Activity::DummyFetchingActivityType;
         _dummyFetchingActivities._accName = _accountState->account()->displayName();
         _dummyFetchingActivities._id = -2;
         _dummyFetchingActivities._objectType = dummyFetchingActivityObjectType;
@@ -762,7 +771,7 @@ void ActivityListModel::slotTriggerDefaultAction(const int activityIndex)
     const auto path = data(modelIndex, PathRole).toString();
 
     const auto activity = _finalList.at(activityIndex);
-    if (activity._status == SyncFileItem::Conflict) {
+    if (activity._syncFileItemStatus == SyncFileItem::Conflict) {
         Q_ASSERT(!activity._file.isEmpty());
         Q_ASSERT(!activity._folder.isEmpty());
         Q_ASSERT(Utility::isConflictFile(activity._file));
@@ -792,7 +801,7 @@ void ActivityListModel::slotTriggerDefaultAction(const int activityIndex)
         _currentConflictDialog->open();
         ownCloudGui::raiseDialog(_currentConflictDialog);
         return;
-    } else if (activity._status == SyncFileItem::FileNameInvalid) {
+    } else if (activity._syncFileItemStatus == SyncFileItem::FileNameInvalid) {
         if (!_currentInvalidFilenameDialog.isNull()) {
             _currentInvalidFilenameDialog->close();
         }
@@ -811,7 +820,7 @@ void ActivityListModel::slotTriggerDefaultAction(const int activityIndex)
         _currentInvalidFilenameDialog->open();
         ownCloudGui::raiseDialog(_currentInvalidFilenameDialog);
         return;
-    } else if (activity._status == SyncFileItem::FileNameClash) {
+    } else if (activity._syncFileItemStatus == SyncFileItem::FileNameClash) {
         const auto folder = FolderMan::instance()->folder(activity._folder);
         const auto relPath = activity._fileAction == QStringLiteral("file_renamed") ? activity._renamedFile : activity._file;
         SyncJournalFileRecord record;
index bdb82dec040d627cbfd7ac327fed1ec143f32a91..994e6bce50fd32c75440c739b371cab87009bd8b 100644 (file)
@@ -73,6 +73,7 @@ public:
         TalkNotificationMessageIdRole,
         TalkNotificationMessageSentRole,
         TalkNotificationUserAvatarRole,
+        ActivityIndexRole,
         ActivityRole,
     };
     Q_ENUM(DataRole)
index 9a9e1893b9e454dbfc702c5c6d55dd6aa05ad056..5ab8ea213f821bcccc114ca208dede26ccc503bb 100644 (file)
@@ -133,8 +133,6 @@ void ServerNotificationHandler::slotNotificationsReceived(const QJsonDocument &j
             }
         } 
 
-        a._status = 0;
-
         QUrl link(json.value("link").toString());
         if (!link.isEmpty()) {
             if (link.host().isEmpty()) {
diff --git a/src/gui/tray/sortedactivitylistmodel.cpp b/src/gui/tray/sortedactivitylistmodel.cpp
new file mode 100644 (file)
index 0000000..8614ec8
--- /dev/null
@@ -0,0 +1,110 @@
+/*
+ * Copyright (C) by Claudio Cambra <claudio.cambra@nextcloud.com>
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
+ * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
+ * for more details.
+ */
+
+#include "activitylistmodel.h"
+
+#include "sortedactivitylistmodel.h"
+
+namespace OCC {
+
+SortedActivityListModel::SortedActivityListModel(QObject *parent)
+    : QSortFilterProxyModel(parent)
+{
+}
+
+void SortedActivityListModel::sortModel()
+{
+    sort(0);
+}
+
+ActivityListModel* SortedActivityListModel::activityListModel() const
+{
+    return static_cast<ActivityListModel*>(sourceModel());
+}
+
+void SortedActivityListModel::setActivityListModel(ActivityListModel* activityListModel)
+{
+     if(const auto currentSetModel = sourceModel()) {
+         disconnect(currentSetModel, &ActivityListModel::rowsInserted, this, &SortedActivityListModel::sortModel);
+         disconnect(currentSetModel, &ActivityListModel::rowsMoved, this, &SortedActivityListModel::sortModel);
+         disconnect(currentSetModel, &ActivityListModel::rowsRemoved, this, &SortedActivityListModel::sortModel);
+         disconnect(currentSetModel, &ActivityListModel::dataChanged, this, &SortedActivityListModel::sortModel);
+         disconnect(currentSetModel, &ActivityListModel::modelReset, this, &SortedActivityListModel::sortModel);
+     }
+
+     // Re-sort model when any changes take place
+     connect(activityListModel, &ActivityListModel::rowsInserted, this, &SortedActivityListModel::sortModel);
+     connect(activityListModel, &ActivityListModel::rowsMoved, this, &SortedActivityListModel::sortModel);
+     connect(activityListModel, &ActivityListModel::rowsRemoved, this, &SortedActivityListModel::sortModel);
+     connect(activityListModel, &ActivityListModel::dataChanged, this, &SortedActivityListModel::sortModel);
+     connect(activityListModel, &ActivityListModel::modelReset, this, &SortedActivityListModel::sortModel);
+
+    setSourceModel(activityListModel);
+    Q_EMIT activityListModelChanged();
+}
+
+bool SortedActivityListModel::lessThan(const QModelIndex &sourceLeft, const QModelIndex &sourceRight) const
+{
+    if (!sourceLeft.isValid() || !sourceRight.isValid()) {
+        return false;
+    }
+
+    const auto leftActivity = sourceLeft.data(ActivityListModel::ActivityRole).value<Activity>();
+    const auto rightActivity = sourceRight.data(ActivityListModel::ActivityRole).value<Activity>();
+
+    // First compare by general activity type
+    const auto leftType = leftActivity._type;
+
+    if (leftType == Activity::DummyFetchingActivityType) {
+        // The fetching activities dummy activity always goes at the top
+        return true;
+    } else if (leftType == Activity::DummyMoreActivitiesAvailableType) {
+        // Likewise the dummy "more activities available" activity always goes at the bottom
+        return false;
+    }
+
+    if (const auto rightType = rightActivity._type; leftType != rightType) {
+        return leftType < rightType;
+    }
+
+    const auto leftSyncFileItemStatus = leftActivity._syncFileItemStatus;
+    const auto rightSyncFileItemStatus = rightActivity._syncFileItemStatus;
+
+    // Then compare by status
+    if (leftSyncFileItemStatus != rightSyncFileItemStatus) {
+        // We want to shove erors towards the top.
+        return (leftSyncFileItemStatus != SyncFileItem::NoStatus &&
+                leftSyncFileItemStatus != SyncFileItem::Success) ||
+                leftSyncFileItemStatus == SyncFileItem::FatalError ||
+                leftSyncFileItemStatus < rightSyncFileItemStatus;
+    }
+
+    const auto leftSyncResultStatus = leftActivity._syncResultStatus;
+    const auto rightSyncResultStatus = rightActivity._syncResultStatus;
+
+    if (leftSyncResultStatus != rightSyncResultStatus) {
+        // We only ever use SyncResult::Error in activities
+        return (leftSyncResultStatus != SyncResult::Undefined &&
+                leftSyncResultStatus != SyncResult::Success) ||
+                leftSyncResultStatus == SyncResult::Error;
+    }
+
+    // Finally sort by time, latest first
+    const auto leftDateTime = leftActivity._dateTime;
+    const auto rightDateTime = rightActivity._dateTime;
+
+    return leftDateTime > rightDateTime;
+}
+
+}
diff --git a/src/gui/tray/sortedactivitylistmodel.h b/src/gui/tray/sortedactivitylistmodel.h
new file mode 100644 (file)
index 0000000..dc72a5f
--- /dev/null
@@ -0,0 +1,46 @@
+/*
+ * Copyright (C) by Claudio Cambra <claudio.cambra@nextcloud.com>
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
+ * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
+ * for more details.
+ */
+
+#pragma once
+
+#include <QSortFilterProxyModel>
+
+namespace OCC {
+
+class ActivityListModel;
+
+class SortedActivityListModel : public QSortFilterProxyModel
+{
+    Q_OBJECT
+    Q_PROPERTY(ActivityListModel* activityListModel READ activityListModel WRITE setActivityListModel NOTIFY activityListModelChanged)
+
+public:
+    explicit SortedActivityListModel(QObject *parent = nullptr);
+
+    ActivityListModel *activityListModel() const;
+
+signals:
+    void activityListModelChanged();
+
+public slots:
+    void setActivityListModel(ActivityListModel *activityListModel);
+
+protected:
+    bool lessThan(const QModelIndex &sourceLeft, const QModelIndex &sourceRight) const override;
+
+private slots:
+    void sortModel();
+};
+
+}
index 27e9b3bd89fd93c5217a536e09bc7b3c6ad84587..23b4d5841458dd7611757ef82565c05411d32c19 100644 (file)
@@ -437,18 +437,18 @@ void User::slotProgressInfo(const QString &folder, const ProgressInfo &progress)
                 continue;
             }
 
-            if (activity._status == SyncFileItem::Conflict && !QFileInfo(f->path() + activity._file).exists()) {
+            if (activity._syncFileItemStatus == SyncFileItem::Conflict && !QFileInfo(f->path() + activity._file).exists()) {
                 _activityModel->removeActivityFromActivityList(activity);
                 continue;
             }
 
-            if (activity._status == SyncFileItem::FileLocked && !QFileInfo(f->path() + activity._file).exists()) {
+            if (activity._syncFileItemStatus == SyncFileItem::FileLocked && !QFileInfo(f->path() + activity._file).exists()) {
                 _activityModel->removeActivityFromActivityList(activity);
                 continue;
             }
 
 
-            if (activity._status == SyncFileItem::FileIgnored && !QFileInfo(f->path() + activity._file).exists()) {
+            if (activity._syncFileItemStatus == SyncFileItem::FileIgnored && !QFileInfo(f->path() + activity._file).exists()) {
                 _activityModel->removeActivityFromActivityList(activity);
                 continue;
             }
@@ -474,7 +474,7 @@ void User::slotProgressInfo(const QString &folder, const ProgressInfo &progress)
         QStringList conflicts;
         foreach (Activity activity, _activityModel->errorsList()) {
             if (activity._folder == folder
-                && activity._status == SyncFileItem::Conflict) {
+                && activity._syncFileItemStatus == SyncFileItem::Conflict) {
                 conflicts.append(activity._file);
             }
         }
@@ -494,7 +494,7 @@ void User::slotAddError(const QString &folderAlias, const QString &message, Erro
 
         Activity activity;
         activity._type = Activity::SyncResultType;
-        activity._status = SyncResult::Error;
+        activity._syncResultStatus = SyncResult::Error;
         activity._dateTime = QDateTime::fromString(QDateTime::currentDateTime().toString(), Qt::ISODate);
         activity._subject = message;
         activity._message = folderInstance->shortGuiLocalPath();
@@ -529,7 +529,7 @@ void User::slotAddErrorToGui(const QString &folderAlias, SyncFileItem::Status st
 
         Activity activity;
         activity._type = Activity::SyncFileItemType;
-        activity._status = status;
+        activity._syncFileItemStatus = status;
         const auto currentDateTime = QDateTime::currentDateTime();
         activity._dateTime = QDateTime::fromString(currentDateTime.toString(), Qt::ISODate);
         activity._expireAtMsecs = currentDateTime.addMSecs(activityDefaultExpirationTimeMsecs).toMSecsSinceEpoch();
@@ -592,7 +592,7 @@ void User::processCompletedSyncItem(const Folder *folder, const SyncFileItemPtr
 
     Activity activity;
     activity._type = Activity::SyncFileItemType; //client activity
-    activity._status = item->_status;
+    activity._syncFileItemStatus = item->_status;
     activity._dateTime = QDateTime::currentDateTime();
     activity._message = item->_originalFile;
     activity._link = account()->url();
index 9668e3ff6e4cf952325dc9825f7dcd460eff1f20..5e30466bdafd81fdf0a4ae9d9f303056b99ab645 100644 (file)
@@ -46,6 +46,7 @@ public:
     };
     Q_ENUM(Direction)
 
+    // Note: the order of these statuses is used for ordering in the SortedActivityListModel
     enum Status { // stored in 4 bits
         NoStatus,
 
@@ -53,8 +54,6 @@ public:
         NormalError, ///< Error attached to a particular file
         SoftError, ///< More like an information
 
-        Success, ///< The file was properly synced
-
         /** Marks a conflict, old or new.
          *
          * With instruction:IGNORE: detected an old unresolved old conflict
@@ -95,7 +94,9 @@ public:
          *
          * A SoftError caused by blacklisting.
          */
-        BlacklistedError
+        BlacklistedError,
+
+        Success, ///< The file was properly synced
     };
     Q_ENUM(Status)