Просмотр исходного кода

Add a sortedactivitylistmodel that automatically handles sorting of activities

Signed-off-by: Claudio Cambra <claudio.cambra@gmail.com>
Claudio Cambra 3 лет назад
Родитель
Сommit
f71ddc4d43

+ 2 - 0
src/gui/CMakeLists.txt

@@ -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

+ 4 - 0
src/gui/owncloudgui.cpp

@@ -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");
 

+ 2 - 2
src/gui/tray/ActivityItem.qml

@@ -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;
                 }
             }

+ 7 - 2
src/gui/tray/ActivityList.qml

@@ -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)
                 }
             }
         }

+ 7 - 3
src/gui/tray/activitydata.h

@@ -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;
     /**

+ 26 - 17
src/gui/tray/activitylistmodel.cpp

@@ -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;

+ 1 - 0
src/gui/tray/activitylistmodel.h

@@ -73,6 +73,7 @@ public:
         TalkNotificationMessageIdRole,
         TalkNotificationMessageSentRole,
         TalkNotificationUserAvatarRole,
+        ActivityIndexRole,
         ActivityRole,
     };
     Q_ENUM(DataRole)

+ 0 - 2
src/gui/tray/notificationhandler.cpp

@@ -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()) {

+ 110 - 0
src/gui/tray/sortedactivitylistmodel.cpp

@@ -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;
+}
+
+}

+ 46 - 0
src/gui/tray/sortedactivitylistmodel.h

@@ -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();
+};
+
+}

+ 7 - 7
src/gui/tray/usermodel.cpp

@@ -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();

+ 4 - 3
src/libsync/syncfileitem.h

@@ -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)