#include "notificationhandler.h" #include "usermodel.h" #include "accountmanager.h" #include "owncloudgui.h" #include #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 "systray.h" #include "tray/activitylistmodel.h" #include "tray/unifiedsearchresultslistmodel.h" #include "tray/talkreply.h" #include "userstatusconnector.h" #include "thumbnailjob.h" #include #include #include #include #include #include // 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 { TrayFolderInfo::TrayFolderInfo(const QString &name, const QString &parentPath, const QString &fullPath, FolderType folderType) : _name(name) , _parentPath(parentPath) , _fullPath(fullPath) , _folderType(folderType) { } bool TrayFolderInfo::isGroupFolder() const { return _folderType == GroupFolder; } 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)) { 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(_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(_account->account().data(), &Account::capabilitiesChanged, this, &User::headerColorChanged); connect(_account->account().data(), &Account::capabilitiesChanged, this, &User::headerTextColorChanged); connect(_account->account().data(), &Account::capabilitiesChanged, this, &User::accentColorChanged); connect(_account->account().data(), &Account::capabilitiesChanged, this, &User::slotAccountCapabilitiesChangedRefreshGroupFolders); connect(_activityModel, &ActivityListModel::sendNotificationRequest, this, &User::slotSendNotificationRequest); connect(this, &User::sendReplyMessage, this, &User::slotSendReplyMessage); } void User::checkNotifiedNotifications() { // after one hour, clear the gui log notification store constexpr qint64 clearGuiLogInterval = 60 * 60 * 1000; if (_guiLogTimer.elapsed() > clearGuiLogInterval) { _notifiedNotifications.clear(); } } bool User::notificationAlreadyShown(const long notificationId) { checkNotifiedNotifications(); return _notifiedNotifications.contains(notificationId); } bool User::canShowNotification(const long notificationId) { ConfigFile cfg; return cfg.optionalServerNotifications() && isDesktopNotificationsAllowed() && !notificationAlreadyShown(notificationId); } void User::showDesktopNotification(const QString &title, const QString &message, const long notificationId) { if(!canShowNotification(notificationId)) { return; } _notifiedNotifications.insert(notificationId); Logger::instance()->postGuiLog(title, message); // restart the gui log timer now that we show a new notification _guiLogTimer.start(); } void User::showDesktopNotification(const Activity &activity) { const auto notificationId = activity._id; const auto message = AccountManager::instance()->accounts().count() == 1 ? "" : activity._accName; // the user needs to interact with this notification if (activity._links.size() > 0) { _activityModel->addNotificationToActivityList(activity); } showDesktopNotification(activity._subject, message, notificationId); } void User::showDesktopNotification(const ActivityList &activityList) { const auto subject = QStringLiteral("%1 notifications").arg(activityList.count()); const auto notificationId = -static_cast(qHash(subject)); if (!canShowNotification(notificationId)) { return; } const auto multipleAccounts = AccountManager::instance()->accounts().count() > 1; const auto message = multipleAccounts ? activityList.constFirst()._accName : QString(); // Notification ids are uints, which are 4 bytes. Error activities don't have ids, however, so we generate one. // To avoid possible collisions between the activity ids which are actually the notification ids received from // the server (which are always positive) and our "fake" error activity ids, we assign a negative id to the // error notification. // // To ensure that we can still treat an unsigned int as normal, we use a long, which is 8 bytes. Logger::instance()->postGuiLog(subject, message); for(const auto &activity : activityList) { _notifiedNotifications.insert(activity._id); _activityModel->addNotificationToActivityList(activity); } } void User::showDesktopTalkNotification(const Activity &activity) { const auto notificationId = activity._id; if (!canShowNotification(notificationId)) { return; } if (activity._talkNotificationData.messageId.isEmpty()) { showDesktopNotification(activity._subject, activity._message, notificationId); return; } _notifiedNotifications.insert(notificationId); _activityModel->addNotificationToActivityList(activity); Systray::instance()->showTalkMessage(activity._subject, activity._message, activity._talkNotificationData.conversationToken, activity._talkNotificationData.messageId, _account); _guiLogTimer.start(); } void User::slotBuildNotificationDisplay(const ActivityList &list) { ActivityList toNotifyList; std::copy_if(list.constBegin(), list.constEnd(), std::back_inserter(toNotifyList), [&](const Activity &activity) { if (_blacklistedNotifications.contains(activity)) { qCInfo(lcActivity) << "Activity in blacklist, skip"; return false; } else if(_notifiedNotifications.contains(activity._id)) { qCInfo(lcActivity) << "Activity already notified, skip"; return false; } if (!activity._shouldNotify) { qCDebug(lcActivity) << "Activity should not be notified"; return false; } return true; }); if(toNotifyList.count() > 2) { showDesktopNotification(toNotifyList); return; } for(const auto &activity : qAsConst(toNotifyList)) { if (activity._objectType == QStringLiteral("chat")) { showDesktopTalkNotification(activity); } else { showDesktopNotification(activity); } } } void User::slotBuildIncomingCallDialogs(const ActivityList &list) { const ConfigFile cfg; const auto userStatus = _account->account()->userStatusConnector()->userStatus().state(); if (userStatus == OCC::UserStatus::OnlineStatus::DoNotDisturb || !cfg.optionalServerNotifications() || !cfg.showCallNotifications() || !isDesktopNotificationsAllowed()) { return; } const auto systray = Systray::instance(); if(systray) { for(const auto &activity : list) { if (!activity._shouldNotify) { qCDebug(lcActivity) << "Activity should not be notified"; continue; } systray->createCallDialog(activity, _account); } } } 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 occurred, 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() { const auto errorsList = _activityModel->errorsList(); for (const Activity &activity : errorsList) { if (activity._expireAtMsecs > 0 && QDateTime::currentDateTime().toMSecsSinceEpoch() >= activity._expireAtMsecs) { _activityModel->removeActivityFromActivityList(activity); } } if (_activityModel->errorsList().size() == 0) { _expiredActivitiesCheckTimer.stop(); } } void User::parseNewGroupFolderPath(const QString &mountPoint) { if (mountPoint.isEmpty()) { return; } auto mountPointSplit = mountPoint.split(QLatin1Char('/'), Qt::SkipEmptyParts); if (mountPointSplit.isEmpty()) { return; } const auto groupFolderName = mountPointSplit.takeLast(); const auto parentPath = mountPointSplit.join(QLatin1Char('/')); _trayFolderInfos.push_back(QVariant::fromValue(TrayFolderInfo{groupFolderName, parentPath, mountPoint, TrayFolderInfo::GroupFolder})); } void User::prePendGroupFoldersWithLocalFolder() { if (!_trayFolderInfos.isEmpty() && !_trayFolderInfos.first().value().isGroupFolder()) { return; } const auto localFolderName = getFolder()->shortGuiLocalPath(); auto localFolderPathSplit = getFolder()->path().split(QLatin1Char('/'), Qt::SkipEmptyParts); if (!localFolderPathSplit.isEmpty()) { localFolderPathSplit.removeLast(); } const auto localFolderParentPath = !localFolderPathSplit.isEmpty() ? localFolderPathSplit.join(QLatin1Char('/')) : "/"; _trayFolderInfos.push_front(QVariant::fromValue(TrayFolderInfo{localFolderName, localFolderParentPath, getFolder()->path(), TrayFolderInfo::Folder})); } 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() && Systray::instance()->isOpen()) { slotRefreshActivities(); } slotRefreshNotifications(); } void User::slotRefresh() { slotRefreshUserStatus(); if (checkPushNotificationsAreReady()) { // we are relying on WebSocket push notifications - ignore refresh attempts from UI slotRefreshActivitiesInitial(); _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()) { slotRefreshActivitiesInitial(); slotRefreshNotifications(); timer.start(); } } void User::slotRefreshActivitiesInitial() { if (_account.data()->isConnected() && Systray::instance()->isOpen()) { _activityModel->slotRefreshActivityInitial(); } } void User::slotRefreshActivities() { if (_account.data()->isConnected() && Systray::instance()->isOpen()) { _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); connect(snh, &ServerNotificationHandler::newIncomingCallsList, this, &User::slotBuildIncomingCallDialogs); 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 or 202 inside the xml if it succeeded. if (statusCode != OCS_SUCCESS_STATUS_CODE && statusCode != OCS_SUCCESS_STATUS_CODE_V2 && statusCode != OCS_ACCEPTED_STATUS_CODE) { 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 succeeded, 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(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(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._syncFileItemStatus == SyncFileItem::Conflict && !QFileInfo::exists(f->path() + activity._file)) { _activityModel->removeActivityFromActivityList(activity); continue; } if (activity._syncFileItemStatus == SyncFileItem::FileLocked && !QFileInfo::exists(f->path() + activity._file)) { _activityModel->removeActivityFromActivityList(activity); continue; } if (activity._syncFileItemStatus == SyncFileItem::FileIgnored && !QFileInfo::exists(f->path() + activity._file)) { _activityModel->removeActivityFromActivityList(activity); continue; } if (!QFileInfo::exists(f->path() + activity._file)) { _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._syncFileItemStatus == 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._syncResultStatus = 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); } auto errorType = ActivityListModel::ErrorType::SyncError; // add 'other errors' to activity list switch (category) { case ErrorCategory::GenericError: errorType = ActivityListModel::ErrorType::SyncError; break; case ErrorCategory::InsufficientRemoteStorage: errorType = ActivityListModel::ErrorType::SyncError; break; case ErrorCategory::NetworkError: errorType = ActivityListModel::ErrorType::NetworkError; break; case ErrorCategory::NoError: break; } _activityModel->addErrorToActivityList(activity, errorType); } } void User::slotAddErrorToGui(const QString &folderAlias, const SyncFileItem::Status status, const QString &errorMessage, const QString &subject, const ErrorCategory category) { 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._syncFileItemStatus = 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; if (status == SyncFileItem::Conflict || status == SyncFileItem::FileNameClash) { ActivityLink buttonActivityLink; buttonActivityLink._label = tr("Resolve conflict"); buttonActivityLink._link = activity._link.toString(); buttonActivityLink._verb = "FIX_CONFLICT_LOCALLY"; buttonActivityLink._primary = true; activity._links = {buttonActivityLink}; } // Error notifications don't have ids by themselves so we will create one for it activity._id = -static_cast(qHash(activity._subject + activity._message)); // add 'other errors' to activity list auto errorType = ActivityListModel::ErrorType::SyncError; switch (category) { case ErrorCategory::GenericError: errorType = ActivityListModel::ErrorType::SyncError; break; case ErrorCategory::InsufficientRemoteStorage: errorType = ActivityListModel::ErrorType::SyncError; break; case ErrorCategory::NetworkError: errorType = ActivityListModel::ErrorType::NetworkError; break; case ErrorCategory::NoError: errorType = {}; break; } _activityModel->addErrorToActivityList(activity, errorType); showDesktopNotification(activity); 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) { const auto fileActionFromInstruction = [](const int instruction) { if (instruction == CSYNC_INSTRUCTION_REMOVE) { return QStringLiteral("file_deleted"); } else if (instruction == CSYNC_INSTRUCTION_NEW) { return QStringLiteral("file_created"); } else if (instruction == CSYNC_INSTRUCTION_RENAME) { return QStringLiteral("file_renamed"); } else { return QStringLiteral("file_changed"); } }; const auto messageFromFileAction = [](const QString &fileAction, const QString &fileName) { if (fileAction == QStringLiteral("file_renamed")) { return QObject::tr("You renamed %1").arg(fileName); } else if (fileAction == QStringLiteral("file_deleted")) { return QObject:: tr("You deleted %1").arg(fileName); } else if (fileAction == QStringLiteral("file_created")) { return QObject::tr("You created %1").arg(fileName); } else { return QObject::tr("You changed %1").arg(fileName); } }; Activity activity; activity._type = Activity::SyncFileItemType; //client activity activity._objectType = QStringLiteral("files"); activity._syncFileItemStatus = item->_status; activity._dateTime = QDateTime::currentDateTime(); activity._message = item->_originalFile; activity._link = account()->url(); activity._accName = account()->displayName(); activity._file = item->_file; activity._folder = folder->alias(); activity._fileAction = ""; const auto fileName = QFileInfo(item->_originalFile).fileName(); activity._fileAction = fileActionFromInstruction(item->_instruction); if (item->_status == SyncFileItem::NoStatus || item->_status == SyncFileItem::Success) { qCWarning(lcActivity) << "Item " << item->_file << " retrieved successfully."; if (item->_direction != SyncFileItem::Up) { activity._message = QObject::tr("Synced %1").arg(fileName); } else { activity._message = messageFromFileAction(activity._fileAction, fileName); } if(activity._fileAction != "file_deleted" && !item->isEmpty()) { const auto localFiles = FolderMan::instance()->findFileInLocalFolders(item->_file, account()); if (!localFiles.isEmpty()) { const auto firstFilePath = localFiles.constFirst(); const auto itemJournalRecord = item->toSyncJournalFileRecordWithInode(firstFilePath); if(!itemJournalRecord.isVirtualFile()) { const auto mimeType = _mimeDb.mimeTypeForFile(QFileInfo(localFiles.constFirst())); // Set the preview data, though for now we can skip setting file ID, link, and view PreviewData preview; preview._mimeType = mimeType.name(); preview._filename = fileName; preview._isMimeTypeIcon = true; if(item->isDirectory()) { preview._source = account()->url().toString() + QStringLiteral("/index.php/apps/theming/img/core/filetypes/folder.svg"); } else { preview._source = account()->url().toString() + Activity::relativeServerFileTypeIconPath(mimeType); } activity._previews.append(preview); } } } _activityModel->addSyncFileItemToActivityList(activity); } else { qCWarning(lcActivity) << "Item " << item->_file << " retrieved resulted in error " << item->_errorString; activity._subject = item->_errorString; activity._id = -static_cast(qHash(activity._subject + activity._message)); 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, activity._id); } else if (item->_status == SyncFileItem::Conflict || item->_status == SyncFileItem::FileNameClash) { ActivityLink buttonActivityLink; buttonActivityLink._label = tr("Resolve conflict"); buttonActivityLink._link = activity._link.toString(); buttonActivityLink._verb = "FIX_CONFLICT_LOCALLY"; buttonActivityLink._primary = true; activity._links = {buttonActivityLink}; } _activityModel->addErrorToActivityList(activity, ActivityListModel::ErrorType::SyncError); } } } const QVariantList &User::groupFolders() const { return _trayFolderInfos; } 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::openFolderLocallyOrInBrowser(const QString &fullRemotePath) { const auto folder = getFolder(); if (!folder) { return; } // remove remote path prefix and leading slash auto fullRemotePathToPathInDb = folder->remotePath() != QStringLiteral("/") ? fullRemotePath.mid(folder->remotePathTrailingSlash().size()) : fullRemotePath; if (fullRemotePathToPathInDb.startsWith("/")) { fullRemotePathToPathInDb = fullRemotePathToPathInDb.mid(1); } SyncJournalFileRecord rec; if (folder->journalDb()->getFileRecord(fullRemotePathToPathInDb, &rec) && rec.isValid()) { // found folder locally, going to open qCInfo(lcActivity) << "Opening locally a folder" << fullRemotePath; QDesktopServices::openUrl(QUrl::fromLocalFile(folder->path() + rec.path())); return; } // try to open it in browser auto folderUrlForBrowser = Utility::concatUrlPath(_account->account()->url(), QStringLiteral("/index.php/apps/files/")); QUrlQuery urlQuery; urlQuery.addQueryItem(QStringLiteral("dir"), fullRemotePath); folderUrlForBrowser.setQuery(urlQuery); if (!folderUrlForBrowser.scheme().startsWith(QStringLiteral("http"))) { folderUrlForBrowser.setScheme(QStringLiteral("https")); } // open https://server.com/index.php/apps/files/?dir=/group_folder/path qCInfo(lcActivity) << "Opening in browser a folder" << fullRemotePath; Utility::openBrowser(folderUrlForBrowser); return; } void User::login() const { _account->account()->resetRejectedCertificates(); _account->signIn(); } void User::logout() const { _account->signOutByUi(); } QString User::name() const { return _account->account()->prettyName(); } 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().userStatus(); } 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(); } QColor User::headerColor() const { return _account->account()->headerColor(); } QColor User::headerTextColor() const { return _account->account()->headerTextColor(); } QColor User::accentColor() const { return _account->account()->accentColor(); } 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(); } void User::slotSendReplyMessage(const int activityIndex, const QString &token, const QString &message, const QString &replyTo) { QPointer talkReply = new TalkReply(_account.data(), this); talkReply->sendReplyMessage(token, message, replyTo); connect(talkReply, &TalkReply::replyMessageSent, this, [&, activityIndex](const QString &message) { _activityModel->setReplyMessageSent(activityIndex, message); }); } void User::forceSyncNow() const { FolderMan::instance()->forceSyncForFolder(getFolder()); } void User::slotAccountCapabilitiesChangedRefreshGroupFolders() { if (!_account->account()->capabilities().groupFoldersAvailable()) { if (!_trayFolderInfos.isEmpty()) { _trayFolderInfos.clear(); emit groupFoldersChanged(); } return; } slotFetchGroupFolders(); } void User::slotFetchGroupFolders() { QNetworkRequest req; req.setRawHeader(QByteArrayLiteral("OCS-APIREQUEST"), QByteArrayLiteral("true")); QUrlQuery query; query.addQueryItem(QLatin1String("format"), QLatin1String("json")); query.addQueryItem(QLatin1String("applicable"), QLatin1String("1")); QUrl groupFolderListUrl = Utility::concatUrlPath(_account->account()->url(), QStringLiteral("/index.php/apps/groupfolders/folders")); groupFolderListUrl.setQuery(query); const auto groupFolderListJob = _account->account()->sendRequest(QByteArrayLiteral("GET"), groupFolderListUrl, req); connect(groupFolderListJob, &SimpleNetworkJob::finishedSignal, this, &User::slotGroupFoldersFetched); } void User::slotGroupFoldersFetched(QNetworkReply *reply) { Q_ASSERT(reply); if (!reply) { qCWarning(lcActivity) << "Group folders fetch error"; return; } const auto oldSize = _trayFolderInfos.size(); const auto oldTrayFolderInfos = _trayFolderInfos; _trayFolderInfos.clear(); const auto replyData = reply->readAll(); if (reply->error() != QNetworkReply::NoError) { if (oldSize != _trayFolderInfos.size()) { emit groupFoldersChanged(); } qCWarning(lcActivity) << "Group folders fetch error" << reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt() << replyData; return; } QJsonParseError jsonParseError{}; const auto json = QJsonDocument::fromJson(replyData, &jsonParseError); if (jsonParseError.error != QJsonParseError::NoError) { qCWarning(lcActivity) << "Group folders JSON parse error" << jsonParseError.error << jsonParseError.errorString(); if (oldSize != _trayFolderInfos.size()) { emit groupFoldersChanged(); } return; } const auto obj = json.object().toVariantMap(); const auto groupFolders = obj["ocs"].toMap()["data"].toMap(); for (const auto &groupFolder : groupFolders.values()) { const auto groupFolderInfo = groupFolder.toMap(); const auto mountPoint = groupFolderInfo.value(QStringLiteral("mount_point"), {}).toString(); parseNewGroupFolderPath(mountPoint); } std::sort(std::begin(_trayFolderInfos), std::end(_trayFolderInfos), [](const auto &leftVariant, const auto &rightVariant) { const auto folderInfoA = leftVariant.template value(); const auto folderInfoB = rightVariant.template value(); return folderInfoA._fullPath < folderInfoB._fullPath; }); if (!_trayFolderInfos.isEmpty()) { if (hasLocalFolder()) { prePendGroupFoldersWithLocalFolder(); } } if (oldSize != _trayFolderInfos.size()) { emit groupFoldersChanged(); } else { for (int i = 0; i < oldTrayFolderInfos.size(); ++i) { const auto oldFolderInfo = oldTrayFolderInfos.at(i).template value(); const auto newFolderInfo = _trayFolderInfos.at(i).template value(); if (oldFolderInfo._folderType != newFolderInfo._folderType || oldFolderInfo._fullPath != newFolderInfo._fullPath) { break; emit groupFoldersChanged(); } } } } /*-------------------------------------------------------------------------------------*/ 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; } } int UserModel::numUsers() { return _users.size(); } int UserModel::currentUserId() const { return _currentUserId; } bool UserModel::isUserConnected(const int id) { if (id < 0 || id >= _users.size()) return false; return _users[id]->isConnected(); } QImage UserModel::avatarById(const int id) const { const auto foundUserByIdIter = std::find_if(std::cbegin(_users), std::cend(_users), [&id](const OCC::User* const user) { return user->account()->id() == QString::number(id); }); if (foundUserByIdIter == std::cend(_users)) { return {}; } return (*foundUserByIdIter)->avatar(); } 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 < 0) { setCurrentUserId(_users.size() - 1); } endInsertRows(); ConfigFile cfg; u->setNotificationRefreshInterval(cfg.notificationRefreshInterval()); emit currentUserChanged(); } } int UserModel::currentUserIndex() { return _currentUserId; } void UserModel::openCurrentAccountLocalFolder() { if (_currentUserId < 0 || _currentUserId >= _users.size()) return; _users[_currentUserId]->openLocalFolder(); } 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(); } } 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); } void UserModel::openCurrentAccountFolderFromTrayInfo(const QString &fullRemotePath) { if (_currentUserId < 0 || _currentUserId >= _users.size()) { return; } _users[_currentUserId]->openFolderLocallyOrInBrowser(fullRemotePath); } void UserModel::setCurrentUserId(const int id) { Q_ASSERT(id < _users.size()); if (id < 0 || id >= _users.size()) { if (id < 0 && _currentUserId != id) { _currentUserId = id; emit currentUserChanged(); } return; } const auto isCurrentUserChanged = !_users[id]->isCurrentUser(); if (isCurrentUserChanged) { for (const auto user : qAsConst(_users)) { user->setCurrentUser(false); } _users[id]->setCurrentUser(true); } if (_currentUserId == id && isCurrentUserChanged) { // order has changed, index remained the same emit currentUserChanged(); } else if (_currentUserId != id) { _currentUserId = id; emit currentUserChanged(); } } void UserModel::login(const int id) { if (id < 0 || id >= _users.size()) return; _users[id]->login(); } void UserModel::logout(const int id) { if (id < 0 || id >= _users.size()) return; _users[id]->logout(); } void UserModel::removeAccount(const int id) { if (id < 0 || id >= _users.size()) { return; } QMessageBox messageBox(QMessageBox::Question, tr("Confirm Account Removal"), tr("

Do you really want to remove the connection to the account %1?

" "

Note: This will not delete any files.

") .arg(_users[id]->name()), QMessageBox::NoButton); const auto * const yesButton = messageBox.addButton(tr("Remove connection"), QMessageBox::YesRole); messageBox.addButton(tr("Cancel"), QMessageBox::NoRole); messageBox.exec(); if (messageBox.clickedButton() != yesButton) { return; } _users[id]->logout(); _users[id]->removeAccount(); beginRemoveRows(QModelIndex(), id, id); _users.removeAt(id); endRemoveRows(); if (_users.size() <= 1) { setCurrentUserId(_users.size() - 1); } else if (currentUserId() > id) { // an account was removed from the in-between 0 and the current one, the index of the current one needs a decrement setCurrentUserId(currentUserId() - 1); } else if (currentUserId() == id) { setCurrentUserId(id < _users.size() ? id : id - 1); } } std::shared_ptr 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 UserModel::roleNames() const { QHash 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 UserAppsModel::roleNames() const { QHash roles; roles[NameRole] = "appName"; roles[UrlRole] = "appUrl"; roles[IconUrlRole] = "appIconUrl"; return roles; } }