diff options
author | Claudio Cambra <claudio.cambra@gmail.com> | 2022-09-14 02:03:56 +0300 |
---|---|---|
committer | Claudio Cambra <claudio.cambra@gmail.com> | 2022-09-26 19:18:28 +0300 |
commit | f71ddc4d43753a6dc85970e4330a646cd695ee8e (patch) | |
tree | 10e8ebfe692b6c272bf16acd9e09c4a2c81d4ebc /src | |
parent | 6a117be9dd6c5cc72d583a3cbcd290f23c7b617c (diff) |
Add a sortedactivitylistmodel that automatically handles sorting of activities
Signed-off-by: Claudio Cambra <claudio.cambra@gmail.com>
Diffstat (limited to 'src')
-rw-r--r-- | src/gui/CMakeLists.txt | 2 | ||||
-rw-r--r-- | src/gui/owncloudgui.cpp | 4 | ||||
-rw-r--r-- | src/gui/tray/ActivityItem.qml | 4 | ||||
-rw-r--r-- | src/gui/tray/ActivityList.qml | 9 | ||||
-rw-r--r-- | src/gui/tray/activitydata.h | 10 | ||||
-rw-r--r-- | src/gui/tray/activitylistmodel.cpp | 43 | ||||
-rw-r--r-- | src/gui/tray/activitylistmodel.h | 1 | ||||
-rw-r--r-- | src/gui/tray/notificationhandler.cpp | 2 | ||||
-rw-r--r-- | src/gui/tray/sortedactivitylistmodel.cpp | 110 | ||||
-rw-r--r-- | src/gui/tray/sortedactivitylistmodel.h | 46 | ||||
-rw-r--r-- | src/gui/tray/usermodel.cpp | 14 | ||||
-rw-r--r-- | src/libsync/syncfileitem.h | 7 |
12 files changed, 216 insertions, 36 deletions
diff --git a/src/gui/CMakeLists.txt b/src/gui/CMakeLists.txt index 15313219e..0258ff41b 100644 --- a/src/gui/CMakeLists.txt +++ b/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 diff --git a/src/gui/owncloudgui.cpp b/src/gui/owncloudgui.cpp index 17ba100db..ab96fe831 100644 --- a/src/gui/owncloudgui.cpp +++ b/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"); diff --git a/src/gui/tray/ActivityItem.qml b/src/gui/tray/ActivityItem.qml index 4c5214762..30c122238 100644 --- a/src/gui/tray/ActivityItem.qml +++ b/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; } } diff --git a/src/gui/tray/ActivityList.qml b/src/gui/tray/ActivityList.qml index a7c3363db..b108f3cca 100644 --- a/src/gui/tray/ActivityList.qml +++ b/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) } } } diff --git a/src/gui/tray/activitydata.h b/src/gui/tray/activitydata.h index a6fe46651..1a04049dd 100644 --- a/src/gui/tray/activitydata.h +++ b/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; /** diff --git a/src/gui/tray/activitylistmodel.cpp b/src/gui/tray/activitylistmodel.cpp index 131a3dbc8..65a6d7ce3 100644 --- a/src/gui/tray/activitylistmodel.cpp +++ b/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; diff --git a/src/gui/tray/activitylistmodel.h b/src/gui/tray/activitylistmodel.h index bdb82dec0..994e6bce5 100644 --- a/src/gui/tray/activitylistmodel.h +++ b/src/gui/tray/activitylistmodel.h @@ -73,6 +73,7 @@ public: TalkNotificationMessageIdRole, TalkNotificationMessageSentRole, TalkNotificationUserAvatarRole, + ActivityIndexRole, ActivityRole, }; Q_ENUM(DataRole) diff --git a/src/gui/tray/notificationhandler.cpp b/src/gui/tray/notificationhandler.cpp index 9a9e1893b..5ab8ea213 100644 --- a/src/gui/tray/notificationhandler.cpp +++ b/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()) { diff --git a/src/gui/tray/sortedactivitylistmodel.cpp b/src/gui/tray/sortedactivitylistmodel.cpp new file mode 100644 index 000000000..8614ec83e --- /dev/null +++ b/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; +} + +} diff --git a/src/gui/tray/sortedactivitylistmodel.h b/src/gui/tray/sortedactivitylistmodel.h new file mode 100644 index 000000000..dc72a5f63 --- /dev/null +++ b/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(); +}; + +} diff --git a/src/gui/tray/usermodel.cpp b/src/gui/tray/usermodel.cpp index 27e9b3bd8..23b4d5841 100644 --- a/src/gui/tray/usermodel.cpp +++ b/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(); diff --git a/src/libsync/syncfileitem.h b/src/libsync/syncfileitem.h index 9668e3ff6..5e30466bd 100644 --- a/src/libsync/syncfileitem.h +++ b/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) |