mirror of
https://github.com/nextcloud/desktop.git
synced 2024-11-21 20:45:51 +03:00
Merge pull request #4933 from nextcloud/bugfix/sorted-activity-model
This commit is contained in:
commit
56fe6a1f30
13 changed files with 243 additions and 244 deletions
|
@ -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
|
||||
|
|
|
@ -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");
|
||||
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
/**
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
@ -455,7 +464,7 @@ void ActivityListModel::ingestActivities(const QJsonArray &activities)
|
|||
}
|
||||
|
||||
if (list.size() > 0) {
|
||||
addEntriesToActivityList(list, ActivityEntryType::ActivityType);
|
||||
addEntriesToActivityList(list);
|
||||
appendMoreActivitiesAvailableEntry();
|
||||
_activityLists.append(list);
|
||||
}
|
||||
|
@ -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;
|
||||
|
@ -479,7 +488,7 @@ void ActivityListModel::appendMoreActivitiesAvailableEntry()
|
|||
a._link = app->url();
|
||||
}
|
||||
|
||||
addEntriesToActivityList({a}, ActivityEntryType::MoreActivitiesAvailableType);
|
||||
addEntriesToActivityList({a});
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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;
|
||||
|
@ -496,7 +505,7 @@ void ActivityListModel::insertOrRemoveDummyFetchingActivity()
|
|||
_dummyFetchingActivities._dateTime = QDateTime::currentDateTime();
|
||||
_dummyFetchingActivities._icon = QLatin1String("qrc:///client/theme/colored/change-bordered.svg");
|
||||
|
||||
addEntriesToActivityList({_dummyFetchingActivities}, ActivityEntryType::DummyFetchingActivityType);
|
||||
addEntriesToActivityList({_dummyFetchingActivities});
|
||||
} else if (!_finalList.isEmpty() && _finalList.first()._objectType == dummyFetchingActivityObjectType) {
|
||||
removeActivityFromActivityList(_dummyFetchingActivities);
|
||||
}
|
||||
|
@ -521,157 +530,25 @@ void ActivityListModel::activitiesReceived(const QJsonDocument &json, int status
|
|||
emit activityJobStatusCode(statusCode);
|
||||
}
|
||||
|
||||
std::pair<int, int> ActivityListModel::rowRangeForEntryType(const ActivityEntryType type) const
|
||||
{
|
||||
// We want to present activities in a certain order, and we want to ensure entry types are grouped together.
|
||||
// We therefore need to find the range of rows in the finalList that represent an entry type group.
|
||||
int startRow = 0;
|
||||
|
||||
// We start from the type that we want to push down the furthest. Cascade through switch cases.
|
||||
// Each time we fall through we add the count of elements in each of the sections that go above.
|
||||
// So, items at the top of the activity list have a startRow of 1. The next section gets the count of that first
|
||||
// section's elements added to startRow. Section 3 gets the count of Section 1 and Section 2 added to startRow.
|
||||
// This goes on and on, with the last section getting startRow as the count of ALL section's element counts.
|
||||
|
||||
switch(type) {
|
||||
case ActivityEntryType::MoreActivitiesAvailableType: // Always needs to be at the bottom
|
||||
startRow = _finalList.count();
|
||||
break;
|
||||
case ActivityEntryType::ActivityType:
|
||||
startRow += _syncFileItemLists.count();
|
||||
Q_FALLTHROUGH();
|
||||
case ActivityEntryType::SyncFileItemType:
|
||||
startRow += _notificationLists.count();
|
||||
Q_FALLTHROUGH();
|
||||
case ActivityEntryType::NotificationType:
|
||||
// We only show one activity for ignored files
|
||||
if(_listOfIgnoredFiles.count() > 0) {
|
||||
startRow += 1;
|
||||
}
|
||||
Q_FALLTHROUGH();
|
||||
case ActivityEntryType::IgnoredFileType:
|
||||
startRow += _notificationErrorsLists.count();
|
||||
Q_FALLTHROUGH();
|
||||
// Remaining types should go at top
|
||||
case ActivityEntryType::ErrorType:
|
||||
case ActivityEntryType::DummyFetchingActivityType:
|
||||
break;
|
||||
}
|
||||
|
||||
// To calculate the end row of the section, we just get the number of entries in the relevant section and then
|
||||
// add it to the startRow.
|
||||
|
||||
int entryRowCount = -1;
|
||||
|
||||
switch(type) {
|
||||
case ActivityEntryType::ActivityType:
|
||||
entryRowCount = _activityLists.count();
|
||||
break;
|
||||
case ActivityEntryType::SyncFileItemType:
|
||||
entryRowCount = _syncFileItemLists.count();
|
||||
break;
|
||||
case ActivityEntryType::NotificationType:
|
||||
entryRowCount = _notificationLists.count();
|
||||
break;
|
||||
case ActivityEntryType::ErrorType:
|
||||
entryRowCount = _notificationErrorsLists.count();
|
||||
break;
|
||||
|
||||
// Single activity sections
|
||||
case ActivityEntryType::IgnoredFileType:
|
||||
if(_listOfIgnoredFiles.count() == 0) {
|
||||
break;
|
||||
}
|
||||
Q_FALLTHROUGH();
|
||||
case ActivityEntryType::MoreActivitiesAvailableType:
|
||||
if(!_showMoreActivitiesAvailableEntry) {
|
||||
break;
|
||||
}
|
||||
Q_FALLTHROUGH();
|
||||
case ActivityEntryType::DummyFetchingActivityType:
|
||||
if(_finalList.count() > 0 && _finalList.first() != _dummyFetchingActivities) {
|
||||
break;
|
||||
}
|
||||
|
||||
// All cascade down to here
|
||||
entryRowCount = 1;
|
||||
break;
|
||||
}
|
||||
|
||||
// Even though we always return a startRow even if the section does not exist,
|
||||
// we return -1 as endRow if the section does not exist.
|
||||
// If we have a -1 we also know that the startRow is "theoretical", where the section
|
||||
// "should" begin, not necessarily where it "does" begin
|
||||
const auto endRow = entryRowCount > 0 ? startRow + entryRowCount - 1 : -1;
|
||||
|
||||
return {startRow, endRow};
|
||||
}
|
||||
|
||||
// Make sure to add activities to their specific entry type lists AFTER adding them to the main list
|
||||
void ActivityListModel::addEntriesToActivityList(const ActivityList &activityList, const ActivityEntryType type)
|
||||
void ActivityListModel::addEntriesToActivityList(const ActivityList &activityList)
|
||||
{
|
||||
if(activityList.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
const auto entryTypeSectionRowRange = rowRangeForEntryType(type);
|
||||
const auto startRow = _finalList.count();
|
||||
|
||||
auto sortedList = activityList;
|
||||
std::sort(sortedList.begin(), sortedList.end());
|
||||
|
||||
if(_finalList.count() == 0 || entryTypeSectionRowRange.second == -1) {
|
||||
// If the finalList is empty or there are no entries belonging to the entry type section, we don't
|
||||
// need to bother with inserting in a correct order and can more quickly just insert all activities.
|
||||
const auto startRow = entryTypeSectionRowRange.first;
|
||||
const auto endRow = startRow + sortedList.count() - 1;
|
||||
|
||||
beginInsertRows({}, startRow, endRow);
|
||||
int i = startRow;
|
||||
for(const auto &activity : sortedList) {
|
||||
_finalList.insert(i, activity);
|
||||
++i;
|
||||
}
|
||||
endInsertRows();
|
||||
return;
|
||||
beginInsertRows({}, startRow, startRow + activityList.count() - 1);
|
||||
for(const auto &activity : activityList) {
|
||||
_finalList.append(activity);
|
||||
}
|
||||
|
||||
// If the finalList is not empty and the entry type's section actually exists (i.e. there is at least
|
||||
// one entry belonging to this entry in the finalList) then we are going to add them granularly.
|
||||
// We make sure to insert the item in a specific place so as to preserve the sort order.
|
||||
int sectionRowEnd = entryTypeSectionRowRange.second;
|
||||
|
||||
const auto insertRow = [&](const int row, const Activity &activity) {
|
||||
beginInsertRows({}, row, row);
|
||||
_finalList.insert(row, activity);
|
||||
endInsertRows();
|
||||
++sectionRowEnd;
|
||||
};
|
||||
|
||||
for(const auto &activity : sortedList) {
|
||||
auto currentRow = entryTypeSectionRowRange.first;
|
||||
|
||||
while(currentRow <= sectionRowEnd) {
|
||||
if(currentRow == sectionRowEnd) {
|
||||
insertRow(currentRow + 1, activity);
|
||||
break;
|
||||
}
|
||||
|
||||
if(activity < _finalList[currentRow]) {
|
||||
insertRow(currentRow, activity);
|
||||
break;
|
||||
}
|
||||
|
||||
++currentRow;
|
||||
}
|
||||
}
|
||||
|
||||
return;
|
||||
endInsertRows();
|
||||
}
|
||||
|
||||
void ActivityListModel::addErrorToActivityList(const Activity &activity)
|
||||
{
|
||||
qCInfo(lcActivity) << "Error successfully added to the notification list: " << activity._subject;
|
||||
addEntriesToActivityList({activity}, ActivityEntryType::ErrorType);
|
||||
addEntriesToActivityList({activity});
|
||||
_notificationErrorsLists.prepend(activity);
|
||||
}
|
||||
|
||||
|
@ -683,7 +560,7 @@ void ActivityListModel::addIgnoredFileToList(const Activity &newActivity)
|
|||
if (_listOfIgnoredFiles.size() == 0) {
|
||||
_notificationIgnoredFiles = newActivity;
|
||||
_notificationIgnoredFiles._subject = tr("Files from the ignore list as well as symbolic links are not synced.");
|
||||
addEntriesToActivityList({_notificationIgnoredFiles}, ActivityEntryType::IgnoredFileType);
|
||||
addEntriesToActivityList({_notificationIgnoredFiles});
|
||||
_listOfIgnoredFiles.append(newActivity);
|
||||
return;
|
||||
}
|
||||
|
@ -703,14 +580,14 @@ void ActivityListModel::addIgnoredFileToList(const Activity &newActivity)
|
|||
void ActivityListModel::addNotificationToActivityList(const Activity &activity)
|
||||
{
|
||||
qCInfo(lcActivity) << "Notification successfully added to the notification list: " << activity._subject;
|
||||
addEntriesToActivityList({activity}, ActivityEntryType::NotificationType);
|
||||
addEntriesToActivityList({activity});
|
||||
_notificationLists.prepend(activity);
|
||||
}
|
||||
|
||||
void ActivityListModel::addSyncFileItemToActivityList(const Activity &activity)
|
||||
{
|
||||
qCInfo(lcActivity) << "Successfully added to the activity list: " << activity._subject;
|
||||
addEntriesToActivityList({activity}, ActivityEntryType::SyncFileItemType);
|
||||
addEntriesToActivityList({activity});
|
||||
_syncFileItemLists.prepend(activity);
|
||||
}
|
||||
|
||||
|
@ -735,16 +612,11 @@ void ActivityListModel::removeActivityFromActivityList(const Activity &activity)
|
|||
endRemoveRows();
|
||||
}
|
||||
|
||||
if (activity._type == Activity::ActivityType) {
|
||||
const auto activityListIndex = _activityLists.indexOf(activity);
|
||||
if (activityListIndex != -1) {
|
||||
_activityLists.removeAt(activityListIndex);
|
||||
}
|
||||
} else if (activity._type == Activity::NotificationType) {
|
||||
const auto notificationListIndex = _notificationLists.indexOf(activity);
|
||||
if (notificationListIndex != -1)
|
||||
_notificationLists.removeAt(notificationListIndex);
|
||||
} else {
|
||||
if (activity._type != Activity::ActivityType &&
|
||||
activity._type != Activity::DummyFetchingActivityType &&
|
||||
activity._type != Activity::DummyMoreActivitiesAvailableType &&
|
||||
activity._type != Activity::NotificationType) {
|
||||
|
||||
const auto notificationErrorsListIndex = _notificationErrorsLists.indexOf(activity);
|
||||
if (notificationErrorsListIndex != -1)
|
||||
_notificationErrorsLists.removeAt(notificationErrorsListIndex);
|
||||
|
@ -762,7 +634,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 +664,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 +683,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;
|
||||
|
|
|
@ -73,21 +73,11 @@ public:
|
|||
TalkNotificationMessageIdRole,
|
||||
TalkNotificationMessageSentRole,
|
||||
TalkNotificationUserAvatarRole,
|
||||
ActivityIndexRole,
|
||||
ActivityRole,
|
||||
};
|
||||
Q_ENUM(DataRole)
|
||||
|
||||
enum class ActivityEntryType {
|
||||
DummyFetchingActivityType,
|
||||
ActivityType,
|
||||
NotificationType,
|
||||
ErrorType,
|
||||
IgnoredFileType,
|
||||
SyncFileItemType,
|
||||
MoreActivitiesAvailableType,
|
||||
};
|
||||
Q_ENUM(ActivityEntryType);
|
||||
|
||||
explicit ActivityListModel(QObject *parent = nullptr);
|
||||
|
||||
explicit ActivityListModel(AccountState *accountState,
|
||||
|
@ -153,14 +143,14 @@ protected slots:
|
|||
|
||||
virtual void startFetchJob();
|
||||
|
||||
private slots:
|
||||
void addEntriesToActivityList(const ActivityList &activityList);
|
||||
|
||||
private:
|
||||
static QVariantList convertLinksToMenuEntries(const Activity &activity);
|
||||
static QVariantList convertLinksToActionButtons(const Activity &activity);
|
||||
static QVariant convertLinkToActionButton(const ActivityLink &activityLink);
|
||||
|
||||
std::pair<int, int> rowRangeForEntryType(const ActivityEntryType type) const;
|
||||
void addEntriesToActivityList(const ActivityList &activityList, const ActivityEntryType type);
|
||||
void clearEntriesInActivityList(ActivityEntryType type);
|
||||
bool canFetchActivities() const;
|
||||
|
||||
void ingestActivities(const QJsonArray &activities);
|
||||
|
|
|
@ -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
src/gui/tray/sortedactivitylistmodel.cpp
Normal file
110
src/gui/tray/sortedactivitylistmodel.cpp
Normal file
|
@ -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
src/gui/tray/sortedactivitylistmodel.h
Normal file
46
src/gui/tray/sortedactivitylistmodel.h
Normal file
|
@ -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();
|
||||
};
|
||||
|
||||
}
|
|
@ -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();
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
|
|
@ -481,7 +481,7 @@ private slots:
|
|||
|
||||
testSyncResultErrorActivity._id = 2;
|
||||
testSyncResultErrorActivity._type = OCC::Activity::SyncResultType;
|
||||
testSyncResultErrorActivity._status = OCC::SyncResult::Error;
|
||||
testSyncResultErrorActivity._syncResultStatus = OCC::SyncResult::Error;
|
||||
testSyncResultErrorActivity._dateTime = QDateTime::currentDateTime();
|
||||
testSyncResultErrorActivity._subject = QStringLiteral("Sample failed sync text");
|
||||
testSyncResultErrorActivity._message = QStringLiteral("/path/to/thingy");
|
||||
|
@ -490,7 +490,7 @@ private slots:
|
|||
|
||||
testSyncFileItemActivity._id = 3;
|
||||
testSyncFileItemActivity._type = OCC::Activity::SyncFileItemType; //client activity
|
||||
testSyncFileItemActivity._status = OCC::SyncFileItem::Success;
|
||||
testSyncFileItemActivity._syncFileItemStatus = OCC::SyncFileItem::Success;
|
||||
testSyncFileItemActivity._dateTime = QDateTime::currentDateTime();
|
||||
testSyncFileItemActivity._message = QStringLiteral("Sample file successfully synced text");
|
||||
testSyncFileItemActivity._link = accountState->account()->url();
|
||||
|
@ -499,7 +499,7 @@ private slots:
|
|||
|
||||
testFileIgnoredActivity._id = 4;
|
||||
testFileIgnoredActivity._type = OCC::Activity::SyncFileItemType;
|
||||
testFileIgnoredActivity._status = OCC::SyncFileItem::FileIgnored;
|
||||
testFileIgnoredActivity._syncFileItemStatus = OCC::SyncFileItem::FileIgnored;
|
||||
testFileIgnoredActivity._dateTime = QDateTime::currentDateTime();
|
||||
testFileIgnoredActivity._subject = QStringLiteral("Sample ignored file sync text");
|
||||
testFileIgnoredActivity._link = accountState->account()->url();
|
||||
|
@ -599,50 +599,15 @@ private slots:
|
|||
model->addNotificationToActivityList(testNotificationActivity);
|
||||
QCOMPARE(model->rowCount(), 54);
|
||||
|
||||
const auto desiredOrder = QVector<OCC::ActivityListModel::ActivityEntryType>{
|
||||
OCC::ActivityListModel::ActivityEntryType::ErrorType,
|
||||
OCC::ActivityListModel::ActivityEntryType::IgnoredFileType,
|
||||
OCC::ActivityListModel::ActivityEntryType::NotificationType,
|
||||
OCC::ActivityListModel::ActivityEntryType::SyncFileItemType,
|
||||
OCC::ActivityListModel::ActivityEntryType::ActivityType};
|
||||
|
||||
// Test all rows for things in common
|
||||
for (int i = 0; i < model->rowCount(); i++) {
|
||||
const auto index = model->index(i, 0);
|
||||
|
||||
int expectedEntryType = qMin(i, desiredOrder.count() - 1);
|
||||
const auto activity = index.data(OCC::ActivityListModel::ActivityRole).value<OCC::Activity>();
|
||||
|
||||
// Make sure the model has sorted our activities in the right order
|
||||
switch(desiredOrder[expectedEntryType]) {
|
||||
case OCC::ActivityListModel::ActivityEntryType::DummyFetchingActivityType:
|
||||
break;
|
||||
case OCC::ActivityListModel::ActivityEntryType::ErrorType:
|
||||
QCOMPARE(activity._type, OCC::Activity::SyncResultType);
|
||||
QCOMPARE(activity._status, OCC::SyncResult::Error);
|
||||
break;
|
||||
case OCC::ActivityListModel::ActivityEntryType::IgnoredFileType:
|
||||
QCOMPARE(activity._type, OCC::Activity::SyncFileItemType);
|
||||
QCOMPARE(activity._status, OCC::SyncFileItem::FileIgnored);
|
||||
break;
|
||||
case OCC::ActivityListModel::ActivityEntryType::NotificationType:
|
||||
QCOMPARE(activity._type, OCC::Activity::NotificationType);
|
||||
break;
|
||||
case OCC::ActivityListModel::ActivityEntryType::SyncFileItemType:
|
||||
QCOMPARE(activity._type, OCC::Activity::SyncFileItemType);
|
||||
QCOMPARE(activity._status, OCC::SyncFileItem::Success);
|
||||
break;
|
||||
case OCC::ActivityListModel::ActivityEntryType::ActivityType:
|
||||
QCOMPARE(activity._type, OCC::Activity::ActivityType);
|
||||
case OCC::ActivityListModel::ActivityEntryType::MoreActivitiesAvailableType:
|
||||
break;
|
||||
}
|
||||
|
||||
auto text = index.data(OCC::ActivityListModel::ActionTextRole).toString();
|
||||
|
||||
QVERIFY(index.data(OCC::ActivityListModel::ActionRole).canConvert<int>());
|
||||
const auto type = index.data(OCC::ActivityListModel::ActionRole).toInt();
|
||||
QVERIFY(type >= OCC::Activity::ActivityType);
|
||||
QVERIFY(type >= OCC::Activity::DummyFetchingActivityType);
|
||||
|
||||
QVERIFY(!index.data(OCC::ActivityListModel::AccountRole).toString().isEmpty());
|
||||
QVERIFY(!index.data(OCC::ActivityListModel::ActionTextColorRole).toString().isEmpty());
|
||||
|
@ -664,6 +629,8 @@ private slots:
|
|||
QVERIFY(index.data(OCC::ActivityListModel::TalkNotificationMessageIdRole).canConvert<QString>());
|
||||
QVERIFY(index.data(OCC::ActivityListModel::TalkNotificationMessageSentRole).canConvert<QString>());
|
||||
|
||||
QVERIFY(index.data(OCC::ActivityListModel::ActivityRole).canConvert<OCC::Activity>());
|
||||
|
||||
// Unfortunately, trying to check anything relating to filepaths causes a crash
|
||||
// when the folder manager is invoked by the model to look for the relevant file
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue