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
#include "fileactivitylistmodel.h"
#include "folderman.h"
-#include "tray/ActivityListModel.h"
+#include "tray/activitylistmodel.h"
namespace OCC {
#pragma once
#include "accountstate.h"
-#include "tray/ActivityListModel.h"
+#include "tray/activitylistmodel.h"
namespace OCC {
#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"
#include <QSystemTrayIcon>
#include "accountmanager.h"
-#include "tray/UserModel.h"
+#include "tray/usermodel.h"
class QScreen;
class QQmlApplicationEngine;
+++ /dev/null
-/*
- * 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);
-}
-}
+++ /dev/null
-/*
- * 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
+++ /dev/null
-/*
- * 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;
-}
-}
+++ /dev/null
-/*
- * 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
+++ /dev/null
-#include "NotificationCache.h"
-
-namespace OCC {
-
-bool NotificationCache::contains(const Notification ¬ification) const
-{
- return _notifications.find(calculateKey(notification)) != _notifications.end();
-}
-
-void NotificationCache::insert(const Notification ¬ification)
-{
- _notifications.insert(calculateKey(notification));
-}
-
-void NotificationCache::clear()
-{
- _notifications.clear();
-}
-
-uint NotificationCache::calculateKey(const Notification ¬ification) const
-{
- return qHash(notification.title + notification.message);
-}
-}
+++ /dev/null
-#pragma once
-
-#include <QSet>
-
-namespace OCC {
-
-class NotificationCache
-{
-public:
- struct Notification
- {
- QString title;
- QString message;
- };
-
- bool contains(const Notification ¬ification) const;
-
- void insert(const Notification ¬ification);
-
- void clear();
-
-private:
- uint calculateKey(const Notification ¬ification) const;
-
-
- QSet<uint> _notifications;
-};
-}
+++ /dev/null
-#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();
-}
-}
+++ /dev/null
-#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
+++ /dev/null
-#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;
-}
-
-}
+++ /dev/null
-#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
--- /dev/null
+/*
+ * 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);
+}
+}
--- /dev/null
+/*
+ * 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
--- /dev/null
+/*
+ * 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;
+}
+}
--- /dev/null
+/*
+ * 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
--- /dev/null
+#include "notificationcache.h"
+
+namespace OCC {
+
+bool NotificationCache::contains(const Notification ¬ification) const
+{
+ return _notifications.find(calculateKey(notification)) != _notifications.end();
+}
+
+void NotificationCache::insert(const Notification ¬ification)
+{
+ _notifications.insert(calculateKey(notification));
+}
+
+void NotificationCache::clear()
+{
+ _notifications.clear();
+}
+
+uint NotificationCache::calculateKey(const Notification ¬ification) const
+{
+ return qHash(notification.title + notification.message);
+}
+}
--- /dev/null
+#pragma once
+
+#include <QSet>
+
+namespace OCC {
+
+class NotificationCache
+{
+public:
+ struct Notification
+ {
+ QString title;
+ QString message;
+ };
+
+ bool contains(const Notification ¬ification) const;
+
+ void insert(const Notification ¬ification);
+
+ void clear();
+
+private:
+ uint calculateKey(const Notification ¬ification) const;
+
+
+ QSet<uint> _notifications;
+};
+}
--- /dev/null
+#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();
+}
+}
--- /dev/null
+#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
#include "navigationpanehelper.h"
#include "networkjobs.h"
#include "syncresult.h"
-#include "tray/UserModel.h"
+#include "tray/usermodel.h"
#include <theme.h>
#include "unifiedsearchresultimageprovider.h"
-#include "UserModel.h"
+#include "usermodel.h"
#include <QImage>
#include <QPainter>
--- /dev/null
+#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;
+}
+
+}
--- /dev/null
+#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
*/
#include "userstatusselectormodel.h"
-#include "tray/UserModel.h"
+#include "tray/usermodel.h"
#include <ocsuserstatusconnector.h>
#include <qnamespace.h>
#include <QTest>
-#include "tray/NotificationCache.h"
+#include "tray/notificationcache.h"
class TestNotificationCache : public QObject
{