]> dgit.raspbian.org Git - nextcloud-desktop.git/commitdiff
our source code files have lower case names
authorMatthieu Gallien <matthieu.gallien@nextcloud.com>
Mon, 25 Oct 2021 09:37:58 +0000 (11:37 +0200)
committerMatthieu Gallien <matthieu.gallien@nextcloud.com>
Mon, 25 Oct 2021 13:38:21 +0000 (15:38 +0200)
Signed-off-by: Matthieu Gallien <matthieu.gallien@nextcloud.com>
29 files changed:
src/gui/CMakeLists.txt
src/gui/fileactivitylistmodel.cpp
src/gui/fileactivitylistmodel.h
src/gui/systray.cpp
src/gui/systray.h
src/gui/tray/ActivityData.cpp [deleted file]
src/gui/tray/ActivityData.h [deleted file]
src/gui/tray/ActivityListModel.cpp [deleted file]
src/gui/tray/ActivityListModel.h [deleted file]
src/gui/tray/NotificationCache.cpp [deleted file]
src/gui/tray/NotificationCache.h [deleted file]
src/gui/tray/NotificationHandler.cpp [deleted file]
src/gui/tray/NotificationHandler.h [deleted file]
src/gui/tray/UserModel.cpp [deleted file]
src/gui/tray/UserModel.h [deleted file]
src/gui/tray/activitydata.cpp [new file with mode: 0644]
src/gui/tray/activitydata.h [new file with mode: 0644]
src/gui/tray/activitylistmodel.cpp [new file with mode: 0644]
src/gui/tray/activitylistmodel.h [new file with mode: 0644]
src/gui/tray/notificationcache.cpp [new file with mode: 0644]
src/gui/tray/notificationcache.h [new file with mode: 0644]
src/gui/tray/notificationhandler.cpp [new file with mode: 0644]
src/gui/tray/notificationhandler.h [new file with mode: 0644]
src/gui/tray/syncstatussummary.cpp
src/gui/tray/unifiedsearchresultimageprovider.cpp
src/gui/tray/usermodel.cpp [new file with mode: 0644]
src/gui/tray/usermodel.h [new file with mode: 0644]
src/gui/userstatusselectormodel.cpp
test/testnotificationcache.cpp

index 3c2d790eaff8cdfc338ac17fff0a203a376e3a85..d276b63d3fb2411cea4e23ec2cf424dff5b54902 100644 (file)
@@ -122,14 +122,14 @@ set(client_SRCS
     emojimodel.cpp
     fileactivitylistmodel.cpp
     tray/syncstatussummary.cpp
-    tray/ActivityData.cpp
-    tray/ActivityListModel.cpp
+    tray/activitydata.cpp
+    tray/activitylistmodel.cpp
     tray/unifiedsearchresult.cpp
     tray/unifiedsearchresultimageprovider.cpp
     tray/unifiedsearchresultslistmodel.cpp
-    tray/UserModel.cpp
-    tray/NotificationHandler.cpp
-    tray/NotificationCache.cpp
+    tray/usermodel.cpp
+    tray/notificationhandler.cpp
+    tray/notificationcache.cpp
     creds/credentialsfactory.cpp
     creds/httpcredentialsgui.cpp
     creds/oauth.cpp
index 08e91625220edab610e9fc6d176cbe0c44135aea..104ff317c29112156d407d23374f08737304cbad 100644 (file)
@@ -14,7 +14,7 @@
 
 #include "fileactivitylistmodel.h"
 #include "folderman.h"
-#include "tray/ActivityListModel.h"
+#include "tray/activitylistmodel.h"
 
 namespace OCC {
 
index 2c465dcf2a742c847b790a1ebb9e020dc2afdb56..421c5812861862290adec8adfff9e54577f57308 100644 (file)
@@ -15,7 +15,7 @@
 #pragma once
 
 #include "accountstate.h"
-#include "tray/ActivityListModel.h"
+#include "tray/activitylistmodel.h"
 
 namespace OCC {
 
index dafb0dfc1aef5f6d258270b09b626935a21354fd..decb58805d4e02725701689c1936c1f2b74d9818 100644 (file)
@@ -17,7 +17,7 @@
 #include "theme.h"
 #include "config.h"
 #include "common/utility.h"
-#include "tray/UserModel.h"
+#include "tray/usermodel.h"
 #include "tray/unifiedsearchresultimageprovider.h"
 #include "configfile.h"
 
index e6b7bdc02543f9573ca477cc2a97b9c71073ad3e..70917aaabbdc3c4dabaae1575437de7d28886191 100644 (file)
@@ -18,7 +18,7 @@
 #include <QSystemTrayIcon>
 
 #include "accountmanager.h"
-#include "tray/UserModel.h"
+#include "tray/usermodel.h"
 
 class QScreen;
 class QQmlApplicationEngine;
diff --git a/src/gui/tray/ActivityData.cpp b/src/gui/tray/ActivityData.cpp
deleted file mode 100644 (file)
index 9378a65..0000000
+++ /dev/null
@@ -1,36 +0,0 @@
-/*
- * Copyright (C) by Klaas Freitag <freitag@owncloud.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 <QtCore>
-
-#include "ActivityData.h"
-
-
-namespace OCC {
-
-bool operator<(const Activity &rhs, const Activity &lhs)
-{
-    return rhs._dateTime > lhs._dateTime;
-}
-
-bool operator==(const Activity &rhs, const Activity &lhs)
-{
-    return (rhs._type == lhs._type && rhs._id == lhs._id && rhs._accName == lhs._accName);
-}
-
-Activity::Identifier Activity::ident() const
-{
-    return Identifier(_id, _accName);
-}
-}
diff --git a/src/gui/tray/ActivityData.h b/src/gui/tray/ActivityData.h
deleted file mode 100644 (file)
index 11171bd..0000000
+++ /dev/null
@@ -1,108 +0,0 @@
-/*
- * Copyright (C) by Klaas Freitag <freitag@owncloud.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.
- */
-
-#ifndef ACTIVITYDATA_H
-#define ACTIVITYDATA_H
-
-#include <QtCore>
-#include <QIcon>
-
-namespace OCC {
-/**
- * @brief The ActivityLink class describes actions of an activity
- *
- * These are part of notifications which are mapped into activities.
- */
-
-class ActivityLink
-{
-    Q_GADGET
-
-    Q_PROPERTY(QString label MEMBER _label)
-    Q_PROPERTY(QString link MEMBER _link)
-    Q_PROPERTY(QByteArray verb MEMBER _verb)
-    Q_PROPERTY(bool primary MEMBER _primary)
-
-public:
-    QString _label;
-    QString _link;
-    QByteArray _verb;
-    bool _primary;
-};
-
-/* ==================================================================== */
-/**
- * @brief Activity Structure
- * @ingroup gui
- *
- * contains all the information describing a single activity.
- */
-
-class Activity
-{
-public:
-    using Identifier = QPair<qlonglong, QString>;
-
-    enum Type {
-        ActivityType,
-        NotificationType,
-        SyncResultType,
-        SyncFileItemType
-    };
-
-    Type _type;
-    qlonglong _id;
-    QString _fileAction;
-    QString _objectType;
-    QString _subject;
-    QString _message;
-    QString _folder;
-    QString _file;
-    QUrl _link;
-    QDateTime _dateTime;
-    qint64 _expireAtMsecs = -1;
-    QString _accName;
-    QString _icon;
-
-    // Stores information about the error
-    int _status;
-
-    QVector<ActivityLink> _links;
-    /**
-     * @brief Sort operator to sort the list youngest first.
-     * @param val
-     * @return
-     */
-
-
-    Identifier ident() const;
-};
-
-bool operator==(const Activity &rhs, const Activity &lhs);
-bool operator<(const Activity &rhs, const Activity &lhs);
-
-/* ==================================================================== */
-/**
- * @brief The ActivityList
- * @ingroup gui
- *
- * A QList based list of Activities
- */
-using ActivityList = QList<Activity>;
-}
-
-Q_DECLARE_METATYPE(OCC::Activity::Type)
-Q_DECLARE_METATYPE(OCC::ActivityLink)
-
-#endif // ACTIVITYDATA_H
diff --git a/src/gui/tray/ActivityListModel.cpp b/src/gui/tray/ActivityListModel.cpp
deleted file mode 100644 (file)
index 3d69956..0000000
+++ /dev/null
@@ -1,637 +0,0 @@
-/*
- * Copyright (C) by Klaas Freitag <freitag@owncloud.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 <QtCore>
-#include <QAbstractListModel>
-#include <QDesktopServices>
-#include <QWidget>
-#include <QJsonObject>
-#include <QJsonDocument>
-#include <qloggingcategory.h>
-
-#include "account.h"
-#include "accountstate.h"
-#include "accountmanager.h"
-#include "conflictdialog.h"
-#include "folderman.h"
-#include "iconjob.h"
-#include "accessmanager.h"
-#include "owncloudgui.h"
-#include "guiutility.h"
-#include "invalidfilenamedialog.h"
-
-#include "ActivityData.h"
-#include "ActivityListModel.h"
-
-#include "theme.h"
-
-namespace OCC {
-
-Q_LOGGING_CATEGORY(lcActivity, "nextcloud.gui.activity", QtInfoMsg)
-
-ActivityListModel::ActivityListModel(QObject *parent)
-    : QAbstractListModel(parent)
-{
-}
-
-ActivityListModel::ActivityListModel(AccountState *accountState,
-    QObject *parent)
-    : QAbstractListModel(parent)
-    , _accountState(accountState)
-{
-}
-
-QHash<int, QByteArray> ActivityListModel::roleNames() const
-{
-    QHash<int, QByteArray> roles;
-    roles[DisplayPathRole] = "displayPath";
-    roles[PathRole] = "path";
-    roles[AbsolutePathRole] = "absolutePath";
-    roles[LinkRole] = "link";
-    roles[MessageRole] = "message";
-    roles[ActionRole] = "type";
-    roles[ActionIconRole] = "icon";
-    roles[ActionTextRole] = "subject";
-    roles[ActionsLinksRole] = "links";
-    roles[ActionTextColorRole] = "activityTextTitleColor";
-    roles[ObjectTypeRole] = "objectType";
-    roles[PointInTimeRole] = "dateTime";
-    roles[DisplayActions] = "displayActions";
-    return roles;
-}
-
-void ActivityListModel::setAccountState(AccountState *state)
-{
-    _accountState = state;
-}
-
-void ActivityListModel::setCurrentlyFetching(bool value)
-{
-    _currentlyFetching = value;
-}
-
-bool ActivityListModel::currentlyFetching() const
-{
-    return _currentlyFetching;
-}
-
-void ActivityListModel::setDoneFetching(bool value)
-{
-    _doneFetching = value;
-}
-
-void ActivityListModel::setHideOldActivities(bool value)
-{
-    _hideOldActivities = value;
-}
-
-void ActivityListModel::setDisplayActions(bool value)
-{
-    _displayActions = value;
-}
-
-QVariant ActivityListModel::data(const QModelIndex &index, int role) const
-{
-    Activity a;
-
-    if (!index.isValid())
-        return QVariant();
-
-    a = _finalList.at(index.row());
-    AccountStatePtr ast = AccountManager::instance()->account(a._accName);
-    if (!ast && _accountState != ast.data())
-        return QVariant();
-
-    switch (role) {
-    case DisplayPathRole:
-        if (!a._file.isEmpty()) {
-            auto folder = FolderMan::instance()->folder(a._folder);
-            QString relPath(a._file);
-            if (folder) {
-                relPath.prepend(folder->remotePath());
-            }
-            const auto localFiles = FolderMan::instance()->findFileInLocalFolders(relPath, ast->account());
-            if (localFiles.count() > 0) {
-                if (relPath.startsWith('/') || relPath.startsWith('\\')) {
-                    return relPath.remove(0, 1);
-                } else {
-                    return relPath;
-                }
-            }
-        }
-        return QString();
-    case PathRole:
-        if (!a._file.isEmpty()) {
-            const auto folder = FolderMan::instance()->folder(a._folder);
-
-            QString relPath(a._file);
-            if (folder) {
-                relPath.prepend(folder->remotePath());
-            }
-
-            // get relative path to the file so we can open it in the file manager
-            const auto localFiles = FolderMan::instance()->findFileInLocalFolders(QFileInfo(relPath).path(), ast->account());
-
-            if (localFiles.isEmpty()) {
-                return QString();
-            }
-
-            // If this is an E2EE file or folder, pretend we got no path, this leads to
-            // hiding the share button which is what we want
-            if (folder) {
-                SyncJournalFileRecord rec;
-                folder->journalDb()->getFileRecord(a._file.mid(1), &rec);
-                if (rec.isValid() && (rec._isE2eEncrypted || !rec._e2eMangledName.isEmpty())) {
-                    return QString();
-                }
-            }
-
-            return QUrl::fromLocalFile(localFiles.constFirst());
-        }
-        return QString();
-    case AbsolutePathRole: {
-        const auto folder = FolderMan::instance()->folder(a._folder);
-        QString relPath(a._file);
-        if (!a._file.isEmpty()) {
-            if (folder) {
-                relPath.prepend(folder->remotePath());
-            }
-            const auto localFiles = FolderMan::instance()->findFileInLocalFolders(relPath, ast->account());
-            if (!localFiles.empty()) {
-                return localFiles.constFirst();
-            } else {
-                qWarning("File not local folders while processing absolute path request.");
-                return QString();
-            }
-        } else {
-            qWarning("Received an absolute path request for an activity without a file path.");
-            return QString();
-        }
-    }
-    case ActionsLinksRole: {
-        QList<QVariant> customList;
-        foreach (ActivityLink activityLink, a._links) {
-            customList << QVariant::fromValue(activityLink);
-        }
-        return customList;
-    }
-    case ActionIconRole: {
-        if (a._type == Activity::NotificationType) {
-            return "qrc:///client/theme/black/bell.svg";
-        } else if (a._type == Activity::SyncResultType) {
-            return "qrc:///client/theme/black/state-error.svg";
-        } else if (a._type == Activity::SyncFileItemType) {
-            if (a._status == SyncFileItem::NormalError
-                || a._status == SyncFileItem::FatalError
-                || a._status == SyncFileItem::DetailError
-                || a._status == SyncFileItem::BlacklistedError) {
-                return "qrc:///client/theme/black/state-error.svg";
-            } else if (a._status == SyncFileItem::SoftError
-                || a._status == SyncFileItem::Conflict
-                || a._status == SyncFileItem::Restoration
-                || a._status == SyncFileItem::FileLocked
-                || a._status == SyncFileItem::FileNameInvalid) {
-                return "qrc:///client/theme/black/state-warning.svg";
-            } else if (a._status == SyncFileItem::FileIgnored) {
-                return "qrc:///client/theme/black/state-info.svg";
-            } else {
-                // File sync successful
-                if (a._fileAction == "file_created") {
-                    return "qrc:///client/theme/colored/add.svg";
-                } else if (a._fileAction == "file_deleted") {
-                    return "qrc:///client/theme/colored/delete.svg";
-                } else {
-                    return "qrc:///client/theme/change.svg";
-                }
-            }
-        } else {
-            // We have an activity
-            if (a._icon.isEmpty()) {
-                return "qrc:///client/theme/black/activity.svg";
-            }
-
-            return a._icon;
-        }
-    }
-    case ObjectTypeRole:
-        return a._objectType;
-    case ActionRole: {
-        switch (a._type) {
-        case Activity::ActivityType:
-            return "Activity";
-        case Activity::NotificationType:
-            return "Notification";
-        case Activity::SyncFileItemType:
-            return "File";
-        case Activity::SyncResultType:
-            return "Sync";
-        default:
-            return QVariant();
-        }
-    }
-    case ActionTextRole:
-        return a._subject;
-    case ActionTextColorRole:
-        return a._id == -1 ? QLatin1String("#808080") : QLatin1String("#222");   // FIXME: This is a temporary workaround for _showMoreActivitiesAvailableEntry
-    case MessageRole:
-        return a._message;
-    case LinkRole: {
-        if (a._link.isEmpty()) {
-            return "";
-        } else {
-            return a._link;
-        }
-    }
-    case AccountRole:
-        return a._accName;
-    case PointInTimeRole:
-        //return a._id == -1 ? "" : QString("%1 - %2").arg(Utility::timeAgoInWords(a._dateTime.toLocalTime()), a._dateTime.toLocalTime().toString(Qt::DefaultLocaleShortDate));
-        return a._id == -1 ? "" : Utility::timeAgoInWords(a._dateTime.toLocalTime());
-    case AccountConnectedRole:
-        return (ast && ast->isConnected());
-    case DisplayActions:
-        return _displayActions;
-    default:
-        return QVariant();
-    }
-    return QVariant();
-}
-
-int ActivityListModel::rowCount(const QModelIndex &) const
-{
-    return _finalList.count();
-}
-
-bool ActivityListModel::canFetchMore(const QModelIndex &) const
-{
-    // We need to be connected to be able to fetch more
-    if (_accountState && _accountState->isConnected()) {
-        // If the fetching is reported to be done or we are currently fetching we can't fetch more
-        if (!_doneFetching && !_currentlyFetching) {
-            return true;
-        }
-    }
-
-    return false;
-}
-
-void ActivityListModel::startFetchJob()
-{
-    if (!_accountState->isConnected()) {
-        return;
-    }
-    auto *job = new JsonApiJob(_accountState->account(), QLatin1String("ocs/v2.php/apps/activity/api/v2/activity"), this);
-    QObject::connect(job, &JsonApiJob::jsonReceived,
-        this, &ActivityListModel::activitiesReceived);
-
-    QUrlQuery params;
-    params.addQueryItem(QLatin1String("since"), QString::number(_currentItem));
-    params.addQueryItem(QLatin1String("limit"), QString::number(50));
-    job->addQueryParams(params);
-
-    _currentlyFetching = true;
-    qCInfo(lcActivity) << "Start fetching activities for " << _accountState->account()->displayName();
-    job->start();
-}
-
-void ActivityListModel::activitiesReceived(const QJsonDocument &json, int statusCode)
-{
-    auto activities = json.object().value("ocs").toObject().value("data").toArray();
-
-    ActivityList list;
-    auto ast = _accountState;
-    if (!ast) {
-        return;
-    }
-
-    if (activities.size() == 0) {
-        _doneFetching = true;
-    }
-
-    _currentlyFetching = false;
-
-    QDateTime oldestDate = QDateTime::currentDateTime();
-    oldestDate = oldestDate.addDays(_maxActivitiesDays * -1);
-
-    foreach (auto activ, activities) {
-        auto json = activ.toObject();
-
-        Activity a;
-        a._type = Activity::ActivityType;
-        a._objectType = json.value("object_type").toString();
-        a._accName = ast->account()->displayName();
-        a._id = json.value("activity_id").toInt();
-        a._fileAction = json.value("type").toString();
-        a._subject = json.value("subject").toString();
-        a._message = json.value("message").toString();
-        a._file = json.value("object_name").toString();
-        a._link = QUrl(json.value("link").toString());
-        a._dateTime = QDateTime::fromString(json.value("datetime").toString(), Qt::ISODate);
-        a._icon = json.value("icon").toString();
-
-        list.append(a);
-        _currentItem = list.last()._id;
-
-        _totalActivitiesFetched++;
-        if (_totalActivitiesFetched == _maxActivities
-            || (_hideOldActivities && a._dateTime < oldestDate)) {
-            _showMoreActivitiesAvailableEntry = true;
-            _doneFetching = true;
-            break;
-        }
-    }
-
-    _activityLists.append(list);
-
-    emit activityJobStatusCode(statusCode);
-
-    combineActivityLists();
-}
-
-void ActivityListModel::addErrorToActivityList(Activity activity)
-{
-    qCInfo(lcActivity) << "Error successfully added to the notification list: " << activity._subject;
-    _notificationErrorsLists.prepend(activity);
-    combineActivityLists();
-}
-
-void ActivityListModel::addIgnoredFileToList(Activity newActivity)
-{
-    qCInfo(lcActivity) << "First checking for duplicates then add file to the notification list of ignored files: " << newActivity._file;
-
-    bool duplicate = false;
-    if (_listOfIgnoredFiles.size() == 0) {
-        _notificationIgnoredFiles = newActivity;
-        _notificationIgnoredFiles._subject = tr("Files from the ignore list as well as symbolic links are not synced.");
-        _listOfIgnoredFiles.append(newActivity);
-        return;
-    }
-
-    foreach (Activity activity, _listOfIgnoredFiles) {
-        if (activity._file == newActivity._file) {
-            duplicate = true;
-            break;
-        }
-    }
-
-    if (!duplicate) {
-        _notificationIgnoredFiles._message.append(", " + newActivity._file);
-    }
-}
-
-void ActivityListModel::addNotificationToActivityList(Activity activity)
-{
-    qCInfo(lcActivity) << "Notification successfully added to the notification list: " << activity._subject;
-    _notificationLists.prepend(activity);
-    combineActivityLists();
-}
-
-void ActivityListModel::clearNotifications()
-{
-    qCInfo(lcActivity) << "Clear the notifications";
-    _notificationLists.clear();
-    combineActivityLists();
-}
-
-void ActivityListModel::removeActivityFromActivityList(int row)
-{
-    Activity activity = _finalList.at(row);
-    removeActivityFromActivityList(activity);
-    combineActivityLists();
-}
-
-void ActivityListModel::addSyncFileItemToActivityList(Activity activity)
-{
-    qCInfo(lcActivity) << "Successfully added to the activity list: " << activity._subject;
-    _syncFileItemLists.prepend(activity);
-    combineActivityLists();
-}
-
-void ActivityListModel::removeActivityFromActivityList(Activity activity)
-{
-    qCInfo(lcActivity) << "Activity/Notification/Error successfully dismissed: " << activity._subject;
-    qCInfo(lcActivity) << "Trying to remove Activity/Notification/Error from view... ";
-
-    int index = -1;
-    if (activity._type == Activity::ActivityType) {
-        index = _activityLists.indexOf(activity);
-        if (index != -1)
-            _activityLists.removeAt(index);
-    } else if (activity._type == Activity::NotificationType) {
-        index = _notificationLists.indexOf(activity);
-        if (index != -1)
-            _notificationLists.removeAt(index);
-    } else {
-        index = _notificationErrorsLists.indexOf(activity);
-        if (index != -1)
-            _notificationErrorsLists.removeAt(index);
-    }
-
-    if (index != -1) {
-        qCInfo(lcActivity) << "Activity/Notification/Error successfully removed from the list.";
-        qCInfo(lcActivity) << "Updating Activity/Notification/Error view.";
-        combineActivityLists();
-    }
-}
-
-void ActivityListModel::triggerDefaultAction(int activityIndex)
-{
-    if (activityIndex < 0 || activityIndex >= _finalList.size()) {
-        qCWarning(lcActivity) << "Couldn't trigger default action at index" << activityIndex << "/ final list size:" << _finalList.size();
-        return;
-    }
-
-    const auto modelIndex = index(activityIndex);
-    const auto path = data(modelIndex, PathRole).toUrl();
-
-    const auto activity = _finalList.at(activityIndex);
-    if (activity._status == SyncFileItem::Conflict) {
-        Q_ASSERT(!activity._file.isEmpty());
-        Q_ASSERT(!activity._folder.isEmpty());
-        Q_ASSERT(Utility::isConflictFile(activity._file));
-
-        const auto folder = FolderMan::instance()->folder(activity._folder);
-
-        const auto conflictedRelativePath = activity._file;
-        const auto baseRelativePath = folder->journalDb()->conflictFileBaseName(conflictedRelativePath.toUtf8());
-
-        const auto dir = QDir(folder->path());
-        const auto conflictedPath = dir.filePath(conflictedRelativePath);
-        const auto basePath = dir.filePath(baseRelativePath);
-
-        const auto baseName = QFileInfo(basePath).fileName();
-
-        if (!_currentConflictDialog.isNull()) {
-            _currentConflictDialog->close();
-        }
-        _currentConflictDialog = new ConflictDialog;
-        _currentConflictDialog->setBaseFilename(baseName);
-        _currentConflictDialog->setLocalVersionFilename(conflictedPath);
-        _currentConflictDialog->setRemoteVersionFilename(basePath);
-        _currentConflictDialog->setAttribute(Qt::WA_DeleteOnClose);
-        connect(_currentConflictDialog, &ConflictDialog::accepted, folder, [folder]() {
-            folder->scheduleThisFolderSoon();
-        });
-        _currentConflictDialog->open();
-        ownCloudGui::raiseDialog(_currentConflictDialog);
-        return;
-    } else if (activity._status == SyncFileItem::FileNameInvalid) {
-        if (!_currentInvalidFilenameDialog.isNull()) {
-            _currentInvalidFilenameDialog->close();
-        }
-
-        auto folder = FolderMan::instance()->folder(activity._folder);
-        const auto folderDir = QDir(folder->path());
-        _currentInvalidFilenameDialog = new InvalidFilenameDialog(_accountState->account(), folder,
-            folderDir.filePath(activity._file));
-        connect(_currentInvalidFilenameDialog, &InvalidFilenameDialog::accepted, folder, [folder]() {
-            folder->scheduleThisFolderSoon();
-        });
-        _currentInvalidFilenameDialog->open();
-        ownCloudGui::raiseDialog(_currentInvalidFilenameDialog);
-        return;
-    }
-
-    if (path.isValid()) {
-        QDesktopServices::openUrl(path);
-    } else {
-        const auto link = data(modelIndex, LinkRole).toUrl();
-        Utility::openBrowser(link);
-    }
-}
-
-void ActivityListModel::triggerAction(int activityIndex, int actionIndex)
-{
-    if (activityIndex < 0 || activityIndex >= _finalList.size()) {
-        qCWarning(lcActivity) << "Couldn't trigger action on activity at index" << activityIndex << "/ final list size:" << _finalList.size();
-        return;
-    }
-
-    const auto activity = _finalList[activityIndex];
-
-    if (actionIndex < 0 || actionIndex >= activity._links.size()) {
-        qCWarning(lcActivity) << "Couldn't trigger action at index" << actionIndex << "/ actions list size:" << activity._links.size();
-        return;
-    }
-
-    const auto action = activity._links[actionIndex];
-
-    if (action._verb == "WEB") {
-        Utility::openBrowser(QUrl(action._link));
-        return;
-    }
-
-    emit sendNotificationRequest(activity._accName, action._link, action._verb, activityIndex);
-}
-
-AccountState *ActivityListModel::accountState() const
-{
-    return _accountState;
-}
-
-void ActivityListModel::combineActivityLists()
-{
-    ActivityList resultList;
-
-    if (_notificationErrorsLists.count() > 0) {
-        std::sort(_notificationErrorsLists.begin(), _notificationErrorsLists.end());
-        resultList.append(_notificationErrorsLists);
-    }
-    if (_listOfIgnoredFiles.size() > 0)
-        resultList.append(_notificationIgnoredFiles);
-
-    if (_notificationLists.count() > 0) {
-        std::sort(_notificationLists.begin(), _notificationLists.end());
-        resultList.append(_notificationLists);
-    }
-
-    if (_syncFileItemLists.count() > 0) {
-        std::sort(_syncFileItemLists.begin(), _syncFileItemLists.end());
-        resultList.append(_syncFileItemLists);
-    }
-
-    if (_activityLists.count() > 0) {
-        std::sort(_activityLists.begin(), _activityLists.end());
-        resultList.append(_activityLists);
-
-        if(_showMoreActivitiesAvailableEntry) {
-            Activity a;
-            a._type = Activity::ActivityType;
-            a._accName = _accountState->account()->displayName();
-            a._id = -1;
-            a._subject = tr("For more activities please open the Activity app.");
-            a._dateTime = QDateTime::currentDateTime();
-
-            AccountApp *app = _accountState->findApp(QLatin1String("activity"));
-            if(app) {
-                a._link = app->url();
-            }
-
-            resultList.append(a);
-        }
-    }
-
-    beginResetModel();
-    _finalList.clear();
-    endResetModel();
-
-    if (resultList.count() > 0) {
-        beginInsertRows(QModelIndex(), 0, resultList.count() - 1);
-        _finalList = resultList;
-        endInsertRows();
-    }
-}
-
-bool ActivityListModel::canFetchActivities() const
-{
-    return _accountState->isConnected() && _accountState->account()->capabilities().hasActivities();
-}
-
-void ActivityListModel::fetchMore(const QModelIndex &)
-{
-    if (canFetchActivities()) {
-        startFetchJob();
-    } else {
-        _doneFetching = true;
-        combineActivityLists();
-    }
-}
-
-void ActivityListModel::slotRefreshActivity()
-{
-    _activityLists.clear();
-    _doneFetching = false;
-    _currentItem = 0;
-    _totalActivitiesFetched = 0;
-    _showMoreActivitiesAvailableEntry = false;
-
-    if (canFetchActivities()) {
-        startFetchJob();
-    } else {
-        _doneFetching = true;
-        combineActivityLists();
-    }
-}
-
-void ActivityListModel::slotRemoveAccount()
-{
-    _finalList.clear();
-    _activityLists.clear();
-    _currentlyFetching = false;
-    _doneFetching = false;
-    _currentItem = 0;
-    _totalActivitiesFetched = 0;
-    _showMoreActivitiesAvailableEntry = false;
-}
-}
diff --git a/src/gui/tray/ActivityListModel.h b/src/gui/tray/ActivityListModel.h
deleted file mode 100644 (file)
index 6573161..0000000
+++ /dev/null
@@ -1,143 +0,0 @@
-/*
- * Copyright (C) by Klaas Freitag <freitag@owncloud.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.
- */
-
-#ifndef ACTIVITYLISTMODEL_H
-#define ACTIVITYLISTMODEL_H
-
-#include <QtCore>
-
-#include "ActivityData.h"
-
-class QJsonDocument;
-
-namespace OCC {
-
-Q_DECLARE_LOGGING_CATEGORY(lcActivity)
-
-class AccountState;
-class ConflictDialog;
-class InvalidFilenameDialog;
-
-/**
- * @brief The ActivityListModel
- * @ingroup gui
- *
- * Simple list model to provide the list view with data.
- */
-
-class ActivityListModel : public QAbstractListModel
-{
-    Q_OBJECT
-
-    Q_PROPERTY(AccountState *accountState READ accountState CONSTANT)
-public:
-    enum DataRole {
-        ActionIconRole = Qt::UserRole + 1,
-        UserIconRole,
-        AccountRole,
-        ObjectTypeRole,
-        ActionsLinksRole,
-        ActionTextRole,
-        ActionTextColorRole,
-        ActionRole,
-        MessageRole,
-        DisplayPathRole,
-        PathRole,
-        AbsolutePathRole,
-        LinkRole,
-        PointInTimeRole,
-        AccountConnectedRole,
-        SyncFileStatusRole,
-        DisplayActions,
-    };
-    Q_ENUM(DataRole)
-
-    explicit ActivityListModel(QObject *parent = nullptr);
-
-    explicit ActivityListModel(AccountState *accountState,
-        QObject *parent = nullptr);
-
-    QVariant data(const QModelIndex &index, int role) const override;
-    int rowCount(const QModelIndex &parent = QModelIndex()) const override;
-
-    bool canFetchMore(const QModelIndex &) const override;
-    void fetchMore(const QModelIndex &) override;
-
-    ActivityList activityList() { return _finalList; }
-    ActivityList errorsList() { return _notificationErrorsLists; }
-    void addNotificationToActivityList(Activity activity);
-    void clearNotifications();
-    void addErrorToActivityList(Activity activity);
-    void addIgnoredFileToList(Activity newActivity);
-    void addSyncFileItemToActivityList(Activity activity);
-    void removeActivityFromActivityList(int row);
-    void removeActivityFromActivityList(Activity activity);
-
-    Q_INVOKABLE void triggerDefaultAction(int activityIndex);
-    Q_INVOKABLE void triggerAction(int activityIndex, int actionIndex);
-
-    AccountState *accountState() const;
-
-public slots:
-    void slotRefreshActivity();
-    void slotRemoveAccount();
-
-signals:
-    void activityJobStatusCode(int statusCode);
-    void sendNotificationRequest(const QString &accountName, const QString &link, const QByteArray &verb, int row);
-
-protected:
-    void activitiesReceived(const QJsonDocument &json, int statusCode);
-    QHash<int, QByteArray> roleNames() const override;
-
-    void setAccountState(AccountState *state);
-    void setCurrentlyFetching(bool value);
-    bool currentlyFetching() const;
-    void setDoneFetching(bool value);
-    void setHideOldActivities(bool value);
-    void setDisplayActions(bool value);
-
-    virtual void startFetchJob();
-
-private:
-    void combineActivityLists();
-    bool canFetchActivities() const;
-
-    ActivityList _activityLists;
-    ActivityList _syncFileItemLists;
-    ActivityList _notificationLists;
-    ActivityList _listOfIgnoredFiles;
-    Activity _notificationIgnoredFiles;
-    ActivityList _notificationErrorsLists;
-    ActivityList _finalList;
-    int _currentItem = 0;
-
-    bool _displayActions = true;
-
-    int _totalActivitiesFetched = 0;
-    int _maxActivities = 100;
-    int _maxActivitiesDays = 30;
-    bool _showMoreActivitiesAvailableEntry = false;
-
-    QPointer<ConflictDialog> _currentConflictDialog;
-    QPointer<InvalidFilenameDialog> _currentInvalidFilenameDialog;
-
-    AccountState *_accountState = nullptr;
-    bool _currentlyFetching = false;
-    bool _doneFetching = false;
-    bool _hideOldActivities = true;
-};
-}
-
-#endif // ACTIVITYLISTMODEL_H
diff --git a/src/gui/tray/NotificationCache.cpp b/src/gui/tray/NotificationCache.cpp
deleted file mode 100644 (file)
index e2d4f77..0000000
+++ /dev/null
@@ -1,24 +0,0 @@
-#include "NotificationCache.h"
-
-namespace OCC {
-
-bool NotificationCache::contains(const Notification &notification) const
-{
-    return _notifications.find(calculateKey(notification)) != _notifications.end();
-}
-
-void NotificationCache::insert(const Notification &notification)
-{
-    _notifications.insert(calculateKey(notification));
-}
-
-void NotificationCache::clear()
-{
-    _notifications.clear();
-}
-
-uint NotificationCache::calculateKey(const Notification &notification) const
-{
-    return qHash(notification.title + notification.message);
-}
-}
diff --git a/src/gui/tray/NotificationCache.h b/src/gui/tray/NotificationCache.h
deleted file mode 100644 (file)
index 65caff6..0000000
+++ /dev/null
@@ -1,28 +0,0 @@
-#pragma once
-
-#include <QSet>
-
-namespace OCC {
-
-class NotificationCache
-{
-public:
-    struct Notification
-    {
-        QString title;
-        QString message;
-    };
-
-    bool contains(const Notification &notification) const;
-
-    void insert(const Notification &notification);
-
-    void clear();
-
-private:
-    uint calculateKey(const Notification &notification) const;
-
-
-    QSet<uint> _notifications;
-};
-}
diff --git a/src/gui/tray/NotificationHandler.cpp b/src/gui/tray/NotificationHandler.cpp
deleted file mode 100644 (file)
index d97feed..0000000
+++ /dev/null
@@ -1,161 +0,0 @@
-#include "NotificationHandler.h"
-
-#include "accountstate.h"
-#include "capabilities.h"
-#include "networkjobs.h"
-
-#include "iconjob.h"
-
-#include <QJsonDocument>
-#include <QJsonObject>
-
-namespace OCC {
-
-Q_LOGGING_CATEGORY(lcServerNotification, "nextcloud.gui.servernotification", QtInfoMsg)
-
-const QString notificationsPath = QLatin1String("ocs/v2.php/apps/notifications/api/v2/notifications");
-const char propertyAccountStateC[] = "oc_account_state";
-const int successStatusCode = 200;
-const int notModifiedStatusCode = 304;
-QMap<int, QByteArray> ServerNotificationHandler::iconCache;
-
-ServerNotificationHandler::ServerNotificationHandler(AccountState *accountState, QObject *parent)
-    : QObject(parent)
-    , _accountState(accountState)
-{
-}
-
-void ServerNotificationHandler::slotFetchNotifications()
-{
-    // check connectivity and credentials
-    if (!(_accountState && _accountState->isConnected() && _accountState->account() && _accountState->account()->credentials() && _accountState->account()->credentials()->ready())) {
-        deleteLater();
-        return;
-    }
-    // check if the account has notifications enabled. If the capabilities are
-    // not yet valid, its assumed that notifications are available.
-    if (_accountState->account()->capabilities().isValid()) {
-        if (!_accountState->account()->capabilities().notificationsAvailable()) {
-            qCInfo(lcServerNotification) << "Account" << _accountState->account()->displayName() << "does not have notifications enabled.";
-            deleteLater();
-            return;
-        }
-    }
-
-    // if the previous notification job has finished, start next.
-    _notificationJob = new JsonApiJob(_accountState->account(), notificationsPath, this);
-    QObject::connect(_notificationJob.data(), &JsonApiJob::jsonReceived,
-        this, &ServerNotificationHandler::slotNotificationsReceived);
-    QObject::connect(_notificationJob.data(), &JsonApiJob::etagResponseHeaderReceived,
-        this, &ServerNotificationHandler::slotEtagResponseHeaderReceived);
-    QObject::connect(_notificationJob.data(), &JsonApiJob::allowDesktopNotificationsChanged,
-            this, &ServerNotificationHandler::slotAllowDesktopNotificationsChanged);
-    _notificationJob->setProperty(propertyAccountStateC, QVariant::fromValue<AccountState *>(_accountState));
-    _notificationJob->addRawHeader("If-None-Match", _accountState->notificationsEtagResponseHeader());
-    _notificationJob->start();
-}
-
-void ServerNotificationHandler::slotEtagResponseHeaderReceived(const QByteArray &value, int statusCode)
-{
-    if (statusCode == successStatusCode) {
-        qCWarning(lcServerNotification) << "New Notification ETag Response Header received " << value;
-        auto *account = qvariant_cast<AccountState *>(sender()->property(propertyAccountStateC));
-        account->setNotificationsEtagResponseHeader(value);
-    }
-}
-
-void ServerNotificationHandler::slotAllowDesktopNotificationsChanged(bool isAllowed)
-{
-    auto *account = qvariant_cast<AccountState *>(sender()->property(propertyAccountStateC));
-    if (account != nullptr) {
-       account->setDesktopNotificationsAllowed(isAllowed);
-    }
-}
-
-void ServerNotificationHandler::slotIconDownloaded(QByteArray iconData)
-{
-    iconCache.insert(sender()->property("activityId").toInt(),iconData);
-}
-
-void ServerNotificationHandler::slotNotificationsReceived(const QJsonDocument &json, int statusCode)
-{
-    if (statusCode != successStatusCode && statusCode != notModifiedStatusCode) {
-        qCWarning(lcServerNotification) << "Notifications failed with status code " << statusCode;
-        deleteLater();
-        return;
-    }
-
-    if (statusCode == notModifiedStatusCode) {
-        qCWarning(lcServerNotification) << "Status code " << statusCode << " Not Modified - No new notifications.";
-        deleteLater();
-        return;
-    }
-
-    auto notifies = json.object().value("ocs").toObject().value("data").toArray();
-
-    auto *ai = qvariant_cast<AccountState *>(sender()->property(propertyAccountStateC));
-
-    ActivityList list;
-
-    foreach (auto element, notifies) {
-        Activity a;
-        auto json = element.toObject();
-        a._type = Activity::NotificationType;
-        a._accName = ai->account()->displayName();
-        a._id = json.value("notification_id").toInt();
-
-        //need to know, specially for remote_share
-        a._objectType = json.value("object_type").toString();
-        a._status = 0;
-
-        a._subject = json.value("subject").toString();
-        a._message = json.value("message").toString();
-        a._icon = json.value("icon").toString();
-
-        if (!a._icon.isEmpty()) {
-            auto *iconJob = new IconJob(QUrl(a._icon));
-            iconJob->setProperty("activityId", a._id);
-            connect(iconJob, &IconJob::jobFinished, this, &ServerNotificationHandler::slotIconDownloaded);
-        }
-
-        QUrl link(json.value("link").toString());
-        if (!link.isEmpty()) {
-            if (link.host().isEmpty()) {
-                link.setScheme(ai->account()->url().scheme());
-                link.setHost(ai->account()->url().host());
-            }
-            if (link.port() == -1) {
-                link.setPort(ai->account()->url().port());
-            }
-        }
-        a._link = link;
-        a._dateTime = QDateTime::fromString(json.value("datetime").toString(), Qt::ISODate);
-
-        auto actions = json.value("actions").toArray();
-        foreach (auto action, actions) {
-            auto actionJson = action.toObject();
-            ActivityLink al;
-            al._label = QUrl::fromPercentEncoding(actionJson.value("label").toString().toUtf8());
-            al._link = actionJson.value("link").toString();
-            al._verb = actionJson.value("type").toString().toUtf8();
-            al._primary = actionJson.value("primary").toBool();
-
-            a._links.append(al);
-        }
-
-        // Add another action to dismiss notification on server
-        // https://github.com/owncloud/notifications/blob/master/docs/ocs-endpoint-v1.md#deleting-a-notification-for-a-user
-        ActivityLink al;
-        al._label = tr("Dismiss");
-        al._link = Utility::concatUrlPath(ai->account()->url(), notificationsPath + "/" + QString::number(a._id)).toString();
-        al._verb = "DELETE";
-        al._primary = false;
-        a._links.append(al);
-
-        list.append(a);
-    }
-    emit newNotificationList(list);
-
-    deleteLater();
-}
-}
diff --git a/src/gui/tray/NotificationHandler.h b/src/gui/tray/NotificationHandler.h
deleted file mode 100644 (file)
index 5bc4692..0000000
+++ /dev/null
@@ -1,37 +0,0 @@
-#ifndef NOTIFICATIONHANDLER_H
-#define NOTIFICATIONHANDLER_H
-
-#include <QtCore>
-
-#include "UserModel.h"
-
-class QJsonDocument;
-
-namespace OCC {
-
-class ServerNotificationHandler : public QObject
-{
-    Q_OBJECT
-public:
-    explicit ServerNotificationHandler(AccountState *accountState, QObject *parent = nullptr);
-    static QMap<int, QByteArray> iconCache;
-
-signals:
-    void newNotificationList(ActivityList);
-
-public slots:
-    void slotFetchNotifications();
-
-private slots:
-    void slotNotificationsReceived(const QJsonDocument &json, int statusCode);
-    void slotEtagResponseHeaderReceived(const QByteArray &value, int statusCode);
-    void slotIconDownloaded(QByteArray iconData);
-    void slotAllowDesktopNotificationsChanged(bool isAllowed);
-
-private:
-    QPointer<JsonApiJob> _notificationJob;
-    AccountState *_accountState;
-};
-}
-
-#endif // NOTIFICATIONHANDLER_H
\ No newline at end of file
diff --git a/src/gui/tray/UserModel.cpp b/src/gui/tray/UserModel.cpp
deleted file mode 100644 (file)
index 3624dd6..0000000
+++ /dev/null
@@ -1,1157 +0,0 @@
-#include "NotificationHandler.h"
-#include "UserModel.h"
-
-#include "accountmanager.h"
-#include "owncloudgui.h"
-#include <pushnotifications.h>
-#include "userstatusselectormodel.h"
-#include "syncengine.h"
-#include "ocsjob.h"
-#include "configfile.h"
-#include "notificationconfirmjob.h"
-#include "logger.h"
-#include "guiutility.h"
-#include "syncfileitem.h"
-#include "tray/ActivityListModel.h"
-#include "tray/NotificationCache.h"
-#include "tray/unifiedsearchresultslistmodel.h"
-#include "userstatusconnector.h"
-
-#include <QDesktopServices>
-#include <QIcon>
-#include <QMessageBox>
-#include <QSvgRenderer>
-#include <QPainter>
-#include <QPushButton>
-
-// time span in milliseconds which has to be between two
-// refreshes of the notifications
-#define NOTIFICATION_REQUEST_FREE_PERIOD 15000
-
-namespace {
-constexpr qint64 expiredActivitiesCheckIntervalMsecs = 1000 * 60;
-constexpr qint64 activityDefaultExpirationTimeMsecs = 1000 * 60 * 10;
-}
-
-namespace OCC {
-
-User::User(AccountStatePtr &account, const bool &isCurrent, QObject *parent)
-    : QObject(parent)
-    , _account(account)
-    , _isCurrentUser(isCurrent)
-    , _activityModel(new ActivityListModel(_account.data(), this))
-    , _unifiedSearchResultsModel(new UnifiedSearchResultsListModel(_account.data(), this))
-    , _notificationRequestsRunning(0)
-{
-    connect(ProgressDispatcher::instance(), &ProgressDispatcher::progressInfo,
-        this, &User::slotProgressInfo);
-    connect(ProgressDispatcher::instance(), &ProgressDispatcher::itemCompleted,
-        this, &User::slotItemCompleted);
-    connect(ProgressDispatcher::instance(), &ProgressDispatcher::syncError,
-        this, &User::slotAddError);
-    connect(ProgressDispatcher::instance(), &ProgressDispatcher::addErrorToGui,
-        this, &User::slotAddErrorToGui);
-
-    connect(&_notificationCheckTimer, &QTimer::timeout,
-        this, &User::slotRefresh);
-
-    connect(&_expiredActivitiesCheckTimer, &QTimer::timeout,
-        this, &User::slotCheckExpiredActivities);
-
-    connect(_account.data(), &AccountState::stateChanged,
-            [=]() { if (isConnected()) {slotRefreshImmediately();} });
-    connect(_account.data(), &AccountState::stateChanged, this, &User::accountStateChanged);
-    connect(_account.data(), &AccountState::hasFetchedNavigationApps,
-        this, &User::slotRebuildNavigationAppList);
-    connect(_account->account().data(), &Account::accountChangedDisplayName, this, &User::nameChanged);
-
-    connect(FolderMan::instance(), &FolderMan::folderListChanged, this, &User::hasLocalFolderChanged);
-
-    connect(this, &User::guiLog, Logger::instance(), &Logger::guiLog);
-
-    connect(_account->account().data(), &Account::accountChangedAvatar, this, &User::avatarChanged);
-    connect(_account->account().data(), &Account::userStatusChanged, this, &User::statusChanged);
-    connect(_account.data(), &AccountState::desktopNotificationsAllowedChanged, this, &User::desktopNotificationsAllowedChanged);
-
-    connect(_activityModel, &ActivityListModel::sendNotificationRequest, this, &User::slotSendNotificationRequest);
-}
-
-void User::showDesktopNotification(const QString &title, const QString &message)
-{
-    ConfigFile cfg;
-    if (!cfg.optionalServerNotifications() || !isDesktopNotificationsAllowed()) {
-        return;
-    }
-
-    // after one hour, clear the gui log notification store
-    constexpr qint64 clearGuiLogInterval = 60 * 60 * 1000;
-    if (_guiLogTimer.elapsed() > clearGuiLogInterval) {
-        _notificationCache.clear();
-    }
-
-    const NotificationCache::Notification notification { title, message };
-    if (_notificationCache.contains(notification)) {
-        return;
-    }
-
-    _notificationCache.insert(notification);
-    emit guiLog(notification.title, notification.message);
-    // restart the gui log timer now that we show a new notification
-    _guiLogTimer.start();
-}
-
-void User::slotBuildNotificationDisplay(const ActivityList &list)
-{
-    _activityModel->clearNotifications();
-
-    foreach (auto activity, list) {
-        if (_blacklistedNotifications.contains(activity)) {
-            qCInfo(lcActivity) << "Activity in blacklist, skip";
-            continue;
-        }
-        const auto message = AccountManager::instance()->accounts().count() == 1 ? "" : activity._accName;
-        showDesktopNotification(activity._subject, message);
-        _activityModel->addNotificationToActivityList(activity);
-    }
-}
-
-void User::setNotificationRefreshInterval(std::chrono::milliseconds interval)
-{
-    if (!checkPushNotificationsAreReady()) {
-        qCDebug(lcActivity) << "Starting Notification refresh timer with " << interval.count() / 1000 << " sec interval";
-        _notificationCheckTimer.start(interval.count());
-    }
-}
-
-void User::slotPushNotificationsReady()
-{
-    qCInfo(lcActivity) << "Push notifications are ready";
-
-    if (_notificationCheckTimer.isActive()) {
-        // as we are now able to use push notifications - let's stop the polling timer
-        _notificationCheckTimer.stop();
-    }
-
-    connectPushNotifications();
-}
-
-void User::slotDisconnectPushNotifications()
-{
-    disconnect(_account->account()->pushNotifications(), &PushNotifications::notificationsChanged, this, &User::slotReceivedPushNotification);
-    disconnect(_account->account()->pushNotifications(), &PushNotifications::activitiesChanged, this, &User::slotReceivedPushActivity);
-
-    disconnect(_account->account().data(), &Account::pushNotificationsDisabled, this, &User::slotDisconnectPushNotifications);
-
-    // connection to WebSocket may have dropped or an error occured, so we need to bring back the polling until we have re-established the connection
-    setNotificationRefreshInterval(ConfigFile().notificationRefreshInterval());
-}
-
-void User::slotReceivedPushNotification(Account *account)
-{
-    if (account->id() == _account->account()->id()) {
-        slotRefreshNotifications();
-    }
-}
-
-void User::slotReceivedPushActivity(Account *account)
-{
-    if (account->id() == _account->account()->id()) {
-        slotRefreshActivities();
-    }
-}
-
-void User::slotCheckExpiredActivities()
-{
-    for (const Activity &activity : _activityModel->errorsList()) {
-        if (activity._expireAtMsecs > 0 && QDateTime::currentDateTime().toMSecsSinceEpoch() >= activity._expireAtMsecs) {
-            _activityModel->removeActivityFromActivityList(activity);
-        }
-    }
-
-    if (_activityModel->errorsList().size() == 0) {
-        _expiredActivitiesCheckTimer.stop();
-    }
-}
-
-void User::connectPushNotifications() const
-{
-    connect(_account->account().data(), &Account::pushNotificationsDisabled, this, &User::slotDisconnectPushNotifications, Qt::UniqueConnection);
-
-    connect(_account->account()->pushNotifications(), &PushNotifications::notificationsChanged, this, &User::slotReceivedPushNotification, Qt::UniqueConnection);
-    connect(_account->account()->pushNotifications(), &PushNotifications::activitiesChanged, this, &User::slotReceivedPushActivity, Qt::UniqueConnection);
-}
-
-bool User::checkPushNotificationsAreReady() const
-{
-    const auto pushNotifications = _account->account()->pushNotifications();
-
-    const auto pushActivitiesAvailable = _account->account()->capabilities().availablePushNotifications() & PushNotificationType::Activities;
-    const auto pushNotificationsAvailable = _account->account()->capabilities().availablePushNotifications() & PushNotificationType::Notifications;
-
-    const auto pushActivitiesAndNotificationsAvailable = pushActivitiesAvailable && pushNotificationsAvailable;
-
-    if (pushActivitiesAndNotificationsAvailable && pushNotifications && pushNotifications->isReady()) {
-        connectPushNotifications();
-        return true;
-    } else {
-        connect(_account->account().data(), &Account::pushNotificationsReady, this, &User::slotPushNotificationsReady, Qt::UniqueConnection);
-        return false;
-    }
-}
-
-void User::slotRefreshImmediately() {
-    if (_account.data() && _account.data()->isConnected()) {
-        slotRefreshActivities();
-    }
-    slotRefreshNotifications();
-}
-
-void User::slotRefresh()
-{
-    slotRefreshUserStatus();
-    
-    if (checkPushNotificationsAreReady()) {
-        // we are relying on WebSocket push notifications - ignore refresh attempts from UI
-        _timeSinceLastCheck[_account.data()].invalidate();
-        return;
-    }
-
-    // QElapsedTimer isn't actually constructed as invalid.
-    if (!_timeSinceLastCheck.contains(_account.data())) {
-        _timeSinceLastCheck[_account.data()].invalidate();
-    }
-    QElapsedTimer &timer = _timeSinceLastCheck[_account.data()];
-
-    // Fetch Activities only if visible and if last check is longer than 15 secs ago
-    if (timer.isValid() && timer.elapsed() < NOTIFICATION_REQUEST_FREE_PERIOD) {
-        qCDebug(lcActivity) << "Do not check as last check is only secs ago: " << timer.elapsed() / 1000;
-        return;
-    }
-    if (_account.data() && _account.data()->isConnected()) {
-        if (!timer.isValid()) {
-            slotRefreshActivities();
-        }
-        slotRefreshNotifications();
-        timer.start();
-    }
-}
-
-void User::slotRefreshActivities()
-{
-    _activityModel->slotRefreshActivity();
-}
-
-void User::slotRefreshUserStatus()
-{
-    if (_account.data() && _account.data()->isConnected()) {
-        _account->account()->userStatusConnector()->fetchUserStatus();
-    }
-}
-
-void User::slotRefreshNotifications()
-{
-    // start a server notification handler if no notification requests
-    // are running
-    if (_notificationRequestsRunning == 0) {
-        auto *snh = new ServerNotificationHandler(_account.data());
-        connect(snh, &ServerNotificationHandler::newNotificationList,
-            this, &User::slotBuildNotificationDisplay);
-
-        snh->slotFetchNotifications();
-    } else {
-        qCWarning(lcActivity) << "Notification request counter not zero.";
-    }
-}
-
-void User::slotRebuildNavigationAppList()
-{
-    emit serverHasTalkChanged();
-    // Rebuild App list
-    UserAppsModel::instance()->buildAppList();
-}
-
-void User::slotNotificationRequestFinished(int statusCode)
-{
-    int row = sender()->property("activityRow").toInt();
-
-    // the ocs API returns stat code 100 or 200 inside the xml if it succeeded.
-    if (statusCode != OCS_SUCCESS_STATUS_CODE && statusCode != OCS_SUCCESS_STATUS_CODE_V2) {
-        qCWarning(lcActivity) << "Notification Request to Server failed, leave notification visible.";
-    } else {
-        // to do use the model to rebuild the list or remove the item
-        qCWarning(lcActivity) << "Notification Request to Server successed, rebuilding list.";
-        _activityModel->removeActivityFromActivityList(row);
-    }
-}
-
-void User::slotEndNotificationRequest(int replyCode)
-{
-    _notificationRequestsRunning--;
-    slotNotificationRequestFinished(replyCode);
-}
-
-void User::slotSendNotificationRequest(const QString &accountName, const QString &link, const QByteArray &verb, int row)
-{
-    qCInfo(lcActivity) << "Server Notification Request " << verb << link << "on account" << accountName;
-
-    const QStringList validVerbs = QStringList() << "GET"
-                                                 << "PUT"
-                                                 << "POST"
-                                                 << "DELETE";
-
-    if (validVerbs.contains(verb)) {
-        AccountStatePtr acc = AccountManager::instance()->account(accountName);
-        if (acc) {
-            auto *job = new NotificationConfirmJob(acc->account());
-            QUrl l(link);
-            job->setLinkAndVerb(l, verb);
-            job->setProperty("activityRow", QVariant::fromValue(row));
-            connect(job, &AbstractNetworkJob::networkError,
-                this, &User::slotNotifyNetworkError);
-            connect(job, &NotificationConfirmJob::jobFinished,
-                this, &User::slotNotifyServerFinished);
-            job->start();
-
-            // count the number of running notification requests. If this member var
-            // is larger than zero, no new fetching of notifications is started
-            _notificationRequestsRunning++;
-        }
-    } else {
-        qCWarning(lcActivity) << "Notification Links: Invalid verb:" << verb;
-    }
-}
-
-void User::slotNotifyNetworkError(QNetworkReply *reply)
-{
-    auto *job = qobject_cast<NotificationConfirmJob *>(sender());
-    if (!job) {
-        return;
-    }
-
-    int resultCode = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt();
-
-    slotEndNotificationRequest(resultCode);
-    qCWarning(lcActivity) << "Server notify job failed with code " << resultCode;
-}
-
-void User::slotNotifyServerFinished(const QString &reply, int replyCode)
-{
-    auto *job = qobject_cast<NotificationConfirmJob *>(sender());
-    if (!job) {
-        return;
-    }
-
-    slotEndNotificationRequest(replyCode);
-    qCInfo(lcActivity) << "Server Notification reply code" << replyCode << reply;
-}
-
-void User::slotProgressInfo(const QString &folder, const ProgressInfo &progress)
-{
-    if (progress.status() == ProgressInfo::Reconcile) {
-        // Wipe all non-persistent entries - as well as the persistent ones
-        // in cases where a local discovery was done.
-        auto f = FolderMan::instance()->folder(folder);
-        if (!f)
-            return;
-        const auto &engine = f->syncEngine();
-        const auto style = engine.lastLocalDiscoveryStyle();
-        foreach (Activity activity, _activityModel->errorsList()) {
-            if (activity._expireAtMsecs != -1) {
-                // we process expired activities in a different slot
-                continue;
-            }
-            if (activity._folder != folder) {
-                continue;
-            }
-
-            if (style == LocalDiscoveryStyle::FilesystemOnly) {
-                _activityModel->removeActivityFromActivityList(activity);
-                continue;
-            }
-
-            if (activity._status == SyncFileItem::Conflict && !QFileInfo(f->path() + activity._file).exists()) {
-                _activityModel->removeActivityFromActivityList(activity);
-                continue;
-            }
-
-            if (activity._status == SyncFileItem::FileLocked && !QFileInfo(f->path() + activity._file).exists()) {
-                _activityModel->removeActivityFromActivityList(activity);
-                continue;
-            }
-
-
-            if (activity._status == SyncFileItem::FileIgnored && !QFileInfo(f->path() + activity._file).exists()) {
-                _activityModel->removeActivityFromActivityList(activity);
-                continue;
-            }
-
-
-            if (!QFileInfo(f->path() + activity._file).exists()) {
-                _activityModel->removeActivityFromActivityList(activity);
-                continue;
-            }
-
-            auto path = QFileInfo(activity._file).dir().path().toUtf8();
-            if (path == ".")
-                path.clear();
-
-            if (engine.shouldDiscoverLocally(path))
-                _activityModel->removeActivityFromActivityList(activity);
-        }
-    }
-
-    if (progress.status() == ProgressInfo::Done) {
-        // We keep track very well of pending conflicts.
-        // Inform other components about them.
-        QStringList conflicts;
-        foreach (Activity activity, _activityModel->errorsList()) {
-            if (activity._folder == folder
-                && activity._status == SyncFileItem::Conflict) {
-                conflicts.append(activity._file);
-            }
-        }
-
-        emit ProgressDispatcher::instance()->folderConflicts(folder, conflicts);
-    }
-}
-
-void User::slotAddError(const QString &folderAlias, const QString &message, ErrorCategory category)
-{
-    auto folderInstance = FolderMan::instance()->folder(folderAlias);
-    if (!folderInstance)
-        return;
-
-    if (folderInstance->accountState() == _account.data()) {
-        qCWarning(lcActivity) << "Item " << folderInstance->shortGuiLocalPath() << " retrieved resulted in " << message;
-
-        Activity activity;
-        activity._type = Activity::SyncResultType;
-        activity._status = SyncResult::Error;
-        activity._dateTime = QDateTime::fromString(QDateTime::currentDateTime().toString(), Qt::ISODate);
-        activity._subject = message;
-        activity._message = folderInstance->shortGuiLocalPath();
-        activity._link = folderInstance->shortGuiLocalPath();
-        activity._accName = folderInstance->accountState()->account()->displayName();
-        activity._folder = folderAlias;
-
-
-        if (category == ErrorCategory::InsufficientRemoteStorage) {
-            ActivityLink link;
-            link._label = tr("Retry all uploads");
-            link._link = folderInstance->path();
-            link._verb = "";
-            link._primary = true;
-            activity._links.append(link);
-        }
-
-        // add 'other errors' to activity list
-        _activityModel->addErrorToActivityList(activity);
-    }
-}
-
-void User::slotAddErrorToGui(const QString &folderAlias, SyncFileItem::Status status, const QString &errorMessage, const QString &subject)
-{
-    const auto folderInstance = FolderMan::instance()->folder(folderAlias);
-    if (!folderInstance) {
-        return;
-    }
-
-    if (folderInstance->accountState() == _account.data()) {
-        qCWarning(lcActivity) << "Item " << folderInstance->shortGuiLocalPath() << " retrieved resulted in " << errorMessage;
-
-        Activity activity;
-        activity._type = Activity::SyncFileItemType;
-        activity._status = status;
-        const auto currentDateTime = QDateTime::currentDateTime();
-        activity._dateTime = QDateTime::fromString(currentDateTime.toString(), Qt::ISODate);
-        activity._expireAtMsecs = currentDateTime.addMSecs(activityDefaultExpirationTimeMsecs).toMSecsSinceEpoch();
-        activity._subject = !subject.isEmpty() ? subject : folderInstance->shortGuiLocalPath();
-        activity._message = errorMessage;
-        activity._link = folderInstance->shortGuiLocalPath();
-        activity._accName = folderInstance->accountState()->account()->displayName();
-        activity._folder = folderAlias;
-
-        // add 'other errors' to activity list
-        _activityModel->addErrorToActivityList(activity);
-
-        showDesktopNotification(activity._subject, activity._message);
-
-        if (!_expiredActivitiesCheckTimer.isActive()) {
-            _expiredActivitiesCheckTimer.start(expiredActivitiesCheckIntervalMsecs);
-        }
-    }
-}
-
-bool User::isActivityOfCurrentAccount(const Folder *folder) const
-{
-    return folder->accountState() == _account.data();
-}
-
-bool User::isUnsolvableConflict(const SyncFileItemPtr &item) const
-{
-    // We just care about conflict issues that we are able to resolve
-    return item->_status == SyncFileItem::Conflict && !Utility::isConflictFile(item->_file);
-}
-
-void User::processCompletedSyncItem(const Folder *folder, const SyncFileItemPtr &item)
-{
-    Activity activity;
-    activity._type = Activity::SyncFileItemType; //client activity
-    activity._status = item->_status;
-    activity._dateTime = QDateTime::currentDateTime();
-    activity._message = item->_originalFile;
-    activity._link = folder->accountState()->account()->url();
-    activity._accName = folder->accountState()->account()->displayName();
-    activity._file = item->_file;
-    activity._folder = folder->alias();
-    activity._fileAction = "";
-
-    if (item->_instruction == CSYNC_INSTRUCTION_REMOVE) {
-        activity._fileAction = "file_deleted";
-    } else if (item->_instruction == CSYNC_INSTRUCTION_NEW) {
-        activity._fileAction = "file_created";
-    } else if (item->_instruction == CSYNC_INSTRUCTION_RENAME) {
-        activity._fileAction = "file_renamed";
-    } else {
-        activity._fileAction = "file_changed";
-    }
-
-    if (item->_status == SyncFileItem::NoStatus || item->_status == SyncFileItem::Success) {
-        qCWarning(lcActivity) << "Item " << item->_file << " retrieved successfully.";
-
-        if (item->_direction != SyncFileItem::Up) {
-            activity._message = tr("Synced %1").arg(item->_originalFile);
-        } else if (activity._fileAction == "file_renamed") {
-            activity._message = tr("You renamed %1").arg(item->_originalFile);
-        } else if (activity._fileAction == "file_deleted") {
-            activity._message = tr("You deleted %1").arg(item->_originalFile);
-        } else if (activity._fileAction == "file_created") {
-            activity._message = tr("You created %1").arg(item->_originalFile);
-        } else {
-            activity._message = tr("You changed %1").arg(item->_originalFile);
-        }
-
-        _activityModel->addSyncFileItemToActivityList(activity);
-    } else {
-        qCWarning(lcActivity) << "Item " << item->_file << " retrieved resulted in error " << item->_errorString;
-        activity._subject = item->_errorString;
-
-        if (item->_status == SyncFileItem::Status::FileIgnored) {
-            _activityModel->addIgnoredFileToList(activity);
-        } else {
-            // add 'protocol error' to activity list
-            if (item->_status == SyncFileItem::Status::FileNameInvalid) {
-                showDesktopNotification(item->_file, activity._subject);
-            }
-            _activityModel->addErrorToActivityList(activity);
-        }
-    }
-}
-
-void User::slotItemCompleted(const QString &folder, const SyncFileItemPtr &item)
-{
-    auto folderInstance = FolderMan::instance()->folder(folder);
-
-    if (!folderInstance || !isActivityOfCurrentAccount(folderInstance) || isUnsolvableConflict(item)) {
-        return;
-    }
-
-    qCWarning(lcActivity) << "Item " << item->_file << " retrieved resulted in " << item->_errorString;
-    processCompletedSyncItem(folderInstance, item);
-}
-
-AccountPtr User::account() const
-{
-    return _account->account();
-}
-
-AccountStatePtr User::accountState() const
-{
-    return _account;
-}
-
-void User::setCurrentUser(const bool &isCurrent)
-{
-    _isCurrentUser = isCurrent;
-}
-
-Folder *User::getFolder() const
-{
-    foreach (Folder *folder, FolderMan::instance()->map()) {
-        if (folder->accountState() == _account.data()) {
-            return folder;
-        }
-    }
-
-    return nullptr;
-}
-
-ActivityListModel *User::getActivityModel()
-{
-    return _activityModel;
-}
-
-UnifiedSearchResultsListModel *User::getUnifiedSearchResultsListModel() const
-{
-    return _unifiedSearchResultsModel;
-}
-
-void User::openLocalFolder()
-{
-    const auto folder = getFolder();
-
-    if (folder) {
-        QDesktopServices::openUrl(QUrl::fromLocalFile(folder->path()));
-    }
-}
-
-void User::login() const
-{
-    _account->account()->resetRejectedCertificates();
-    _account->signIn();
-}
-
-void User::logout() const
-{
-    _account->signOutByUi();
-}
-
-QString User::name() const
-{
-    // If davDisplayName is empty (can be several reasons, simplest is missing login at startup), fall back to username
-    QString name = _account->account()->davDisplayName();
-    if (name == "") {
-        name = _account->account()->credentials()->user();
-    }
-    return name;
-}
-
-QString User::server(bool shortened) const
-{
-    QString serverUrl = _account->account()->url().toString();
-    if (shortened) {
-        serverUrl.replace(QLatin1String("https://"), QLatin1String(""));
-        serverUrl.replace(QLatin1String("http://"), QLatin1String(""));
-    }
-    return serverUrl;
-}
-
-UserStatus::OnlineStatus User::status() const
-{
-    return _account->account()->userStatusConnector()->userStatus().state();
-}
-
-QString User::statusMessage() const
-{
-    return _account->account()->userStatusConnector()->userStatus().message();
-}
-
-QUrl User::statusIcon() const
-{
-    return _account->account()->userStatusConnector()->userStatus().stateIcon();
-}
-
-QString User::statusEmoji() const
-{
-    return _account->account()->userStatusConnector()->userStatus().icon();
-}
-
-bool User::serverHasUserStatus() const
-{
-    return _account->account()->capabilities().userStatusNotification();
-}
-
-QImage User::avatar() const
-{
-    return AvatarJob::makeCircularAvatar(_account->account()->avatar());
-}
-
-QString User::avatarUrl() const
-{
-    if (avatar().isNull()) {
-        return QString();
-    }
-
-    return QStringLiteral("image://avatars/") + _account->account()->id();
-}
-
-bool User::hasLocalFolder() const
-{
-    return getFolder() != nullptr;
-}
-
-bool User::serverHasTalk() const
-{
-    return talkApp() != nullptr;
-}
-
-AccountApp *User::talkApp() const
-{
-    return _account->findApp(QStringLiteral("spreed"));
-}
-
-bool User::hasActivities() const
-{
-    return _account->account()->capabilities().hasActivities();
-}
-
-AccountAppList User::appList() const
-{
-    return _account->appList();
-}
-
-bool User::isCurrentUser() const
-{
-    return _isCurrentUser;
-}
-
-bool User::isConnected() const
-{
-    return (_account->connectionStatus() == AccountState::ConnectionStatus::Connected);
-}
-
-
-bool User::isDesktopNotificationsAllowed() const
-{
-    return _account.data()->isDesktopNotificationsAllowed();
-}
-
-void User::removeAccount() const
-{
-    AccountManager::instance()->deleteAccount(_account.data());
-    AccountManager::instance()->save();
-}
-
-/*-------------------------------------------------------------------------------------*/
-
-UserModel *UserModel::_instance = nullptr;
-
-UserModel *UserModel::instance()
-{
-    if (!_instance) {
-        _instance = new UserModel();
-    }
-    return _instance;
-}
-
-UserModel::UserModel(QObject *parent)
-    : QAbstractListModel(parent)
-{
-    // TODO: Remember selected user from last quit via settings file
-    if (AccountManager::instance()->accounts().size() > 0) {
-        buildUserList();
-    }
-
-    connect(AccountManager::instance(), &AccountManager::accountAdded,
-        this, &UserModel::buildUserList);
-}
-
-void UserModel::buildUserList()
-{
-    for (int i = 0; i < AccountManager::instance()->accounts().size(); i++) {
-        auto user = AccountManager::instance()->accounts().at(i);
-        addUser(user);
-    }
-    if (_init) {
-        _users.first()->setCurrentUser(true);
-        _init = false;
-    }
-}
-
-Q_INVOKABLE int UserModel::numUsers()
-{
-    return _users.size();
-}
-
-Q_INVOKABLE int UserModel::currentUserId() const
-{
-    return _currentUserId;
-}
-
-Q_INVOKABLE bool UserModel::isUserConnected(const int &id)
-{
-    if (id < 0 || id >= _users.size())
-        return false;
-
-    return _users[id]->isConnected();
-}
-
-QImage UserModel::avatarById(const int &id)
-{
-    if (id < 0 || id >= _users.size())
-        return {};
-
-    return _users[id]->avatar();
-}
-
-Q_INVOKABLE QString UserModel::currentUserServer()
-{
-    if (_currentUserId < 0 || _currentUserId >= _users.size())
-        return {};
-
-    return _users[_currentUserId]->server();
-}
-
-void UserModel::addUser(AccountStatePtr &user, const bool &isCurrent)
-{
-    bool containsUser = false;
-    for (const auto &u : qAsConst(_users)) {
-        if (u->account() == user->account()) {
-            containsUser = true;
-            continue;
-        }
-    }
-
-    if (!containsUser) {
-        int row = rowCount();
-        beginInsertRows(QModelIndex(), row, row);
-
-        User *u = new User(user, isCurrent);
-
-        connect(u, &User::avatarChanged, this, [this, row] {
-           emit dataChanged(index(row, 0), index(row, 0), {UserModel::AvatarRole});
-        });
-
-        connect(u, &User::statusChanged, this, [this, row] {
-            emit dataChanged(index(row, 0), index(row, 0), {UserModel::StatusIconRole, 
-                                                           UserModel::StatusEmojiRole,     
-                                                            UserModel::StatusMessageRole});
-        });
-        
-        connect(u, &User::desktopNotificationsAllowedChanged, this, [this, row] {
-            emit dataChanged(index(row, 0), index(row, 0), { UserModel::DesktopNotificationsAllowedRole });
-        });
-        
-        connect(u, &User::accountStateChanged, this, [this, row] {
-            emit dataChanged(index(row, 0), index(row, 0), { UserModel::IsConnectedRole });
-        });
-
-        _users << u;
-        if (isCurrent) {
-            _currentUserId = _users.indexOf(_users.last());
-        }
-
-        endInsertRows();
-        ConfigFile cfg;
-        _users.last()->setNotificationRefreshInterval(cfg.notificationRefreshInterval());
-        emit newUserSelected();
-    }
-}
-
-int UserModel::currentUserIndex()
-{
-    return _currentUserId;
-}
-
-Q_INVOKABLE void UserModel::openCurrentAccountLocalFolder()
-{
-    if (_currentUserId < 0 || _currentUserId >= _users.size())
-        return;
-
-    _users[_currentUserId]->openLocalFolder();
-}
-
-Q_INVOKABLE void UserModel::openCurrentAccountTalk()
-{
-    if (!currentUser())
-        return;
-
-    const auto talkApp = currentUser()->talkApp();
-    if (talkApp) {
-        Utility::openBrowser(talkApp->url());
-    } else {
-        qCWarning(lcActivity) << "The Talk app is not enabled on" << currentUser()->server();
-    }
-}
-
-Q_INVOKABLE void UserModel::openCurrentAccountServer()
-{
-    if (_currentUserId < 0 || _currentUserId >= _users.size())
-        return;
-
-    QString url = _users[_currentUserId]->server(false);
-    if (!url.startsWith("http://") && !url.startsWith("https://")) {
-        url = "https://" + _users[_currentUserId]->server(false);
-    }
-
-    QDesktopServices::openUrl(url);
-}
-
-Q_INVOKABLE void UserModel::switchCurrentUser(const int &id)
-{
-    if (_currentUserId < 0 || _currentUserId >= _users.size())
-        return;
-    
-    _users[_currentUserId]->setCurrentUser(false);
-    _users[id]->setCurrentUser(true);
-    _currentUserId = id;
-    emit newUserSelected();
-}
-
-Q_INVOKABLE void UserModel::login(const int &id)
-{
-    if (id < 0 || id >= _users.size())
-        return;
-
-    _users[id]->login();
-}
-
-Q_INVOKABLE void UserModel::logout(const int &id)
-{
-    if (id < 0 || id >= _users.size())
-        return;
-
-    _users[id]->logout();
-}
-
-Q_INVOKABLE void UserModel::removeAccount(const int &id)
-{
-    if (id < 0 || id >= _users.size())
-        return;
-
-    QMessageBox messageBox(QMessageBox::Question,
-        tr("Confirm Account Removal"),
-        tr("<p>Do you really want to remove the connection to the account <i>%1</i>?</p>"
-           "<p><b>Note:</b> This will <b>not</b> delete any files.</p>")
-            .arg(_users[id]->name()),
-        QMessageBox::NoButton);
-    QPushButton *yesButton =
-        messageBox.addButton(tr("Remove connection"), QMessageBox::YesRole);
-    messageBox.addButton(tr("Cancel"), QMessageBox::NoRole);
-
-    messageBox.exec();
-    if (messageBox.clickedButton() != yesButton) {
-        return;
-    }
-
-    if (_users[id]->isCurrentUser() && _users.count() > 1) {
-        id == 0 ? switchCurrentUser(1) : switchCurrentUser(0);
-    }
-
-    _users[id]->logout();
-    _users[id]->removeAccount();
-
-    beginRemoveRows(QModelIndex(), id, id);
-    _users.removeAt(id);
-    endRemoveRows();
-}
-
-std::shared_ptr<OCC::UserStatusConnector> UserModel::userStatusConnector(int id)
-{
-    if (id < 0 || id >= _users.size()) {
-        return nullptr;
-    }
-
-    return _users[id]->account()->userStatusConnector();
-}
-
-int UserModel::rowCount(const QModelIndex &parent) const
-{
-    Q_UNUSED(parent);
-    return _users.count();
-}
-
-QVariant UserModel::data(const QModelIndex &index, int role) const
-{
-    if (index.row() < 0 || index.row() >= _users.count()) {
-        return QVariant();
-    }
-
-    if (role == NameRole) {
-        return _users[index.row()]->name();
-    } else if (role == ServerRole) {
-        return _users[index.row()]->server();
-    } else if (role == ServerHasUserStatusRole) {
-        return _users[index.row()]->serverHasUserStatus();
-    } else if (role == StatusIconRole) {
-        return _users[index.row()]->statusIcon();
-    } else if (role == StatusEmojiRole) {
-        return _users[index.row()]->statusEmoji();
-    } else if (role == StatusMessageRole) {
-        return _users[index.row()]->statusMessage();
-    } else if (role == DesktopNotificationsAllowedRole) {
-        return _users[index.row()]->isDesktopNotificationsAllowed();
-    } else if (role == AvatarRole) {
-        return _users[index.row()]->avatarUrl();
-    } else if (role == IsCurrentUserRole) {
-        return _users[index.row()]->isCurrentUser();
-    } else if (role == IsConnectedRole) {
-        return _users[index.row()]->isConnected();
-    } else if (role == IdRole) {
-        return index.row();
-    }
-    return QVariant();
-}
-
-QHash<int, QByteArray> UserModel::roleNames() const
-{
-    QHash<int, QByteArray> roles;
-    roles[NameRole] = "name";
-    roles[ServerRole] = "server";
-    roles[ServerHasUserStatusRole] = "serverHasUserStatus";
-    roles[StatusIconRole] = "statusIcon";
-    roles[StatusEmojiRole] = "statusEmoji";
-    roles[StatusMessageRole] = "statusMessage";
-    roles[DesktopNotificationsAllowedRole] = "desktopNotificationsAllowed";
-    roles[AvatarRole] = "avatar";
-    roles[IsCurrentUserRole] = "isCurrentUser";
-    roles[IsConnectedRole] = "isConnected";
-    roles[IdRole] = "id";
-    return roles;
-}
-
-ActivityListModel *UserModel::currentActivityModel()
-{
-    if (currentUserIndex() < 0 || currentUserIndex() >= _users.size())
-        return nullptr;
-
-    return _users[currentUserIndex()]->getActivityModel();
-}
-
-void UserModel::fetchCurrentActivityModel()
-{
-    if (currentUserId() < 0 || currentUserId() >= _users.size())
-        return;
-
-    _users[currentUserId()]->slotRefresh();
-}
-
-AccountAppList UserModel::appList() const
-{
-    if (_currentUserId < 0 || _currentUserId >= _users.size())
-        return {};
-
-    return _users[_currentUserId]->appList();
-}
-
-User *UserModel::currentUser() const
-{
-    if (currentUserId() < 0 || currentUserId() >= _users.size())
-        return nullptr;
-
-    return _users[currentUserId()];
-}
-
-int UserModel::findUserIdForAccount(AccountState *account) const
-{
-    const auto it = std::find_if(std::cbegin(_users), std::cend(_users), [=](const User *user) {
-        return user->account()->id() == account->account()->id();
-    });
-
-    if (it == std::cend(_users)) {
-        return -1;
-    }
-
-    const auto id = std::distance(std::cbegin(_users), it);
-    return id;
-}
-
-/*-------------------------------------------------------------------------------------*/
-
-ImageProvider::ImageProvider()
-    : QQuickImageProvider(QQuickImageProvider::Image)
-{
-}
-
-QImage ImageProvider::requestImage(const QString &id, QSize *size, const QSize &requestedSize)
-{
-    Q_UNUSED(size)
-    Q_UNUSED(requestedSize)
-
-    const auto makeIcon = [](const QString &path) {
-        QImage image(128, 128, QImage::Format_ARGB32);
-        image.fill(Qt::GlobalColor::transparent);
-        QPainter painter(&image);
-        QSvgRenderer renderer(path);
-        renderer.render(&painter);
-        return image;
-    };
-
-    if (id == QLatin1String("fallbackWhite")) {
-        return makeIcon(QStringLiteral(":/client/theme/white/user.svg"));
-    }
-
-    if (id == QLatin1String("fallbackBlack")) {
-        return makeIcon(QStringLiteral(":/client/theme/black/user.svg"));
-    }
-
-    const int uid = id.toInt();
-    return UserModel::instance()->avatarById(uid);
-}
-
-/*-------------------------------------------------------------------------------------*/
-
-UserAppsModel *UserAppsModel::_instance = nullptr;
-
-UserAppsModel *UserAppsModel::instance()
-{
-    if (!_instance) {
-        _instance = new UserAppsModel();
-    }
-    return _instance;
-}
-
-UserAppsModel::UserAppsModel(QObject *parent)
-    : QAbstractListModel(parent)
-{
-}
-
-void UserAppsModel::buildAppList()
-{
-    if (rowCount() > 0) {
-        beginRemoveRows(QModelIndex(), 0, rowCount() - 1);
-        _apps.clear();
-        endRemoveRows();
-    }
-
-    if (UserModel::instance()->appList().count() > 0) {
-        const auto talkApp = UserModel::instance()->currentUser()->talkApp();
-        foreach (AccountApp *app, UserModel::instance()->appList()) {
-            // Filter out Talk because we have a dedicated button for it
-            if (talkApp && app->id() == talkApp->id())
-                continue;
-
-            beginInsertRows(QModelIndex(), rowCount(), rowCount());
-            _apps << app;
-            endInsertRows();
-        }
-    }
-}
-
-void UserAppsModel::openAppUrl(const QUrl &url)
-{
-    Utility::openBrowser(url);
-}
-
-int UserAppsModel::rowCount(const QModelIndex &parent) const
-{
-    Q_UNUSED(parent);
-    return _apps.count();
-}
-
-QVariant UserAppsModel::data(const QModelIndex &index, int role) const
-{
-    if (index.row() < 0 || index.row() >= _apps.count()) {
-        return QVariant();
-    }
-
-    if (role == NameRole) {
-        return _apps[index.row()]->name();
-    } else if (role == UrlRole) {
-        return _apps[index.row()]->url();
-    } else if (role == IconUrlRole) {
-        return _apps[index.row()]->iconUrl().toString();
-    }
-    return QVariant();
-}
-
-QHash<int, QByteArray> UserAppsModel::roleNames() const
-{
-    QHash<int, QByteArray> roles;
-    roles[NameRole] = "appName";
-    roles[UrlRole] = "appUrl";
-    roles[IconUrlRole] = "appIconUrl";
-    return roles;
-}
-
-}
diff --git a/src/gui/tray/UserModel.h b/src/gui/tray/UserModel.h
deleted file mode 100644 (file)
index 88dc78a..0000000
+++ /dev/null
@@ -1,246 +0,0 @@
-#ifndef USERMODEL_H
-#define USERMODEL_H
-
-#include <QAbstractListModel>
-#include <QImage>
-#include <QDateTime>
-#include <QStringList>
-#include <QQuickImageProvider>
-#include <QHash>
-
-#include "ActivityListModel.h"
-#include "accountfwd.h"
-#include "accountmanager.h"
-#include "folderman.h"
-#include "NotificationCache.h"
-#include "userstatusselectormodel.h"
-#include "userstatusconnector.h"
-#include <chrono>
-
-namespace OCC {
-class UnifiedSearchResultsListModel;
-
-class User : public QObject
-{
-    Q_OBJECT
-    Q_PROPERTY(QString name READ name NOTIFY nameChanged)
-    Q_PROPERTY(QString server READ server CONSTANT)
-    Q_PROPERTY(bool serverHasUserStatus READ serverHasUserStatus CONSTANT)
-    Q_PROPERTY(QUrl statusIcon READ statusIcon NOTIFY statusChanged)
-    Q_PROPERTY(QString statusEmoji READ statusEmoji NOTIFY statusChanged)
-    Q_PROPERTY(QString statusMessage READ statusMessage NOTIFY statusChanged)
-    Q_PROPERTY(bool desktopNotificationsAllowed READ isDesktopNotificationsAllowed NOTIFY desktopNotificationsAllowedChanged)
-    Q_PROPERTY(bool hasLocalFolder READ hasLocalFolder NOTIFY hasLocalFolderChanged)
-    Q_PROPERTY(bool serverHasTalk READ serverHasTalk NOTIFY serverHasTalkChanged)
-    Q_PROPERTY(QString avatar READ avatarUrl NOTIFY avatarChanged)
-    Q_PROPERTY(bool isConnected READ isConnected NOTIFY accountStateChanged)
-    Q_PROPERTY(UnifiedSearchResultsListModel* unifiedSearchResultsListModel READ getUnifiedSearchResultsListModel CONSTANT)
-public:
-    User(AccountStatePtr &account, const bool &isCurrent = false, QObject *parent = nullptr);
-
-    AccountPtr account() const;
-    AccountStatePtr accountState() const;
-
-    bool isConnected() const;
-    bool isCurrentUser() const;
-    void setCurrentUser(const bool &isCurrent);
-    Folder *getFolder() const;
-    ActivityListModel *getActivityModel();
-    UnifiedSearchResultsListModel *getUnifiedSearchResultsListModel() const;
-    void openLocalFolder();
-    QString name() const;
-    QString server(bool shortened = true) const;
-    bool hasLocalFolder() const;
-    bool serverHasTalk() const;
-    bool serverHasUserStatus() const;
-    AccountApp *talkApp() const;
-    bool hasActivities() const;
-    AccountAppList appList() const;
-    QImage avatar() const;
-    void login() const;
-    void logout() const;
-    void removeAccount() const;
-    QString avatarUrl() const;
-    bool isDesktopNotificationsAllowed() const;
-    UserStatus::OnlineStatus status() const;
-    QString statusMessage() const;
-    QUrl statusIcon() const;
-    QString statusEmoji() const;
-    void processCompletedSyncItem(const Folder *folder, const SyncFileItemPtr &item);
-
-signals:
-    void guiLog(const QString &, const QString &);
-    void nameChanged();
-    void hasLocalFolderChanged();
-    void serverHasTalkChanged();
-    void avatarChanged();
-    void accountStateChanged();
-    void statusChanged();
-    void desktopNotificationsAllowedChanged();
-
-public slots:
-    void slotItemCompleted(const QString &folder, const SyncFileItemPtr &item);
-    void slotProgressInfo(const QString &folder, const ProgressInfo &progress);
-    void slotAddError(const QString &folderAlias, const QString &message, ErrorCategory category);
-    void slotAddErrorToGui(const QString &folderAlias, SyncFileItem::Status status, const QString &errorMessage, const QString &subject = {});
-    void slotNotificationRequestFinished(int statusCode);
-    void slotNotifyNetworkError(QNetworkReply *reply);
-    void slotEndNotificationRequest(int replyCode);
-    void slotNotifyServerFinished(const QString &reply, int replyCode);
-    void slotSendNotificationRequest(const QString &accountName, const QString &link, const QByteArray &verb, int row);
-    void slotBuildNotificationDisplay(const ActivityList &list);
-    void slotRefreshNotifications();
-    void slotRefreshActivities();
-    void slotRefresh();
-    void slotRefreshUserStatus();
-    void slotRefreshImmediately();
-    void setNotificationRefreshInterval(std::chrono::milliseconds interval);
-    void slotRebuildNavigationAppList();
-
-private:
-    void slotPushNotificationsReady();
-    void slotDisconnectPushNotifications();
-    void slotReceivedPushNotification(Account *account);
-    void slotReceivedPushActivity(Account *account);
-    void slotCheckExpiredActivities();
-
-    void connectPushNotifications() const;
-    bool checkPushNotificationsAreReady() const;
-
-    bool isActivityOfCurrentAccount(const Folder *folder) const;
-    bool isUnsolvableConflict(const SyncFileItemPtr &item) const;
-
-    void showDesktopNotification(const QString &title, const QString &message);
-
-private:
-    AccountStatePtr _account;
-    bool _isCurrentUser;
-    ActivityListModel *_activityModel;
-    UnifiedSearchResultsListModel *_unifiedSearchResultsModel;
-    ActivityList _blacklistedNotifications;
-
-    QTimer _expiredActivitiesCheckTimer;
-    QTimer _notificationCheckTimer;
-    QHash<AccountState *, QElapsedTimer> _timeSinceLastCheck;
-
-    QElapsedTimer _guiLogTimer;
-    NotificationCache _notificationCache;
-
-    // number of currently running notification requests. If non zero,
-    // no query for notifications is started.
-    int _notificationRequestsRunning;
-};
-
-class UserModel : public QAbstractListModel
-{
-    Q_OBJECT
-    Q_PROPERTY(User* currentUser READ currentUser NOTIFY newUserSelected)
-    Q_PROPERTY(int currentUserId READ currentUserId NOTIFY newUserSelected)
-public:
-    static UserModel *instance();
-    ~UserModel() override = default;
-
-    void addUser(AccountStatePtr &user, const bool &isCurrent = false);
-    int currentUserIndex();
-
-    int rowCount(const QModelIndex &parent = QModelIndex()) const override;
-
-    QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override;
-
-    QImage avatarById(const int &id);
-
-    User *currentUser() const;
-
-    int findUserIdForAccount(AccountState *account) const;
-
-    Q_INVOKABLE void fetchCurrentActivityModel();
-    Q_INVOKABLE void openCurrentAccountLocalFolder();
-    Q_INVOKABLE void openCurrentAccountTalk();
-    Q_INVOKABLE void openCurrentAccountServer();
-    Q_INVOKABLE int numUsers();
-    Q_INVOKABLE QString currentUserServer();
-    int currentUserId() const;
-    Q_INVOKABLE bool isUserConnected(const int &id);
-    Q_INVOKABLE void switchCurrentUser(const int &id);
-    Q_INVOKABLE void login(const int &id);
-    Q_INVOKABLE void logout(const int &id);
-    Q_INVOKABLE void removeAccount(const int &id);
-
-    Q_INVOKABLE std::shared_ptr<OCC::UserStatusConnector> userStatusConnector(int id);
-
-    ActivityListModel *currentActivityModel();
-
-    enum UserRoles {
-        NameRole = Qt::UserRole + 1,
-        ServerRole,
-        ServerHasUserStatusRole,
-        StatusIconRole,
-        StatusEmojiRole,
-        StatusMessageRole,
-        DesktopNotificationsAllowedRole,
-        AvatarRole,
-        IsCurrentUserRole,
-        IsConnectedRole,
-        IdRole
-    };
-
-    AccountAppList appList() const;
-
-signals:
-    Q_INVOKABLE void addAccount();
-    Q_INVOKABLE void newUserSelected();
-
-protected:
-    QHash<int, QByteArray> roleNames() const override;
-
-private:
-    static UserModel *_instance;
-    UserModel(QObject *parent = nullptr);
-    QList<User*> _users;
-    int _currentUserId = 0;
-    bool _init = true;
-
-    void buildUserList();
-};
-
-class ImageProvider : public QQuickImageProvider
-{
-public:
-    ImageProvider();
-    QImage requestImage(const QString &id, QSize *size, const QSize &requestedSize) override;
-};
-
-class UserAppsModel : public QAbstractListModel
-{
-    Q_OBJECT
-public:
-    static UserAppsModel *instance();
-    ~UserAppsModel() override = default;
-
-    int rowCount(const QModelIndex &parent = QModelIndex()) const override;
-
-    QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override;
-
-    enum UserAppsRoles {
-        NameRole = Qt::UserRole + 1,
-        UrlRole,
-        IconUrlRole
-    };
-
-    void buildAppList();
-
-public slots:
-    void openAppUrl(const QUrl &url);
-
-protected:
-    QHash<int, QByteArray> roleNames() const override;
-
-private:
-    static UserAppsModel *_instance;
-    UserAppsModel(QObject *parent = nullptr);
-
-    AccountAppList _apps;
-};
-
-}
-#endif // USERMODEL_H
diff --git a/src/gui/tray/activitydata.cpp b/src/gui/tray/activitydata.cpp
new file mode 100644 (file)
index 0000000..866c979
--- /dev/null
@@ -0,0 +1,36 @@
+/*
+ * Copyright (C) by Klaas Freitag <freitag@owncloud.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 <QtCore>
+
+#include "activitydata.h"
+
+
+namespace OCC {
+
+bool operator<(const Activity &rhs, const Activity &lhs)
+{
+    return rhs._dateTime > lhs._dateTime;
+}
+
+bool operator==(const Activity &rhs, const Activity &lhs)
+{
+    return (rhs._type == lhs._type && rhs._id == lhs._id && rhs._accName == lhs._accName);
+}
+
+Activity::Identifier Activity::ident() const
+{
+    return Identifier(_id, _accName);
+}
+}
diff --git a/src/gui/tray/activitydata.h b/src/gui/tray/activitydata.h
new file mode 100644 (file)
index 0000000..11171bd
--- /dev/null
@@ -0,0 +1,108 @@
+/*
+ * Copyright (C) by Klaas Freitag <freitag@owncloud.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.
+ */
+
+#ifndef ACTIVITYDATA_H
+#define ACTIVITYDATA_H
+
+#include <QtCore>
+#include <QIcon>
+
+namespace OCC {
+/**
+ * @brief The ActivityLink class describes actions of an activity
+ *
+ * These are part of notifications which are mapped into activities.
+ */
+
+class ActivityLink
+{
+    Q_GADGET
+
+    Q_PROPERTY(QString label MEMBER _label)
+    Q_PROPERTY(QString link MEMBER _link)
+    Q_PROPERTY(QByteArray verb MEMBER _verb)
+    Q_PROPERTY(bool primary MEMBER _primary)
+
+public:
+    QString _label;
+    QString _link;
+    QByteArray _verb;
+    bool _primary;
+};
+
+/* ==================================================================== */
+/**
+ * @brief Activity Structure
+ * @ingroup gui
+ *
+ * contains all the information describing a single activity.
+ */
+
+class Activity
+{
+public:
+    using Identifier = QPair<qlonglong, QString>;
+
+    enum Type {
+        ActivityType,
+        NotificationType,
+        SyncResultType,
+        SyncFileItemType
+    };
+
+    Type _type;
+    qlonglong _id;
+    QString _fileAction;
+    QString _objectType;
+    QString _subject;
+    QString _message;
+    QString _folder;
+    QString _file;
+    QUrl _link;
+    QDateTime _dateTime;
+    qint64 _expireAtMsecs = -1;
+    QString _accName;
+    QString _icon;
+
+    // Stores information about the error
+    int _status;
+
+    QVector<ActivityLink> _links;
+    /**
+     * @brief Sort operator to sort the list youngest first.
+     * @param val
+     * @return
+     */
+
+
+    Identifier ident() const;
+};
+
+bool operator==(const Activity &rhs, const Activity &lhs);
+bool operator<(const Activity &rhs, const Activity &lhs);
+
+/* ==================================================================== */
+/**
+ * @brief The ActivityList
+ * @ingroup gui
+ *
+ * A QList based list of Activities
+ */
+using ActivityList = QList<Activity>;
+}
+
+Q_DECLARE_METATYPE(OCC::Activity::Type)
+Q_DECLARE_METATYPE(OCC::ActivityLink)
+
+#endif // ACTIVITYDATA_H
diff --git a/src/gui/tray/activitylistmodel.cpp b/src/gui/tray/activitylistmodel.cpp
new file mode 100644 (file)
index 0000000..3d12624
--- /dev/null
@@ -0,0 +1,637 @@
+/*
+ * Copyright (C) by Klaas Freitag <freitag@owncloud.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 <QtCore>
+#include <QAbstractListModel>
+#include <QDesktopServices>
+#include <QWidget>
+#include <QJsonObject>
+#include <QJsonDocument>
+#include <qloggingcategory.h>
+
+#include "account.h"
+#include "accountstate.h"
+#include "accountmanager.h"
+#include "conflictdialog.h"
+#include "folderman.h"
+#include "iconjob.h"
+#include "accessmanager.h"
+#include "owncloudgui.h"
+#include "guiutility.h"
+#include "invalidfilenamedialog.h"
+
+#include "activitydata.h"
+#include "activitylistmodel.h"
+
+#include "theme.h"
+
+namespace OCC {
+
+Q_LOGGING_CATEGORY(lcActivity, "nextcloud.gui.activity", QtInfoMsg)
+
+ActivityListModel::ActivityListModel(QObject *parent)
+    : QAbstractListModel(parent)
+{
+}
+
+ActivityListModel::ActivityListModel(AccountState *accountState,
+    QObject *parent)
+    : QAbstractListModel(parent)
+    , _accountState(accountState)
+{
+}
+
+QHash<int, QByteArray> ActivityListModel::roleNames() const
+{
+    QHash<int, QByteArray> roles;
+    roles[DisplayPathRole] = "displayPath";
+    roles[PathRole] = "path";
+    roles[AbsolutePathRole] = "absolutePath";
+    roles[LinkRole] = "link";
+    roles[MessageRole] = "message";
+    roles[ActionRole] = "type";
+    roles[ActionIconRole] = "icon";
+    roles[ActionTextRole] = "subject";
+    roles[ActionsLinksRole] = "links";
+    roles[ActionTextColorRole] = "activityTextTitleColor";
+    roles[ObjectTypeRole] = "objectType";
+    roles[PointInTimeRole] = "dateTime";
+    roles[DisplayActions] = "displayActions";
+    return roles;
+}
+
+void ActivityListModel::setAccountState(AccountState *state)
+{
+    _accountState = state;
+}
+
+void ActivityListModel::setCurrentlyFetching(bool value)
+{
+    _currentlyFetching = value;
+}
+
+bool ActivityListModel::currentlyFetching() const
+{
+    return _currentlyFetching;
+}
+
+void ActivityListModel::setDoneFetching(bool value)
+{
+    _doneFetching = value;
+}
+
+void ActivityListModel::setHideOldActivities(bool value)
+{
+    _hideOldActivities = value;
+}
+
+void ActivityListModel::setDisplayActions(bool value)
+{
+    _displayActions = value;
+}
+
+QVariant ActivityListModel::data(const QModelIndex &index, int role) const
+{
+    Activity a;
+
+    if (!index.isValid())
+        return QVariant();
+
+    a = _finalList.at(index.row());
+    AccountStatePtr ast = AccountManager::instance()->account(a._accName);
+    if (!ast && _accountState != ast.data())
+        return QVariant();
+
+    switch (role) {
+    case DisplayPathRole:
+        if (!a._file.isEmpty()) {
+            auto folder = FolderMan::instance()->folder(a._folder);
+            QString relPath(a._file);
+            if (folder) {
+                relPath.prepend(folder->remotePath());
+            }
+            const auto localFiles = FolderMan::instance()->findFileInLocalFolders(relPath, ast->account());
+            if (localFiles.count() > 0) {
+                if (relPath.startsWith('/') || relPath.startsWith('\\')) {
+                    return relPath.remove(0, 1);
+                } else {
+                    return relPath;
+                }
+            }
+        }
+        return QString();
+    case PathRole:
+        if (!a._file.isEmpty()) {
+            const auto folder = FolderMan::instance()->folder(a._folder);
+
+            QString relPath(a._file);
+            if (folder) {
+                relPath.prepend(folder->remotePath());
+            }
+
+            // get relative path to the file so we can open it in the file manager
+            const auto localFiles = FolderMan::instance()->findFileInLocalFolders(QFileInfo(relPath).path(), ast->account());
+
+            if (localFiles.isEmpty()) {
+                return QString();
+            }
+
+            // If this is an E2EE file or folder, pretend we got no path, this leads to
+            // hiding the share button which is what we want
+            if (folder) {
+                SyncJournalFileRecord rec;
+                folder->journalDb()->getFileRecord(a._file.mid(1), &rec);
+                if (rec.isValid() && (rec._isE2eEncrypted || !rec._e2eMangledName.isEmpty())) {
+                    return QString();
+                }
+            }
+
+            return QUrl::fromLocalFile(localFiles.constFirst());
+        }
+        return QString();
+    case AbsolutePathRole: {
+        const auto folder = FolderMan::instance()->folder(a._folder);
+        QString relPath(a._file);
+        if (!a._file.isEmpty()) {
+            if (folder) {
+                relPath.prepend(folder->remotePath());
+            }
+            const auto localFiles = FolderMan::instance()->findFileInLocalFolders(relPath, ast->account());
+            if (!localFiles.empty()) {
+                return localFiles.constFirst();
+            } else {
+                qWarning("File not local folders while processing absolute path request.");
+                return QString();
+            }
+        } else {
+            qWarning("Received an absolute path request for an activity without a file path.");
+            return QString();
+        }
+    }
+    case ActionsLinksRole: {
+        QList<QVariant> customList;
+        foreach (ActivityLink activityLink, a._links) {
+            customList << QVariant::fromValue(activityLink);
+        }
+        return customList;
+    }
+    case ActionIconRole: {
+        if (a._type == Activity::NotificationType) {
+            return "qrc:///client/theme/black/bell.svg";
+        } else if (a._type == Activity::SyncResultType) {
+            return "qrc:///client/theme/black/state-error.svg";
+        } else if (a._type == Activity::SyncFileItemType) {
+            if (a._status == SyncFileItem::NormalError
+                || a._status == SyncFileItem::FatalError
+                || a._status == SyncFileItem::DetailError
+                || a._status == SyncFileItem::BlacklistedError) {
+                return "qrc:///client/theme/black/state-error.svg";
+            } else if (a._status == SyncFileItem::SoftError
+                || a._status == SyncFileItem::Conflict
+                || a._status == SyncFileItem::Restoration
+                || a._status == SyncFileItem::FileLocked
+                || a._status == SyncFileItem::FileNameInvalid) {
+                return "qrc:///client/theme/black/state-warning.svg";
+            } else if (a._status == SyncFileItem::FileIgnored) {
+                return "qrc:///client/theme/black/state-info.svg";
+            } else {
+                // File sync successful
+                if (a._fileAction == "file_created") {
+                    return "qrc:///client/theme/colored/add.svg";
+                } else if (a._fileAction == "file_deleted") {
+                    return "qrc:///client/theme/colored/delete.svg";
+                } else {
+                    return "qrc:///client/theme/change.svg";
+                }
+            }
+        } else {
+            // We have an activity
+            if (a._icon.isEmpty()) {
+                return "qrc:///client/theme/black/activity.svg";
+            }
+
+            return a._icon;
+        }
+    }
+    case ObjectTypeRole:
+        return a._objectType;
+    case ActionRole: {
+        switch (a._type) {
+        case Activity::ActivityType:
+            return "Activity";
+        case Activity::NotificationType:
+            return "Notification";
+        case Activity::SyncFileItemType:
+            return "File";
+        case Activity::SyncResultType:
+            return "Sync";
+        default:
+            return QVariant();
+        }
+    }
+    case ActionTextRole:
+        return a._subject;
+    case ActionTextColorRole:
+        return a._id == -1 ? QLatin1String("#808080") : QLatin1String("#222");   // FIXME: This is a temporary workaround for _showMoreActivitiesAvailableEntry
+    case MessageRole:
+        return a._message;
+    case LinkRole: {
+        if (a._link.isEmpty()) {
+            return "";
+        } else {
+            return a._link;
+        }
+    }
+    case AccountRole:
+        return a._accName;
+    case PointInTimeRole:
+        //return a._id == -1 ? "" : QString("%1 - %2").arg(Utility::timeAgoInWords(a._dateTime.toLocalTime()), a._dateTime.toLocalTime().toString(Qt::DefaultLocaleShortDate));
+        return a._id == -1 ? "" : Utility::timeAgoInWords(a._dateTime.toLocalTime());
+    case AccountConnectedRole:
+        return (ast && ast->isConnected());
+    case DisplayActions:
+        return _displayActions;
+    default:
+        return QVariant();
+    }
+    return QVariant();
+}
+
+int ActivityListModel::rowCount(const QModelIndex &) const
+{
+    return _finalList.count();
+}
+
+bool ActivityListModel::canFetchMore(const QModelIndex &) const
+{
+    // We need to be connected to be able to fetch more
+    if (_accountState && _accountState->isConnected()) {
+        // If the fetching is reported to be done or we are currently fetching we can't fetch more
+        if (!_doneFetching && !_currentlyFetching) {
+            return true;
+        }
+    }
+
+    return false;
+}
+
+void ActivityListModel::startFetchJob()
+{
+    if (!_accountState->isConnected()) {
+        return;
+    }
+    auto *job = new JsonApiJob(_accountState->account(), QLatin1String("ocs/v2.php/apps/activity/api/v2/activity"), this);
+    QObject::connect(job, &JsonApiJob::jsonReceived,
+        this, &ActivityListModel::activitiesReceived);
+
+    QUrlQuery params;
+    params.addQueryItem(QLatin1String("since"), QString::number(_currentItem));
+    params.addQueryItem(QLatin1String("limit"), QString::number(50));
+    job->addQueryParams(params);
+
+    _currentlyFetching = true;
+    qCInfo(lcActivity) << "Start fetching activities for " << _accountState->account()->displayName();
+    job->start();
+}
+
+void ActivityListModel::activitiesReceived(const QJsonDocument &json, int statusCode)
+{
+    auto activities = json.object().value("ocs").toObject().value("data").toArray();
+
+    ActivityList list;
+    auto ast = _accountState;
+    if (!ast) {
+        return;
+    }
+
+    if (activities.size() == 0) {
+        _doneFetching = true;
+    }
+
+    _currentlyFetching = false;
+
+    QDateTime oldestDate = QDateTime::currentDateTime();
+    oldestDate = oldestDate.addDays(_maxActivitiesDays * -1);
+
+    foreach (auto activ, activities) {
+        auto json = activ.toObject();
+
+        Activity a;
+        a._type = Activity::ActivityType;
+        a._objectType = json.value("object_type").toString();
+        a._accName = ast->account()->displayName();
+        a._id = json.value("activity_id").toInt();
+        a._fileAction = json.value("type").toString();
+        a._subject = json.value("subject").toString();
+        a._message = json.value("message").toString();
+        a._file = json.value("object_name").toString();
+        a._link = QUrl(json.value("link").toString());
+        a._dateTime = QDateTime::fromString(json.value("datetime").toString(), Qt::ISODate);
+        a._icon = json.value("icon").toString();
+
+        list.append(a);
+        _currentItem = list.last()._id;
+
+        _totalActivitiesFetched++;
+        if (_totalActivitiesFetched == _maxActivities
+            || (_hideOldActivities && a._dateTime < oldestDate)) {
+            _showMoreActivitiesAvailableEntry = true;
+            _doneFetching = true;
+            break;
+        }
+    }
+
+    _activityLists.append(list);
+
+    emit activityJobStatusCode(statusCode);
+
+    combineActivityLists();
+}
+
+void ActivityListModel::addErrorToActivityList(Activity activity)
+{
+    qCInfo(lcActivity) << "Error successfully added to the notification list: " << activity._subject;
+    _notificationErrorsLists.prepend(activity);
+    combineActivityLists();
+}
+
+void ActivityListModel::addIgnoredFileToList(Activity newActivity)
+{
+    qCInfo(lcActivity) << "First checking for duplicates then add file to the notification list of ignored files: " << newActivity._file;
+
+    bool duplicate = false;
+    if (_listOfIgnoredFiles.size() == 0) {
+        _notificationIgnoredFiles = newActivity;
+        _notificationIgnoredFiles._subject = tr("Files from the ignore list as well as symbolic links are not synced.");
+        _listOfIgnoredFiles.append(newActivity);
+        return;
+    }
+
+    foreach (Activity activity, _listOfIgnoredFiles) {
+        if (activity._file == newActivity._file) {
+            duplicate = true;
+            break;
+        }
+    }
+
+    if (!duplicate) {
+        _notificationIgnoredFiles._message.append(", " + newActivity._file);
+    }
+}
+
+void ActivityListModel::addNotificationToActivityList(Activity activity)
+{
+    qCInfo(lcActivity) << "Notification successfully added to the notification list: " << activity._subject;
+    _notificationLists.prepend(activity);
+    combineActivityLists();
+}
+
+void ActivityListModel::clearNotifications()
+{
+    qCInfo(lcActivity) << "Clear the notifications";
+    _notificationLists.clear();
+    combineActivityLists();
+}
+
+void ActivityListModel::removeActivityFromActivityList(int row)
+{
+    Activity activity = _finalList.at(row);
+    removeActivityFromActivityList(activity);
+    combineActivityLists();
+}
+
+void ActivityListModel::addSyncFileItemToActivityList(Activity activity)
+{
+    qCInfo(lcActivity) << "Successfully added to the activity list: " << activity._subject;
+    _syncFileItemLists.prepend(activity);
+    combineActivityLists();
+}
+
+void ActivityListModel::removeActivityFromActivityList(Activity activity)
+{
+    qCInfo(lcActivity) << "Activity/Notification/Error successfully dismissed: " << activity._subject;
+    qCInfo(lcActivity) << "Trying to remove Activity/Notification/Error from view... ";
+
+    int index = -1;
+    if (activity._type == Activity::ActivityType) {
+        index = _activityLists.indexOf(activity);
+        if (index != -1)
+            _activityLists.removeAt(index);
+    } else if (activity._type == Activity::NotificationType) {
+        index = _notificationLists.indexOf(activity);
+        if (index != -1)
+            _notificationLists.removeAt(index);
+    } else {
+        index = _notificationErrorsLists.indexOf(activity);
+        if (index != -1)
+            _notificationErrorsLists.removeAt(index);
+    }
+
+    if (index != -1) {
+        qCInfo(lcActivity) << "Activity/Notification/Error successfully removed from the list.";
+        qCInfo(lcActivity) << "Updating Activity/Notification/Error view.";
+        combineActivityLists();
+    }
+}
+
+void ActivityListModel::triggerDefaultAction(int activityIndex)
+{
+    if (activityIndex < 0 || activityIndex >= _finalList.size()) {
+        qCWarning(lcActivity) << "Couldn't trigger default action at index" << activityIndex << "/ final list size:" << _finalList.size();
+        return;
+    }
+
+    const auto modelIndex = index(activityIndex);
+    const auto path = data(modelIndex, PathRole).toUrl();
+
+    const auto activity = _finalList.at(activityIndex);
+    if (activity._status == SyncFileItem::Conflict) {
+        Q_ASSERT(!activity._file.isEmpty());
+        Q_ASSERT(!activity._folder.isEmpty());
+        Q_ASSERT(Utility::isConflictFile(activity._file));
+
+        const auto folder = FolderMan::instance()->folder(activity._folder);
+
+        const auto conflictedRelativePath = activity._file;
+        const auto baseRelativePath = folder->journalDb()->conflictFileBaseName(conflictedRelativePath.toUtf8());
+
+        const auto dir = QDir(folder->path());
+        const auto conflictedPath = dir.filePath(conflictedRelativePath);
+        const auto basePath = dir.filePath(baseRelativePath);
+
+        const auto baseName = QFileInfo(basePath).fileName();
+
+        if (!_currentConflictDialog.isNull()) {
+            _currentConflictDialog->close();
+        }
+        _currentConflictDialog = new ConflictDialog;
+        _currentConflictDialog->setBaseFilename(baseName);
+        _currentConflictDialog->setLocalVersionFilename(conflictedPath);
+        _currentConflictDialog->setRemoteVersionFilename(basePath);
+        _currentConflictDialog->setAttribute(Qt::WA_DeleteOnClose);
+        connect(_currentConflictDialog, &ConflictDialog::accepted, folder, [folder]() {
+            folder->scheduleThisFolderSoon();
+        });
+        _currentConflictDialog->open();
+        ownCloudGui::raiseDialog(_currentConflictDialog);
+        return;
+    } else if (activity._status == SyncFileItem::FileNameInvalid) {
+        if (!_currentInvalidFilenameDialog.isNull()) {
+            _currentInvalidFilenameDialog->close();
+        }
+
+        auto folder = FolderMan::instance()->folder(activity._folder);
+        const auto folderDir = QDir(folder->path());
+        _currentInvalidFilenameDialog = new InvalidFilenameDialog(_accountState->account(), folder,
+            folderDir.filePath(activity._file));
+        connect(_currentInvalidFilenameDialog, &InvalidFilenameDialog::accepted, folder, [folder]() {
+            folder->scheduleThisFolderSoon();
+        });
+        _currentInvalidFilenameDialog->open();
+        ownCloudGui::raiseDialog(_currentInvalidFilenameDialog);
+        return;
+    }
+
+    if (path.isValid()) {
+        QDesktopServices::openUrl(path);
+    } else {
+        const auto link = data(modelIndex, LinkRole).toUrl();
+        Utility::openBrowser(link);
+    }
+}
+
+void ActivityListModel::triggerAction(int activityIndex, int actionIndex)
+{
+    if (activityIndex < 0 || activityIndex >= _finalList.size()) {
+        qCWarning(lcActivity) << "Couldn't trigger action on activity at index" << activityIndex << "/ final list size:" << _finalList.size();
+        return;
+    }
+
+    const auto activity = _finalList[activityIndex];
+
+    if (actionIndex < 0 || actionIndex >= activity._links.size()) {
+        qCWarning(lcActivity) << "Couldn't trigger action at index" << actionIndex << "/ actions list size:" << activity._links.size();
+        return;
+    }
+
+    const auto action = activity._links[actionIndex];
+
+    if (action._verb == "WEB") {
+        Utility::openBrowser(QUrl(action._link));
+        return;
+    }
+
+    emit sendNotificationRequest(activity._accName, action._link, action._verb, activityIndex);
+}
+
+AccountState *ActivityListModel::accountState() const
+{
+    return _accountState;
+}
+
+void ActivityListModel::combineActivityLists()
+{
+    ActivityList resultList;
+
+    if (_notificationErrorsLists.count() > 0) {
+        std::sort(_notificationErrorsLists.begin(), _notificationErrorsLists.end());
+        resultList.append(_notificationErrorsLists);
+    }
+    if (_listOfIgnoredFiles.size() > 0)
+        resultList.append(_notificationIgnoredFiles);
+
+    if (_notificationLists.count() > 0) {
+        std::sort(_notificationLists.begin(), _notificationLists.end());
+        resultList.append(_notificationLists);
+    }
+
+    if (_syncFileItemLists.count() > 0) {
+        std::sort(_syncFileItemLists.begin(), _syncFileItemLists.end());
+        resultList.append(_syncFileItemLists);
+    }
+
+    if (_activityLists.count() > 0) {
+        std::sort(_activityLists.begin(), _activityLists.end());
+        resultList.append(_activityLists);
+
+        if(_showMoreActivitiesAvailableEntry) {
+            Activity a;
+            a._type = Activity::ActivityType;
+            a._accName = _accountState->account()->displayName();
+            a._id = -1;
+            a._subject = tr("For more activities please open the Activity app.");
+            a._dateTime = QDateTime::currentDateTime();
+
+            AccountApp *app = _accountState->findApp(QLatin1String("activity"));
+            if(app) {
+                a._link = app->url();
+            }
+
+            resultList.append(a);
+        }
+    }
+
+    beginResetModel();
+    _finalList.clear();
+    endResetModel();
+
+    if (resultList.count() > 0) {
+        beginInsertRows(QModelIndex(), 0, resultList.count() - 1);
+        _finalList = resultList;
+        endInsertRows();
+    }
+}
+
+bool ActivityListModel::canFetchActivities() const
+{
+    return _accountState->isConnected() && _accountState->account()->capabilities().hasActivities();
+}
+
+void ActivityListModel::fetchMore(const QModelIndex &)
+{
+    if (canFetchActivities()) {
+        startFetchJob();
+    } else {
+        _doneFetching = true;
+        combineActivityLists();
+    }
+}
+
+void ActivityListModel::slotRefreshActivity()
+{
+    _activityLists.clear();
+    _doneFetching = false;
+    _currentItem = 0;
+    _totalActivitiesFetched = 0;
+    _showMoreActivitiesAvailableEntry = false;
+
+    if (canFetchActivities()) {
+        startFetchJob();
+    } else {
+        _doneFetching = true;
+        combineActivityLists();
+    }
+}
+
+void ActivityListModel::slotRemoveAccount()
+{
+    _finalList.clear();
+    _activityLists.clear();
+    _currentlyFetching = false;
+    _doneFetching = false;
+    _currentItem = 0;
+    _totalActivitiesFetched = 0;
+    _showMoreActivitiesAvailableEntry = false;
+}
+}
diff --git a/src/gui/tray/activitylistmodel.h b/src/gui/tray/activitylistmodel.h
new file mode 100644 (file)
index 0000000..379fc9b
--- /dev/null
@@ -0,0 +1,143 @@
+/*
+ * Copyright (C) by Klaas Freitag <freitag@owncloud.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.
+ */
+
+#ifndef ACTIVITYLISTMODEL_H
+#define ACTIVITYLISTMODEL_H
+
+#include <QtCore>
+
+#include "activitydata.h"
+
+class QJsonDocument;
+
+namespace OCC {
+
+Q_DECLARE_LOGGING_CATEGORY(lcActivity)
+
+class AccountState;
+class ConflictDialog;
+class InvalidFilenameDialog;
+
+/**
+ * @brief The ActivityListModel
+ * @ingroup gui
+ *
+ * Simple list model to provide the list view with data.
+ */
+
+class ActivityListModel : public QAbstractListModel
+{
+    Q_OBJECT
+
+    Q_PROPERTY(AccountState *accountState READ accountState CONSTANT)
+public:
+    enum DataRole {
+        ActionIconRole = Qt::UserRole + 1,
+        UserIconRole,
+        AccountRole,
+        ObjectTypeRole,
+        ActionsLinksRole,
+        ActionTextRole,
+        ActionTextColorRole,
+        ActionRole,
+        MessageRole,
+        DisplayPathRole,
+        PathRole,
+        AbsolutePathRole,
+        LinkRole,
+        PointInTimeRole,
+        AccountConnectedRole,
+        SyncFileStatusRole,
+        DisplayActions,
+    };
+    Q_ENUM(DataRole)
+
+    explicit ActivityListModel(QObject *parent = nullptr);
+
+    explicit ActivityListModel(AccountState *accountState,
+        QObject *parent = nullptr);
+
+    QVariant data(const QModelIndex &index, int role) const override;
+    int rowCount(const QModelIndex &parent = QModelIndex()) const override;
+
+    bool canFetchMore(const QModelIndex &) const override;
+    void fetchMore(const QModelIndex &) override;
+
+    ActivityList activityList() { return _finalList; }
+    ActivityList errorsList() { return _notificationErrorsLists; }
+    void addNotificationToActivityList(Activity activity);
+    void clearNotifications();
+    void addErrorToActivityList(Activity activity);
+    void addIgnoredFileToList(Activity newActivity);
+    void addSyncFileItemToActivityList(Activity activity);
+    void removeActivityFromActivityList(int row);
+    void removeActivityFromActivityList(Activity activity);
+
+    Q_INVOKABLE void triggerDefaultAction(int activityIndex);
+    Q_INVOKABLE void triggerAction(int activityIndex, int actionIndex);
+
+    AccountState *accountState() const;
+
+public slots:
+    void slotRefreshActivity();
+    void slotRemoveAccount();
+
+signals:
+    void activityJobStatusCode(int statusCode);
+    void sendNotificationRequest(const QString &accountName, const QString &link, const QByteArray &verb, int row);
+
+protected:
+    void activitiesReceived(const QJsonDocument &json, int statusCode);
+    QHash<int, QByteArray> roleNames() const override;
+
+    void setAccountState(AccountState *state);
+    void setCurrentlyFetching(bool value);
+    bool currentlyFetching() const;
+    void setDoneFetching(bool value);
+    void setHideOldActivities(bool value);
+    void setDisplayActions(bool value);
+
+    virtual void startFetchJob();
+
+private:
+    void combineActivityLists();
+    bool canFetchActivities() const;
+
+    ActivityList _activityLists;
+    ActivityList _syncFileItemLists;
+    ActivityList _notificationLists;
+    ActivityList _listOfIgnoredFiles;
+    Activity _notificationIgnoredFiles;
+    ActivityList _notificationErrorsLists;
+    ActivityList _finalList;
+    int _currentItem = 0;
+
+    bool _displayActions = true;
+
+    int _totalActivitiesFetched = 0;
+    int _maxActivities = 100;
+    int _maxActivitiesDays = 30;
+    bool _showMoreActivitiesAvailableEntry = false;
+
+    QPointer<ConflictDialog> _currentConflictDialog;
+    QPointer<InvalidFilenameDialog> _currentInvalidFilenameDialog;
+
+    AccountState *_accountState = nullptr;
+    bool _currentlyFetching = false;
+    bool _doneFetching = false;
+    bool _hideOldActivities = true;
+};
+}
+
+#endif // ACTIVITYLISTMODEL_H
diff --git a/src/gui/tray/notificationcache.cpp b/src/gui/tray/notificationcache.cpp
new file mode 100644 (file)
index 0000000..f60c245
--- /dev/null
@@ -0,0 +1,24 @@
+#include "notificationcache.h"
+
+namespace OCC {
+
+bool NotificationCache::contains(const Notification &notification) const
+{
+    return _notifications.find(calculateKey(notification)) != _notifications.end();
+}
+
+void NotificationCache::insert(const Notification &notification)
+{
+    _notifications.insert(calculateKey(notification));
+}
+
+void NotificationCache::clear()
+{
+    _notifications.clear();
+}
+
+uint NotificationCache::calculateKey(const Notification &notification) const
+{
+    return qHash(notification.title + notification.message);
+}
+}
diff --git a/src/gui/tray/notificationcache.h b/src/gui/tray/notificationcache.h
new file mode 100644 (file)
index 0000000..65caff6
--- /dev/null
@@ -0,0 +1,28 @@
+#pragma once
+
+#include <QSet>
+
+namespace OCC {
+
+class NotificationCache
+{
+public:
+    struct Notification
+    {
+        QString title;
+        QString message;
+    };
+
+    bool contains(const Notification &notification) const;
+
+    void insert(const Notification &notification);
+
+    void clear();
+
+private:
+    uint calculateKey(const Notification &notification) const;
+
+
+    QSet<uint> _notifications;
+};
+}
diff --git a/src/gui/tray/notificationhandler.cpp b/src/gui/tray/notificationhandler.cpp
new file mode 100644 (file)
index 0000000..2bb4b7e
--- /dev/null
@@ -0,0 +1,161 @@
+#include "notificationhandler.h"
+
+#include "accountstate.h"
+#include "capabilities.h"
+#include "networkjobs.h"
+
+#include "iconjob.h"
+
+#include <QJsonDocument>
+#include <QJsonObject>
+
+namespace OCC {
+
+Q_LOGGING_CATEGORY(lcServerNotification, "nextcloud.gui.servernotification", QtInfoMsg)
+
+const QString notificationsPath = QLatin1String("ocs/v2.php/apps/notifications/api/v2/notifications");
+const char propertyAccountStateC[] = "oc_account_state";
+const int successStatusCode = 200;
+const int notModifiedStatusCode = 304;
+QMap<int, QByteArray> ServerNotificationHandler::iconCache;
+
+ServerNotificationHandler::ServerNotificationHandler(AccountState *accountState, QObject *parent)
+    : QObject(parent)
+    , _accountState(accountState)
+{
+}
+
+void ServerNotificationHandler::slotFetchNotifications()
+{
+    // check connectivity and credentials
+    if (!(_accountState && _accountState->isConnected() && _accountState->account() && _accountState->account()->credentials() && _accountState->account()->credentials()->ready())) {
+        deleteLater();
+        return;
+    }
+    // check if the account has notifications enabled. If the capabilities are
+    // not yet valid, its assumed that notifications are available.
+    if (_accountState->account()->capabilities().isValid()) {
+        if (!_accountState->account()->capabilities().notificationsAvailable()) {
+            qCInfo(lcServerNotification) << "Account" << _accountState->account()->displayName() << "does not have notifications enabled.";
+            deleteLater();
+            return;
+        }
+    }
+
+    // if the previous notification job has finished, start next.
+    _notificationJob = new JsonApiJob(_accountState->account(), notificationsPath, this);
+    QObject::connect(_notificationJob.data(), &JsonApiJob::jsonReceived,
+        this, &ServerNotificationHandler::slotNotificationsReceived);
+    QObject::connect(_notificationJob.data(), &JsonApiJob::etagResponseHeaderReceived,
+        this, &ServerNotificationHandler::slotEtagResponseHeaderReceived);
+    QObject::connect(_notificationJob.data(), &JsonApiJob::allowDesktopNotificationsChanged,
+            this, &ServerNotificationHandler::slotAllowDesktopNotificationsChanged);
+    _notificationJob->setProperty(propertyAccountStateC, QVariant::fromValue<AccountState *>(_accountState));
+    _notificationJob->addRawHeader("If-None-Match", _accountState->notificationsEtagResponseHeader());
+    _notificationJob->start();
+}
+
+void ServerNotificationHandler::slotEtagResponseHeaderReceived(const QByteArray &value, int statusCode)
+{
+    if (statusCode == successStatusCode) {
+        qCWarning(lcServerNotification) << "New Notification ETag Response Header received " << value;
+        auto *account = qvariant_cast<AccountState *>(sender()->property(propertyAccountStateC));
+        account->setNotificationsEtagResponseHeader(value);
+    }
+}
+
+void ServerNotificationHandler::slotAllowDesktopNotificationsChanged(bool isAllowed)
+{
+    auto *account = qvariant_cast<AccountState *>(sender()->property(propertyAccountStateC));
+    if (account != nullptr) {
+       account->setDesktopNotificationsAllowed(isAllowed);
+    }
+}
+
+void ServerNotificationHandler::slotIconDownloaded(QByteArray iconData)
+{
+    iconCache.insert(sender()->property("activityId").toInt(),iconData);
+}
+
+void ServerNotificationHandler::slotNotificationsReceived(const QJsonDocument &json, int statusCode)
+{
+    if (statusCode != successStatusCode && statusCode != notModifiedStatusCode) {
+        qCWarning(lcServerNotification) << "Notifications failed with status code " << statusCode;
+        deleteLater();
+        return;
+    }
+
+    if (statusCode == notModifiedStatusCode) {
+        qCWarning(lcServerNotification) << "Status code " << statusCode << " Not Modified - No new notifications.";
+        deleteLater();
+        return;
+    }
+
+    auto notifies = json.object().value("ocs").toObject().value("data").toArray();
+
+    auto *ai = qvariant_cast<AccountState *>(sender()->property(propertyAccountStateC));
+
+    ActivityList list;
+
+    foreach (auto element, notifies) {
+        Activity a;
+        auto json = element.toObject();
+        a._type = Activity::NotificationType;
+        a._accName = ai->account()->displayName();
+        a._id = json.value("notification_id").toInt();
+
+        //need to know, specially for remote_share
+        a._objectType = json.value("object_type").toString();
+        a._status = 0;
+
+        a._subject = json.value("subject").toString();
+        a._message = json.value("message").toString();
+        a._icon = json.value("icon").toString();
+
+        if (!a._icon.isEmpty()) {
+            auto *iconJob = new IconJob(QUrl(a._icon));
+            iconJob->setProperty("activityId", a._id);
+            connect(iconJob, &IconJob::jobFinished, this, &ServerNotificationHandler::slotIconDownloaded);
+        }
+
+        QUrl link(json.value("link").toString());
+        if (!link.isEmpty()) {
+            if (link.host().isEmpty()) {
+                link.setScheme(ai->account()->url().scheme());
+                link.setHost(ai->account()->url().host());
+            }
+            if (link.port() == -1) {
+                link.setPort(ai->account()->url().port());
+            }
+        }
+        a._link = link;
+        a._dateTime = QDateTime::fromString(json.value("datetime").toString(), Qt::ISODate);
+
+        auto actions = json.value("actions").toArray();
+        foreach (auto action, actions) {
+            auto actionJson = action.toObject();
+            ActivityLink al;
+            al._label = QUrl::fromPercentEncoding(actionJson.value("label").toString().toUtf8());
+            al._link = actionJson.value("link").toString();
+            al._verb = actionJson.value("type").toString().toUtf8();
+            al._primary = actionJson.value("primary").toBool();
+
+            a._links.append(al);
+        }
+
+        // Add another action to dismiss notification on server
+        // https://github.com/owncloud/notifications/blob/master/docs/ocs-endpoint-v1.md#deleting-a-notification-for-a-user
+        ActivityLink al;
+        al._label = tr("Dismiss");
+        al._link = Utility::concatUrlPath(ai->account()->url(), notificationsPath + "/" + QString::number(a._id)).toString();
+        al._verb = "DELETE";
+        al._primary = false;
+        a._links.append(al);
+
+        list.append(a);
+    }
+    emit newNotificationList(list);
+
+    deleteLater();
+}
+}
diff --git a/src/gui/tray/notificationhandler.h b/src/gui/tray/notificationhandler.h
new file mode 100644 (file)
index 0000000..5bd824f
--- /dev/null
@@ -0,0 +1,37 @@
+#ifndef NOTIFICATIONHANDLER_H
+#define NOTIFICATIONHANDLER_H
+
+#include <QtCore>
+
+#include "usermodel.h"
+
+class QJsonDocument;
+
+namespace OCC {
+
+class ServerNotificationHandler : public QObject
+{
+    Q_OBJECT
+public:
+    explicit ServerNotificationHandler(AccountState *accountState, QObject *parent = nullptr);
+    static QMap<int, QByteArray> iconCache;
+
+signals:
+    void newNotificationList(ActivityList);
+
+public slots:
+    void slotFetchNotifications();
+
+private slots:
+    void slotNotificationsReceived(const QJsonDocument &json, int statusCode);
+    void slotEtagResponseHeaderReceived(const QByteArray &value, int statusCode);
+    void slotIconDownloaded(QByteArray iconData);
+    void slotAllowDesktopNotificationsChanged(bool isAllowed);
+
+private:
+    QPointer<JsonApiJob> _notificationJob;
+    AccountState *_accountState;
+};
+}
+
+#endif // NOTIFICATIONHANDLER_H
index adb2003e911875848169a31e24cf6eb1db31f04d..8f74985c8e6d33961dd9c091e0425ee849f0c439 100644 (file)
@@ -18,7 +18,7 @@
 #include "navigationpanehelper.h"
 #include "networkjobs.h"
 #include "syncresult.h"
-#include "tray/UserModel.h"
+#include "tray/usermodel.h"
 
 #include <theme.h>
 
index f0bba8efef53998d0c4b6fa7ed964d714b8e60d3..97a57f519dfd28647c4275d170070a459d04252a 100644 (file)
@@ -14,7 +14,7 @@
 
 #include "unifiedsearchresultimageprovider.h"
 
-#include "UserModel.h"
+#include "usermodel.h"
 
 #include <QImage>
 #include <QPainter>
diff --git a/src/gui/tray/usermodel.cpp b/src/gui/tray/usermodel.cpp
new file mode 100644 (file)
index 0000000..36016ea
--- /dev/null
@@ -0,0 +1,1157 @@
+#include "notificationhandler.h"
+#include "usermodel.h"
+
+#include "accountmanager.h"
+#include "owncloudgui.h"
+#include <pushnotifications.h>
+#include "userstatusselectormodel.h"
+#include "syncengine.h"
+#include "ocsjob.h"
+#include "configfile.h"
+#include "notificationconfirmjob.h"
+#include "logger.h"
+#include "guiutility.h"
+#include "syncfileitem.h"
+#include "tray/activitylistmodel.h"
+#include "tray/notificationcache.h"
+#include "tray/unifiedsearchresultslistmodel.h"
+#include "userstatusconnector.h"
+
+#include <QDesktopServices>
+#include <QIcon>
+#include <QMessageBox>
+#include <QSvgRenderer>
+#include <QPainter>
+#include <QPushButton>
+
+// time span in milliseconds which has to be between two
+// refreshes of the notifications
+#define NOTIFICATION_REQUEST_FREE_PERIOD 15000
+
+namespace {
+constexpr qint64 expiredActivitiesCheckIntervalMsecs = 1000 * 60;
+constexpr qint64 activityDefaultExpirationTimeMsecs = 1000 * 60 * 10;
+}
+
+namespace OCC {
+
+User::User(AccountStatePtr &account, const bool &isCurrent, QObject *parent)
+    : QObject(parent)
+    , _account(account)
+    , _isCurrentUser(isCurrent)
+    , _activityModel(new ActivityListModel(_account.data(), this))
+    , _unifiedSearchResultsModel(new UnifiedSearchResultsListModel(_account.data(), this))
+    , _notificationRequestsRunning(0)
+{
+    connect(ProgressDispatcher::instance(), &ProgressDispatcher::progressInfo,
+        this, &User::slotProgressInfo);
+    connect(ProgressDispatcher::instance(), &ProgressDispatcher::itemCompleted,
+        this, &User::slotItemCompleted);
+    connect(ProgressDispatcher::instance(), &ProgressDispatcher::syncError,
+        this, &User::slotAddError);
+    connect(ProgressDispatcher::instance(), &ProgressDispatcher::addErrorToGui,
+        this, &User::slotAddErrorToGui);
+
+    connect(&_notificationCheckTimer, &QTimer::timeout,
+        this, &User::slotRefresh);
+
+    connect(&_expiredActivitiesCheckTimer, &QTimer::timeout,
+        this, &User::slotCheckExpiredActivities);
+
+    connect(_account.data(), &AccountState::stateChanged,
+            [=]() { if (isConnected()) {slotRefreshImmediately();} });
+    connect(_account.data(), &AccountState::stateChanged, this, &User::accountStateChanged);
+    connect(_account.data(), &AccountState::hasFetchedNavigationApps,
+        this, &User::slotRebuildNavigationAppList);
+    connect(_account->account().data(), &Account::accountChangedDisplayName, this, &User::nameChanged);
+
+    connect(FolderMan::instance(), &FolderMan::folderListChanged, this, &User::hasLocalFolderChanged);
+
+    connect(this, &User::guiLog, Logger::instance(), &Logger::guiLog);
+
+    connect(_account->account().data(), &Account::accountChangedAvatar, this, &User::avatarChanged);
+    connect(_account->account().data(), &Account::userStatusChanged, this, &User::statusChanged);
+    connect(_account.data(), &AccountState::desktopNotificationsAllowedChanged, this, &User::desktopNotificationsAllowedChanged);
+
+    connect(_activityModel, &ActivityListModel::sendNotificationRequest, this, &User::slotSendNotificationRequest);
+}
+
+void User::showDesktopNotification(const QString &title, const QString &message)
+{
+    ConfigFile cfg;
+    if (!cfg.optionalServerNotifications() || !isDesktopNotificationsAllowed()) {
+        return;
+    }
+
+    // after one hour, clear the gui log notification store
+    constexpr qint64 clearGuiLogInterval = 60 * 60 * 1000;
+    if (_guiLogTimer.elapsed() > clearGuiLogInterval) {
+        _notificationCache.clear();
+    }
+
+    const NotificationCache::Notification notification { title, message };
+    if (_notificationCache.contains(notification)) {
+        return;
+    }
+
+    _notificationCache.insert(notification);
+    emit guiLog(notification.title, notification.message);
+    // restart the gui log timer now that we show a new notification
+    _guiLogTimer.start();
+}
+
+void User::slotBuildNotificationDisplay(const ActivityList &list)
+{
+    _activityModel->clearNotifications();
+
+    foreach (auto activity, list) {
+        if (_blacklistedNotifications.contains(activity)) {
+            qCInfo(lcActivity) << "Activity in blacklist, skip";
+            continue;
+        }
+        const auto message = AccountManager::instance()->accounts().count() == 1 ? "" : activity._accName;
+        showDesktopNotification(activity._subject, message);
+        _activityModel->addNotificationToActivityList(activity);
+    }
+}
+
+void User::setNotificationRefreshInterval(std::chrono::milliseconds interval)
+{
+    if (!checkPushNotificationsAreReady()) {
+        qCDebug(lcActivity) << "Starting Notification refresh timer with " << interval.count() / 1000 << " sec interval";
+        _notificationCheckTimer.start(interval.count());
+    }
+}
+
+void User::slotPushNotificationsReady()
+{
+    qCInfo(lcActivity) << "Push notifications are ready";
+
+    if (_notificationCheckTimer.isActive()) {
+        // as we are now able to use push notifications - let's stop the polling timer
+        _notificationCheckTimer.stop();
+    }
+
+    connectPushNotifications();
+}
+
+void User::slotDisconnectPushNotifications()
+{
+    disconnect(_account->account()->pushNotifications(), &PushNotifications::notificationsChanged, this, &User::slotReceivedPushNotification);
+    disconnect(_account->account()->pushNotifications(), &PushNotifications::activitiesChanged, this, &User::slotReceivedPushActivity);
+
+    disconnect(_account->account().data(), &Account::pushNotificationsDisabled, this, &User::slotDisconnectPushNotifications);
+
+    // connection to WebSocket may have dropped or an error occured, so we need to bring back the polling until we have re-established the connection
+    setNotificationRefreshInterval(ConfigFile().notificationRefreshInterval());
+}
+
+void User::slotReceivedPushNotification(Account *account)
+{
+    if (account->id() == _account->account()->id()) {
+        slotRefreshNotifications();
+    }
+}
+
+void User::slotReceivedPushActivity(Account *account)
+{
+    if (account->id() == _account->account()->id()) {
+        slotRefreshActivities();
+    }
+}
+
+void User::slotCheckExpiredActivities()
+{
+    for (const Activity &activity : _activityModel->errorsList()) {
+        if (activity._expireAtMsecs > 0 && QDateTime::currentDateTime().toMSecsSinceEpoch() >= activity._expireAtMsecs) {
+            _activityModel->removeActivityFromActivityList(activity);
+        }
+    }
+
+    if (_activityModel->errorsList().size() == 0) {
+        _expiredActivitiesCheckTimer.stop();
+    }
+}
+
+void User::connectPushNotifications() const
+{
+    connect(_account->account().data(), &Account::pushNotificationsDisabled, this, &User::slotDisconnectPushNotifications, Qt::UniqueConnection);
+
+    connect(_account->account()->pushNotifications(), &PushNotifications::notificationsChanged, this, &User::slotReceivedPushNotification, Qt::UniqueConnection);
+    connect(_account->account()->pushNotifications(), &PushNotifications::activitiesChanged, this, &User::slotReceivedPushActivity, Qt::UniqueConnection);
+}
+
+bool User::checkPushNotificationsAreReady() const
+{
+    const auto pushNotifications = _account->account()->pushNotifications();
+
+    const auto pushActivitiesAvailable = _account->account()->capabilities().availablePushNotifications() & PushNotificationType::Activities;
+    const auto pushNotificationsAvailable = _account->account()->capabilities().availablePushNotifications() & PushNotificationType::Notifications;
+
+    const auto pushActivitiesAndNotificationsAvailable = pushActivitiesAvailable && pushNotificationsAvailable;
+
+    if (pushActivitiesAndNotificationsAvailable && pushNotifications && pushNotifications->isReady()) {
+        connectPushNotifications();
+        return true;
+    } else {
+        connect(_account->account().data(), &Account::pushNotificationsReady, this, &User::slotPushNotificationsReady, Qt::UniqueConnection);
+        return false;
+    }
+}
+
+void User::slotRefreshImmediately() {
+    if (_account.data() && _account.data()->isConnected()) {
+        slotRefreshActivities();
+    }
+    slotRefreshNotifications();
+}
+
+void User::slotRefresh()
+{
+    slotRefreshUserStatus();
+    
+    if (checkPushNotificationsAreReady()) {
+        // we are relying on WebSocket push notifications - ignore refresh attempts from UI
+        _timeSinceLastCheck[_account.data()].invalidate();
+        return;
+    }
+
+    // QElapsedTimer isn't actually constructed as invalid.
+    if (!_timeSinceLastCheck.contains(_account.data())) {
+        _timeSinceLastCheck[_account.data()].invalidate();
+    }
+    QElapsedTimer &timer = _timeSinceLastCheck[_account.data()];
+
+    // Fetch Activities only if visible and if last check is longer than 15 secs ago
+    if (timer.isValid() && timer.elapsed() < NOTIFICATION_REQUEST_FREE_PERIOD) {
+        qCDebug(lcActivity) << "Do not check as last check is only secs ago: " << timer.elapsed() / 1000;
+        return;
+    }
+    if (_account.data() && _account.data()->isConnected()) {
+        if (!timer.isValid()) {
+            slotRefreshActivities();
+        }
+        slotRefreshNotifications();
+        timer.start();
+    }
+}
+
+void User::slotRefreshActivities()
+{
+    _activityModel->slotRefreshActivity();
+}
+
+void User::slotRefreshUserStatus()
+{
+    if (_account.data() && _account.data()->isConnected()) {
+        _account->account()->userStatusConnector()->fetchUserStatus();
+    }
+}
+
+void User::slotRefreshNotifications()
+{
+    // start a server notification handler if no notification requests
+    // are running
+    if (_notificationRequestsRunning == 0) {
+        auto *snh = new ServerNotificationHandler(_account.data());
+        connect(snh, &ServerNotificationHandler::newNotificationList,
+            this, &User::slotBuildNotificationDisplay);
+
+        snh->slotFetchNotifications();
+    } else {
+        qCWarning(lcActivity) << "Notification request counter not zero.";
+    }
+}
+
+void User::slotRebuildNavigationAppList()
+{
+    emit serverHasTalkChanged();
+    // Rebuild App list
+    UserAppsModel::instance()->buildAppList();
+}
+
+void User::slotNotificationRequestFinished(int statusCode)
+{
+    int row = sender()->property("activityRow").toInt();
+
+    // the ocs API returns stat code 100 or 200 inside the xml if it succeeded.
+    if (statusCode != OCS_SUCCESS_STATUS_CODE && statusCode != OCS_SUCCESS_STATUS_CODE_V2) {
+        qCWarning(lcActivity) << "Notification Request to Server failed, leave notification visible.";
+    } else {
+        // to do use the model to rebuild the list or remove the item
+        qCWarning(lcActivity) << "Notification Request to Server successed, rebuilding list.";
+        _activityModel->removeActivityFromActivityList(row);
+    }
+}
+
+void User::slotEndNotificationRequest(int replyCode)
+{
+    _notificationRequestsRunning--;
+    slotNotificationRequestFinished(replyCode);
+}
+
+void User::slotSendNotificationRequest(const QString &accountName, const QString &link, const QByteArray &verb, int row)
+{
+    qCInfo(lcActivity) << "Server Notification Request " << verb << link << "on account" << accountName;
+
+    const QStringList validVerbs = QStringList() << "GET"
+                                                 << "PUT"
+                                                 << "POST"
+                                                 << "DELETE";
+
+    if (validVerbs.contains(verb)) {
+        AccountStatePtr acc = AccountManager::instance()->account(accountName);
+        if (acc) {
+            auto *job = new NotificationConfirmJob(acc->account());
+            QUrl l(link);
+            job->setLinkAndVerb(l, verb);
+            job->setProperty("activityRow", QVariant::fromValue(row));
+            connect(job, &AbstractNetworkJob::networkError,
+                this, &User::slotNotifyNetworkError);
+            connect(job, &NotificationConfirmJob::jobFinished,
+                this, &User::slotNotifyServerFinished);
+            job->start();
+
+            // count the number of running notification requests. If this member var
+            // is larger than zero, no new fetching of notifications is started
+            _notificationRequestsRunning++;
+        }
+    } else {
+        qCWarning(lcActivity) << "Notification Links: Invalid verb:" << verb;
+    }
+}
+
+void User::slotNotifyNetworkError(QNetworkReply *reply)
+{
+    auto *job = qobject_cast<NotificationConfirmJob *>(sender());
+    if (!job) {
+        return;
+    }
+
+    int resultCode = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt();
+
+    slotEndNotificationRequest(resultCode);
+    qCWarning(lcActivity) << "Server notify job failed with code " << resultCode;
+}
+
+void User::slotNotifyServerFinished(const QString &reply, int replyCode)
+{
+    auto *job = qobject_cast<NotificationConfirmJob *>(sender());
+    if (!job) {
+        return;
+    }
+
+    slotEndNotificationRequest(replyCode);
+    qCInfo(lcActivity) << "Server Notification reply code" << replyCode << reply;
+}
+
+void User::slotProgressInfo(const QString &folder, const ProgressInfo &progress)
+{
+    if (progress.status() == ProgressInfo::Reconcile) {
+        // Wipe all non-persistent entries - as well as the persistent ones
+        // in cases where a local discovery was done.
+        auto f = FolderMan::instance()->folder(folder);
+        if (!f)
+            return;
+        const auto &engine = f->syncEngine();
+        const auto style = engine.lastLocalDiscoveryStyle();
+        foreach (Activity activity, _activityModel->errorsList()) {
+            if (activity._expireAtMsecs != -1) {
+                // we process expired activities in a different slot
+                continue;
+            }
+            if (activity._folder != folder) {
+                continue;
+            }
+
+            if (style == LocalDiscoveryStyle::FilesystemOnly) {
+                _activityModel->removeActivityFromActivityList(activity);
+                continue;
+            }
+
+            if (activity._status == SyncFileItem::Conflict && !QFileInfo(f->path() + activity._file).exists()) {
+                _activityModel->removeActivityFromActivityList(activity);
+                continue;
+            }
+
+            if (activity._status == SyncFileItem::FileLocked && !QFileInfo(f->path() + activity._file).exists()) {
+                _activityModel->removeActivityFromActivityList(activity);
+                continue;
+            }
+
+
+            if (activity._status == SyncFileItem::FileIgnored && !QFileInfo(f->path() + activity._file).exists()) {
+                _activityModel->removeActivityFromActivityList(activity);
+                continue;
+            }
+
+
+            if (!QFileInfo(f->path() + activity._file).exists()) {
+                _activityModel->removeActivityFromActivityList(activity);
+                continue;
+            }
+
+            auto path = QFileInfo(activity._file).dir().path().toUtf8();
+            if (path == ".")
+                path.clear();
+
+            if (engine.shouldDiscoverLocally(path))
+                _activityModel->removeActivityFromActivityList(activity);
+        }
+    }
+
+    if (progress.status() == ProgressInfo::Done) {
+        // We keep track very well of pending conflicts.
+        // Inform other components about them.
+        QStringList conflicts;
+        foreach (Activity activity, _activityModel->errorsList()) {
+            if (activity._folder == folder
+                && activity._status == SyncFileItem::Conflict) {
+                conflicts.append(activity._file);
+            }
+        }
+
+        emit ProgressDispatcher::instance()->folderConflicts(folder, conflicts);
+    }
+}
+
+void User::slotAddError(const QString &folderAlias, const QString &message, ErrorCategory category)
+{
+    auto folderInstance = FolderMan::instance()->folder(folderAlias);
+    if (!folderInstance)
+        return;
+
+    if (folderInstance->accountState() == _account.data()) {
+        qCWarning(lcActivity) << "Item " << folderInstance->shortGuiLocalPath() << " retrieved resulted in " << message;
+
+        Activity activity;
+        activity._type = Activity::SyncResultType;
+        activity._status = SyncResult::Error;
+        activity._dateTime = QDateTime::fromString(QDateTime::currentDateTime().toString(), Qt::ISODate);
+        activity._subject = message;
+        activity._message = folderInstance->shortGuiLocalPath();
+        activity._link = folderInstance->shortGuiLocalPath();
+        activity._accName = folderInstance->accountState()->account()->displayName();
+        activity._folder = folderAlias;
+
+
+        if (category == ErrorCategory::InsufficientRemoteStorage) {
+            ActivityLink link;
+            link._label = tr("Retry all uploads");
+            link._link = folderInstance->path();
+            link._verb = "";
+            link._primary = true;
+            activity._links.append(link);
+        }
+
+        // add 'other errors' to activity list
+        _activityModel->addErrorToActivityList(activity);
+    }
+}
+
+void User::slotAddErrorToGui(const QString &folderAlias, SyncFileItem::Status status, const QString &errorMessage, const QString &subject)
+{
+    const auto folderInstance = FolderMan::instance()->folder(folderAlias);
+    if (!folderInstance) {
+        return;
+    }
+
+    if (folderInstance->accountState() == _account.data()) {
+        qCWarning(lcActivity) << "Item " << folderInstance->shortGuiLocalPath() << " retrieved resulted in " << errorMessage;
+
+        Activity activity;
+        activity._type = Activity::SyncFileItemType;
+        activity._status = status;
+        const auto currentDateTime = QDateTime::currentDateTime();
+        activity._dateTime = QDateTime::fromString(currentDateTime.toString(), Qt::ISODate);
+        activity._expireAtMsecs = currentDateTime.addMSecs(activityDefaultExpirationTimeMsecs).toMSecsSinceEpoch();
+        activity._subject = !subject.isEmpty() ? subject : folderInstance->shortGuiLocalPath();
+        activity._message = errorMessage;
+        activity._link = folderInstance->shortGuiLocalPath();
+        activity._accName = folderInstance->accountState()->account()->displayName();
+        activity._folder = folderAlias;
+
+        // add 'other errors' to activity list
+        _activityModel->addErrorToActivityList(activity);
+
+        showDesktopNotification(activity._subject, activity._message);
+
+        if (!_expiredActivitiesCheckTimer.isActive()) {
+            _expiredActivitiesCheckTimer.start(expiredActivitiesCheckIntervalMsecs);
+        }
+    }
+}
+
+bool User::isActivityOfCurrentAccount(const Folder *folder) const
+{
+    return folder->accountState() == _account.data();
+}
+
+bool User::isUnsolvableConflict(const SyncFileItemPtr &item) const
+{
+    // We just care about conflict issues that we are able to resolve
+    return item->_status == SyncFileItem::Conflict && !Utility::isConflictFile(item->_file);
+}
+
+void User::processCompletedSyncItem(const Folder *folder, const SyncFileItemPtr &item)
+{
+    Activity activity;
+    activity._type = Activity::SyncFileItemType; //client activity
+    activity._status = item->_status;
+    activity._dateTime = QDateTime::currentDateTime();
+    activity._message = item->_originalFile;
+    activity._link = folder->accountState()->account()->url();
+    activity._accName = folder->accountState()->account()->displayName();
+    activity._file = item->_file;
+    activity._folder = folder->alias();
+    activity._fileAction = "";
+
+    if (item->_instruction == CSYNC_INSTRUCTION_REMOVE) {
+        activity._fileAction = "file_deleted";
+    } else if (item->_instruction == CSYNC_INSTRUCTION_NEW) {
+        activity._fileAction = "file_created";
+    } else if (item->_instruction == CSYNC_INSTRUCTION_RENAME) {
+        activity._fileAction = "file_renamed";
+    } else {
+        activity._fileAction = "file_changed";
+    }
+
+    if (item->_status == SyncFileItem::NoStatus || item->_status == SyncFileItem::Success) {
+        qCWarning(lcActivity) << "Item " << item->_file << " retrieved successfully.";
+
+        if (item->_direction != SyncFileItem::Up) {
+            activity._message = tr("Synced %1").arg(item->_originalFile);
+        } else if (activity._fileAction == "file_renamed") {
+            activity._message = tr("You renamed %1").arg(item->_originalFile);
+        } else if (activity._fileAction == "file_deleted") {
+            activity._message = tr("You deleted %1").arg(item->_originalFile);
+        } else if (activity._fileAction == "file_created") {
+            activity._message = tr("You created %1").arg(item->_originalFile);
+        } else {
+            activity._message = tr("You changed %1").arg(item->_originalFile);
+        }
+
+        _activityModel->addSyncFileItemToActivityList(activity);
+    } else {
+        qCWarning(lcActivity) << "Item " << item->_file << " retrieved resulted in error " << item->_errorString;
+        activity._subject = item->_errorString;
+
+        if (item->_status == SyncFileItem::Status::FileIgnored) {
+            _activityModel->addIgnoredFileToList(activity);
+        } else {
+            // add 'protocol error' to activity list
+            if (item->_status == SyncFileItem::Status::FileNameInvalid) {
+                showDesktopNotification(item->_file, activity._subject);
+            }
+            _activityModel->addErrorToActivityList(activity);
+        }
+    }
+}
+
+void User::slotItemCompleted(const QString &folder, const SyncFileItemPtr &item)
+{
+    auto folderInstance = FolderMan::instance()->folder(folder);
+
+    if (!folderInstance || !isActivityOfCurrentAccount(folderInstance) || isUnsolvableConflict(item)) {
+        return;
+    }
+
+    qCWarning(lcActivity) << "Item " << item->_file << " retrieved resulted in " << item->_errorString;
+    processCompletedSyncItem(folderInstance, item);
+}
+
+AccountPtr User::account() const
+{
+    return _account->account();
+}
+
+AccountStatePtr User::accountState() const
+{
+    return _account;
+}
+
+void User::setCurrentUser(const bool &isCurrent)
+{
+    _isCurrentUser = isCurrent;
+}
+
+Folder *User::getFolder() const
+{
+    foreach (Folder *folder, FolderMan::instance()->map()) {
+        if (folder->accountState() == _account.data()) {
+            return folder;
+        }
+    }
+
+    return nullptr;
+}
+
+ActivityListModel *User::getActivityModel()
+{
+    return _activityModel;
+}
+
+UnifiedSearchResultsListModel *User::getUnifiedSearchResultsListModel() const
+{
+    return _unifiedSearchResultsModel;
+}
+
+void User::openLocalFolder()
+{
+    const auto folder = getFolder();
+
+    if (folder) {
+        QDesktopServices::openUrl(QUrl::fromLocalFile(folder->path()));
+    }
+}
+
+void User::login() const
+{
+    _account->account()->resetRejectedCertificates();
+    _account->signIn();
+}
+
+void User::logout() const
+{
+    _account->signOutByUi();
+}
+
+QString User::name() const
+{
+    // If davDisplayName is empty (can be several reasons, simplest is missing login at startup), fall back to username
+    QString name = _account->account()->davDisplayName();
+    if (name == "") {
+        name = _account->account()->credentials()->user();
+    }
+    return name;
+}
+
+QString User::server(bool shortened) const
+{
+    QString serverUrl = _account->account()->url().toString();
+    if (shortened) {
+        serverUrl.replace(QLatin1String("https://"), QLatin1String(""));
+        serverUrl.replace(QLatin1String("http://"), QLatin1String(""));
+    }
+    return serverUrl;
+}
+
+UserStatus::OnlineStatus User::status() const
+{
+    return _account->account()->userStatusConnector()->userStatus().state();
+}
+
+QString User::statusMessage() const
+{
+    return _account->account()->userStatusConnector()->userStatus().message();
+}
+
+QUrl User::statusIcon() const
+{
+    return _account->account()->userStatusConnector()->userStatus().stateIcon();
+}
+
+QString User::statusEmoji() const
+{
+    return _account->account()->userStatusConnector()->userStatus().icon();
+}
+
+bool User::serverHasUserStatus() const
+{
+    return _account->account()->capabilities().userStatusNotification();
+}
+
+QImage User::avatar() const
+{
+    return AvatarJob::makeCircularAvatar(_account->account()->avatar());
+}
+
+QString User::avatarUrl() const
+{
+    if (avatar().isNull()) {
+        return QString();
+    }
+
+    return QStringLiteral("image://avatars/") + _account->account()->id();
+}
+
+bool User::hasLocalFolder() const
+{
+    return getFolder() != nullptr;
+}
+
+bool User::serverHasTalk() const
+{
+    return talkApp() != nullptr;
+}
+
+AccountApp *User::talkApp() const
+{
+    return _account->findApp(QStringLiteral("spreed"));
+}
+
+bool User::hasActivities() const
+{
+    return _account->account()->capabilities().hasActivities();
+}
+
+AccountAppList User::appList() const
+{
+    return _account->appList();
+}
+
+bool User::isCurrentUser() const
+{
+    return _isCurrentUser;
+}
+
+bool User::isConnected() const
+{
+    return (_account->connectionStatus() == AccountState::ConnectionStatus::Connected);
+}
+
+
+bool User::isDesktopNotificationsAllowed() const
+{
+    return _account.data()->isDesktopNotificationsAllowed();
+}
+
+void User::removeAccount() const
+{
+    AccountManager::instance()->deleteAccount(_account.data());
+    AccountManager::instance()->save();
+}
+
+/*-------------------------------------------------------------------------------------*/
+
+UserModel *UserModel::_instance = nullptr;
+
+UserModel *UserModel::instance()
+{
+    if (!_instance) {
+        _instance = new UserModel();
+    }
+    return _instance;
+}
+
+UserModel::UserModel(QObject *parent)
+    : QAbstractListModel(parent)
+{
+    // TODO: Remember selected user from last quit via settings file
+    if (AccountManager::instance()->accounts().size() > 0) {
+        buildUserList();
+    }
+
+    connect(AccountManager::instance(), &AccountManager::accountAdded,
+        this, &UserModel::buildUserList);
+}
+
+void UserModel::buildUserList()
+{
+    for (int i = 0; i < AccountManager::instance()->accounts().size(); i++) {
+        auto user = AccountManager::instance()->accounts().at(i);
+        addUser(user);
+    }
+    if (_init) {
+        _users.first()->setCurrentUser(true);
+        _init = false;
+    }
+}
+
+Q_INVOKABLE int UserModel::numUsers()
+{
+    return _users.size();
+}
+
+Q_INVOKABLE int UserModel::currentUserId() const
+{
+    return _currentUserId;
+}
+
+Q_INVOKABLE bool UserModel::isUserConnected(const int &id)
+{
+    if (id < 0 || id >= _users.size())
+        return false;
+
+    return _users[id]->isConnected();
+}
+
+QImage UserModel::avatarById(const int &id)
+{
+    if (id < 0 || id >= _users.size())
+        return {};
+
+    return _users[id]->avatar();
+}
+
+Q_INVOKABLE QString UserModel::currentUserServer()
+{
+    if (_currentUserId < 0 || _currentUserId >= _users.size())
+        return {};
+
+    return _users[_currentUserId]->server();
+}
+
+void UserModel::addUser(AccountStatePtr &user, const bool &isCurrent)
+{
+    bool containsUser = false;
+    for (const auto &u : qAsConst(_users)) {
+        if (u->account() == user->account()) {
+            containsUser = true;
+            continue;
+        }
+    }
+
+    if (!containsUser) {
+        int row = rowCount();
+        beginInsertRows(QModelIndex(), row, row);
+
+        User *u = new User(user, isCurrent);
+
+        connect(u, &User::avatarChanged, this, [this, row] {
+           emit dataChanged(index(row, 0), index(row, 0), {UserModel::AvatarRole});
+        });
+
+        connect(u, &User::statusChanged, this, [this, row] {
+            emit dataChanged(index(row, 0), index(row, 0), {UserModel::StatusIconRole, 
+                                                           UserModel::StatusEmojiRole,     
+                                                            UserModel::StatusMessageRole});
+        });
+        
+        connect(u, &User::desktopNotificationsAllowedChanged, this, [this, row] {
+            emit dataChanged(index(row, 0), index(row, 0), { UserModel::DesktopNotificationsAllowedRole });
+        });
+        
+        connect(u, &User::accountStateChanged, this, [this, row] {
+            emit dataChanged(index(row, 0), index(row, 0), { UserModel::IsConnectedRole });
+        });
+
+        _users << u;
+        if (isCurrent) {
+            _currentUserId = _users.indexOf(_users.last());
+        }
+
+        endInsertRows();
+        ConfigFile cfg;
+        _users.last()->setNotificationRefreshInterval(cfg.notificationRefreshInterval());
+        emit newUserSelected();
+    }
+}
+
+int UserModel::currentUserIndex()
+{
+    return _currentUserId;
+}
+
+Q_INVOKABLE void UserModel::openCurrentAccountLocalFolder()
+{
+    if (_currentUserId < 0 || _currentUserId >= _users.size())
+        return;
+
+    _users[_currentUserId]->openLocalFolder();
+}
+
+Q_INVOKABLE void UserModel::openCurrentAccountTalk()
+{
+    if (!currentUser())
+        return;
+
+    const auto talkApp = currentUser()->talkApp();
+    if (talkApp) {
+        Utility::openBrowser(talkApp->url());
+    } else {
+        qCWarning(lcActivity) << "The Talk app is not enabled on" << currentUser()->server();
+    }
+}
+
+Q_INVOKABLE void UserModel::openCurrentAccountServer()
+{
+    if (_currentUserId < 0 || _currentUserId >= _users.size())
+        return;
+
+    QString url = _users[_currentUserId]->server(false);
+    if (!url.startsWith("http://") && !url.startsWith("https://")) {
+        url = "https://" + _users[_currentUserId]->server(false);
+    }
+
+    QDesktopServices::openUrl(url);
+}
+
+Q_INVOKABLE void UserModel::switchCurrentUser(const int &id)
+{
+    if (_currentUserId < 0 || _currentUserId >= _users.size())
+        return;
+    
+    _users[_currentUserId]->setCurrentUser(false);
+    _users[id]->setCurrentUser(true);
+    _currentUserId = id;
+    emit newUserSelected();
+}
+
+Q_INVOKABLE void UserModel::login(const int &id)
+{
+    if (id < 0 || id >= _users.size())
+        return;
+
+    _users[id]->login();
+}
+
+Q_INVOKABLE void UserModel::logout(const int &id)
+{
+    if (id < 0 || id >= _users.size())
+        return;
+
+    _users[id]->logout();
+}
+
+Q_INVOKABLE void UserModel::removeAccount(const int &id)
+{
+    if (id < 0 || id >= _users.size())
+        return;
+
+    QMessageBox messageBox(QMessageBox::Question,
+        tr("Confirm Account Removal"),
+        tr("<p>Do you really want to remove the connection to the account <i>%1</i>?</p>"
+           "<p><b>Note:</b> This will <b>not</b> delete any files.</p>")
+            .arg(_users[id]->name()),
+        QMessageBox::NoButton);
+    QPushButton *yesButton =
+        messageBox.addButton(tr("Remove connection"), QMessageBox::YesRole);
+    messageBox.addButton(tr("Cancel"), QMessageBox::NoRole);
+
+    messageBox.exec();
+    if (messageBox.clickedButton() != yesButton) {
+        return;
+    }
+
+    if (_users[id]->isCurrentUser() && _users.count() > 1) {
+        id == 0 ? switchCurrentUser(1) : switchCurrentUser(0);
+    }
+
+    _users[id]->logout();
+    _users[id]->removeAccount();
+
+    beginRemoveRows(QModelIndex(), id, id);
+    _users.removeAt(id);
+    endRemoveRows();
+}
+
+std::shared_ptr<OCC::UserStatusConnector> UserModel::userStatusConnector(int id)
+{
+    if (id < 0 || id >= _users.size()) {
+        return nullptr;
+    }
+
+    return _users[id]->account()->userStatusConnector();
+}
+
+int UserModel::rowCount(const QModelIndex &parent) const
+{
+    Q_UNUSED(parent);
+    return _users.count();
+}
+
+QVariant UserModel::data(const QModelIndex &index, int role) const
+{
+    if (index.row() < 0 || index.row() >= _users.count()) {
+        return QVariant();
+    }
+
+    if (role == NameRole) {
+        return _users[index.row()]->name();
+    } else if (role == ServerRole) {
+        return _users[index.row()]->server();
+    } else if (role == ServerHasUserStatusRole) {
+        return _users[index.row()]->serverHasUserStatus();
+    } else if (role == StatusIconRole) {
+        return _users[index.row()]->statusIcon();
+    } else if (role == StatusEmojiRole) {
+        return _users[index.row()]->statusEmoji();
+    } else if (role == StatusMessageRole) {
+        return _users[index.row()]->statusMessage();
+    } else if (role == DesktopNotificationsAllowedRole) {
+        return _users[index.row()]->isDesktopNotificationsAllowed();
+    } else if (role == AvatarRole) {
+        return _users[index.row()]->avatarUrl();
+    } else if (role == IsCurrentUserRole) {
+        return _users[index.row()]->isCurrentUser();
+    } else if (role == IsConnectedRole) {
+        return _users[index.row()]->isConnected();
+    } else if (role == IdRole) {
+        return index.row();
+    }
+    return QVariant();
+}
+
+QHash<int, QByteArray> UserModel::roleNames() const
+{
+    QHash<int, QByteArray> roles;
+    roles[NameRole] = "name";
+    roles[ServerRole] = "server";
+    roles[ServerHasUserStatusRole] = "serverHasUserStatus";
+    roles[StatusIconRole] = "statusIcon";
+    roles[StatusEmojiRole] = "statusEmoji";
+    roles[StatusMessageRole] = "statusMessage";
+    roles[DesktopNotificationsAllowedRole] = "desktopNotificationsAllowed";
+    roles[AvatarRole] = "avatar";
+    roles[IsCurrentUserRole] = "isCurrentUser";
+    roles[IsConnectedRole] = "isConnected";
+    roles[IdRole] = "id";
+    return roles;
+}
+
+ActivityListModel *UserModel::currentActivityModel()
+{
+    if (currentUserIndex() < 0 || currentUserIndex() >= _users.size())
+        return nullptr;
+
+    return _users[currentUserIndex()]->getActivityModel();
+}
+
+void UserModel::fetchCurrentActivityModel()
+{
+    if (currentUserId() < 0 || currentUserId() >= _users.size())
+        return;
+
+    _users[currentUserId()]->slotRefresh();
+}
+
+AccountAppList UserModel::appList() const
+{
+    if (_currentUserId < 0 || _currentUserId >= _users.size())
+        return {};
+
+    return _users[_currentUserId]->appList();
+}
+
+User *UserModel::currentUser() const
+{
+    if (currentUserId() < 0 || currentUserId() >= _users.size())
+        return nullptr;
+
+    return _users[currentUserId()];
+}
+
+int UserModel::findUserIdForAccount(AccountState *account) const
+{
+    const auto it = std::find_if(std::cbegin(_users), std::cend(_users), [=](const User *user) {
+        return user->account()->id() == account->account()->id();
+    });
+
+    if (it == std::cend(_users)) {
+        return -1;
+    }
+
+    const auto id = std::distance(std::cbegin(_users), it);
+    return id;
+}
+
+/*-------------------------------------------------------------------------------------*/
+
+ImageProvider::ImageProvider()
+    : QQuickImageProvider(QQuickImageProvider::Image)
+{
+}
+
+QImage ImageProvider::requestImage(const QString &id, QSize *size, const QSize &requestedSize)
+{
+    Q_UNUSED(size)
+    Q_UNUSED(requestedSize)
+
+    const auto makeIcon = [](const QString &path) {
+        QImage image(128, 128, QImage::Format_ARGB32);
+        image.fill(Qt::GlobalColor::transparent);
+        QPainter painter(&image);
+        QSvgRenderer renderer(path);
+        renderer.render(&painter);
+        return image;
+    };
+
+    if (id == QLatin1String("fallbackWhite")) {
+        return makeIcon(QStringLiteral(":/client/theme/white/user.svg"));
+    }
+
+    if (id == QLatin1String("fallbackBlack")) {
+        return makeIcon(QStringLiteral(":/client/theme/black/user.svg"));
+    }
+
+    const int uid = id.toInt();
+    return UserModel::instance()->avatarById(uid);
+}
+
+/*-------------------------------------------------------------------------------------*/
+
+UserAppsModel *UserAppsModel::_instance = nullptr;
+
+UserAppsModel *UserAppsModel::instance()
+{
+    if (!_instance) {
+        _instance = new UserAppsModel();
+    }
+    return _instance;
+}
+
+UserAppsModel::UserAppsModel(QObject *parent)
+    : QAbstractListModel(parent)
+{
+}
+
+void UserAppsModel::buildAppList()
+{
+    if (rowCount() > 0) {
+        beginRemoveRows(QModelIndex(), 0, rowCount() - 1);
+        _apps.clear();
+        endRemoveRows();
+    }
+
+    if (UserModel::instance()->appList().count() > 0) {
+        const auto talkApp = UserModel::instance()->currentUser()->talkApp();
+        foreach (AccountApp *app, UserModel::instance()->appList()) {
+            // Filter out Talk because we have a dedicated button for it
+            if (talkApp && app->id() == talkApp->id())
+                continue;
+
+            beginInsertRows(QModelIndex(), rowCount(), rowCount());
+            _apps << app;
+            endInsertRows();
+        }
+    }
+}
+
+void UserAppsModel::openAppUrl(const QUrl &url)
+{
+    Utility::openBrowser(url);
+}
+
+int UserAppsModel::rowCount(const QModelIndex &parent) const
+{
+    Q_UNUSED(parent);
+    return _apps.count();
+}
+
+QVariant UserAppsModel::data(const QModelIndex &index, int role) const
+{
+    if (index.row() < 0 || index.row() >= _apps.count()) {
+        return QVariant();
+    }
+
+    if (role == NameRole) {
+        return _apps[index.row()]->name();
+    } else if (role == UrlRole) {
+        return _apps[index.row()]->url();
+    } else if (role == IconUrlRole) {
+        return _apps[index.row()]->iconUrl().toString();
+    }
+    return QVariant();
+}
+
+QHash<int, QByteArray> UserAppsModel::roleNames() const
+{
+    QHash<int, QByteArray> roles;
+    roles[NameRole] = "appName";
+    roles[UrlRole] = "appUrl";
+    roles[IconUrlRole] = "appIconUrl";
+    return roles;
+}
+
+}
diff --git a/src/gui/tray/usermodel.h b/src/gui/tray/usermodel.h
new file mode 100644 (file)
index 0000000..5ec34a9
--- /dev/null
@@ -0,0 +1,246 @@
+#ifndef USERMODEL_H
+#define USERMODEL_H
+
+#include <QAbstractListModel>
+#include <QImage>
+#include <QDateTime>
+#include <QStringList>
+#include <QQuickImageProvider>
+#include <QHash>
+
+#include "activitylistmodel.h"
+#include "accountfwd.h"
+#include "accountmanager.h"
+#include "folderman.h"
+#include "notificationcache.h"
+#include "userstatusselectormodel.h"
+#include "userstatusconnector.h"
+#include <chrono>
+
+namespace OCC {
+class UnifiedSearchResultsListModel;
+
+class User : public QObject
+{
+    Q_OBJECT
+    Q_PROPERTY(QString name READ name NOTIFY nameChanged)
+    Q_PROPERTY(QString server READ server CONSTANT)
+    Q_PROPERTY(bool serverHasUserStatus READ serverHasUserStatus CONSTANT)
+    Q_PROPERTY(QUrl statusIcon READ statusIcon NOTIFY statusChanged)
+    Q_PROPERTY(QString statusEmoji READ statusEmoji NOTIFY statusChanged)
+    Q_PROPERTY(QString statusMessage READ statusMessage NOTIFY statusChanged)
+    Q_PROPERTY(bool desktopNotificationsAllowed READ isDesktopNotificationsAllowed NOTIFY desktopNotificationsAllowedChanged)
+    Q_PROPERTY(bool hasLocalFolder READ hasLocalFolder NOTIFY hasLocalFolderChanged)
+    Q_PROPERTY(bool serverHasTalk READ serverHasTalk NOTIFY serverHasTalkChanged)
+    Q_PROPERTY(QString avatar READ avatarUrl NOTIFY avatarChanged)
+    Q_PROPERTY(bool isConnected READ isConnected NOTIFY accountStateChanged)
+    Q_PROPERTY(UnifiedSearchResultsListModel* unifiedSearchResultsListModel READ getUnifiedSearchResultsListModel CONSTANT)
+public:
+    User(AccountStatePtr &account, const bool &isCurrent = false, QObject *parent = nullptr);
+
+    AccountPtr account() const;
+    AccountStatePtr accountState() const;
+
+    bool isConnected() const;
+    bool isCurrentUser() const;
+    void setCurrentUser(const bool &isCurrent);
+    Folder *getFolder() const;
+    ActivityListModel *getActivityModel();
+    UnifiedSearchResultsListModel *getUnifiedSearchResultsListModel() const;
+    void openLocalFolder();
+    QString name() const;
+    QString server(bool shortened = true) const;
+    bool hasLocalFolder() const;
+    bool serverHasTalk() const;
+    bool serverHasUserStatus() const;
+    AccountApp *talkApp() const;
+    bool hasActivities() const;
+    AccountAppList appList() const;
+    QImage avatar() const;
+    void login() const;
+    void logout() const;
+    void removeAccount() const;
+    QString avatarUrl() const;
+    bool isDesktopNotificationsAllowed() const;
+    UserStatus::OnlineStatus status() const;
+    QString statusMessage() const;
+    QUrl statusIcon() const;
+    QString statusEmoji() const;
+    void processCompletedSyncItem(const Folder *folder, const SyncFileItemPtr &item);
+
+signals:
+    void guiLog(const QString &, const QString &);
+    void nameChanged();
+    void hasLocalFolderChanged();
+    void serverHasTalkChanged();
+    void avatarChanged();
+    void accountStateChanged();
+    void statusChanged();
+    void desktopNotificationsAllowedChanged();
+
+public slots:
+    void slotItemCompleted(const QString &folder, const SyncFileItemPtr &item);
+    void slotProgressInfo(const QString &folder, const ProgressInfo &progress);
+    void slotAddError(const QString &folderAlias, const QString &message, ErrorCategory category);
+    void slotAddErrorToGui(const QString &folderAlias, SyncFileItem::Status status, const QString &errorMessage, const QString &subject = {});
+    void slotNotificationRequestFinished(int statusCode);
+    void slotNotifyNetworkError(QNetworkReply *reply);
+    void slotEndNotificationRequest(int replyCode);
+    void slotNotifyServerFinished(const QString &reply, int replyCode);
+    void slotSendNotificationRequest(const QString &accountName, const QString &link, const QByteArray &verb, int row);
+    void slotBuildNotificationDisplay(const ActivityList &list);
+    void slotRefreshNotifications();
+    void slotRefreshActivities();
+    void slotRefresh();
+    void slotRefreshUserStatus();
+    void slotRefreshImmediately();
+    void setNotificationRefreshInterval(std::chrono::milliseconds interval);
+    void slotRebuildNavigationAppList();
+
+private:
+    void slotPushNotificationsReady();
+    void slotDisconnectPushNotifications();
+    void slotReceivedPushNotification(Account *account);
+    void slotReceivedPushActivity(Account *account);
+    void slotCheckExpiredActivities();
+
+    void connectPushNotifications() const;
+    bool checkPushNotificationsAreReady() const;
+
+    bool isActivityOfCurrentAccount(const Folder *folder) const;
+    bool isUnsolvableConflict(const SyncFileItemPtr &item) const;
+
+    void showDesktopNotification(const QString &title, const QString &message);
+
+private:
+    AccountStatePtr _account;
+    bool _isCurrentUser;
+    ActivityListModel *_activityModel;
+    UnifiedSearchResultsListModel *_unifiedSearchResultsModel;
+    ActivityList _blacklistedNotifications;
+
+    QTimer _expiredActivitiesCheckTimer;
+    QTimer _notificationCheckTimer;
+    QHash<AccountState *, QElapsedTimer> _timeSinceLastCheck;
+
+    QElapsedTimer _guiLogTimer;
+    NotificationCache _notificationCache;
+
+    // number of currently running notification requests. If non zero,
+    // no query for notifications is started.
+    int _notificationRequestsRunning;
+};
+
+class UserModel : public QAbstractListModel
+{
+    Q_OBJECT
+    Q_PROPERTY(User* currentUser READ currentUser NOTIFY newUserSelected)
+    Q_PROPERTY(int currentUserId READ currentUserId NOTIFY newUserSelected)
+public:
+    static UserModel *instance();
+    ~UserModel() override = default;
+
+    void addUser(AccountStatePtr &user, const bool &isCurrent = false);
+    int currentUserIndex();
+
+    int rowCount(const QModelIndex &parent = QModelIndex()) const override;
+
+    QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override;
+
+    QImage avatarById(const int &id);
+
+    User *currentUser() const;
+
+    int findUserIdForAccount(AccountState *account) const;
+
+    Q_INVOKABLE void fetchCurrentActivityModel();
+    Q_INVOKABLE void openCurrentAccountLocalFolder();
+    Q_INVOKABLE void openCurrentAccountTalk();
+    Q_INVOKABLE void openCurrentAccountServer();
+    Q_INVOKABLE int numUsers();
+    Q_INVOKABLE QString currentUserServer();
+    int currentUserId() const;
+    Q_INVOKABLE bool isUserConnected(const int &id);
+    Q_INVOKABLE void switchCurrentUser(const int &id);
+    Q_INVOKABLE void login(const int &id);
+    Q_INVOKABLE void logout(const int &id);
+    Q_INVOKABLE void removeAccount(const int &id);
+
+    Q_INVOKABLE std::shared_ptr<OCC::UserStatusConnector> userStatusConnector(int id);
+
+    ActivityListModel *currentActivityModel();
+
+    enum UserRoles {
+        NameRole = Qt::UserRole + 1,
+        ServerRole,
+        ServerHasUserStatusRole,
+        StatusIconRole,
+        StatusEmojiRole,
+        StatusMessageRole,
+        DesktopNotificationsAllowedRole,
+        AvatarRole,
+        IsCurrentUserRole,
+        IsConnectedRole,
+        IdRole
+    };
+
+    AccountAppList appList() const;
+
+signals:
+    Q_INVOKABLE void addAccount();
+    Q_INVOKABLE void newUserSelected();
+
+protected:
+    QHash<int, QByteArray> roleNames() const override;
+
+private:
+    static UserModel *_instance;
+    UserModel(QObject *parent = nullptr);
+    QList<User*> _users;
+    int _currentUserId = 0;
+    bool _init = true;
+
+    void buildUserList();
+};
+
+class ImageProvider : public QQuickImageProvider
+{
+public:
+    ImageProvider();
+    QImage requestImage(const QString &id, QSize *size, const QSize &requestedSize) override;
+};
+
+class UserAppsModel : public QAbstractListModel
+{
+    Q_OBJECT
+public:
+    static UserAppsModel *instance();
+    ~UserAppsModel() override = default;
+
+    int rowCount(const QModelIndex &parent = QModelIndex()) const override;
+
+    QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override;
+
+    enum UserAppsRoles {
+        NameRole = Qt::UserRole + 1,
+        UrlRole,
+        IconUrlRole
+    };
+
+    void buildAppList();
+
+public slots:
+    void openAppUrl(const QUrl &url);
+
+protected:
+    QHash<int, QByteArray> roleNames() const override;
+
+private:
+    static UserAppsModel *_instance;
+    UserAppsModel(QObject *parent = nullptr);
+
+    AccountAppList _apps;
+};
+
+}
+#endif // USERMODEL_H
index 47a8c74a95e6c1606478344b5ed7fc3b955a116e..6ae0dd256f28c886e2a36f4283e1f60f90adbda6 100644 (file)
@@ -13,7 +13,7 @@
  */
 
 #include "userstatusselectormodel.h"
-#include "tray/UserModel.h"
+#include "tray/usermodel.h"
 
 #include <ocsuserstatusconnector.h>
 #include <qnamespace.h>
index 6ee05e3d79834a4a6f04c14ea2bd41f3bd86bf0b..eba7e3f4d52693d97a3c58f23f4069b1cc46bc77 100644 (file)
@@ -1,6 +1,6 @@
 #include <QTest>
 
-#include "tray/NotificationCache.h"
+#include "tray/notificationcache.h"
 
 class TestNotificationCache : public QObject
 {