1 #include "notificationhandler.h"
2 #include "usermodel.h"
3 
4 #include "accountmanager.h"
5 #include "owncloudgui.h"
6 #include <pushnotifications.h>
7 #include "userstatusselectormodel.h"
8 #include "syncengine.h"
9 #include "ocsjob.h"
10 #include "configfile.h"
11 #include "notificationconfirmjob.h"
12 #include "logger.h"
13 #include "guiutility.h"
14 #include "syncfileitem.h"
15 #include "tray/activitylistmodel.h"
16 #include "tray/notificationcache.h"
17 #include "tray/unifiedsearchresultslistmodel.h"
18 #include "userstatusconnector.h"
19 
20 #include <QDesktopServices>
21 #include <QIcon>
22 #include <QMessageBox>
23 #include <QSvgRenderer>
24 #include <QPainter>
25 #include <QPushButton>
26 
27 // time span in milliseconds which has to be between two
28 // refreshes of the notifications
29 #define NOTIFICATION_REQUEST_FREE_PERIOD 15000
30 
31 namespace {
32 constexpr qint64 expiredActivitiesCheckIntervalMsecs = 1000 * 60;
33 constexpr qint64 activityDefaultExpirationTimeMsecs = 1000 * 60 * 10;
34 }
35 
36 namespace OCC {
37 
User(AccountStatePtr & account,const bool & isCurrent,QObject * parent)38 User::User(AccountStatePtr &account, const bool &isCurrent, QObject *parent)
39     : QObject(parent)
40     , _account(account)
41     , _isCurrentUser(isCurrent)
42     , _activityModel(new ActivityListModel(_account.data(), this))
43     , _unifiedSearchResultsModel(new UnifiedSearchResultsListModel(_account.data(), this))
44     , _notificationRequestsRunning(0)
45 {
46     connect(ProgressDispatcher::instance(), &ProgressDispatcher::progressInfo,
47         this, &User::slotProgressInfo);
48     connect(ProgressDispatcher::instance(), &ProgressDispatcher::itemCompleted,
49         this, &User::slotItemCompleted);
50     connect(ProgressDispatcher::instance(), &ProgressDispatcher::syncError,
51         this, &User::slotAddError);
52     connect(ProgressDispatcher::instance(), &ProgressDispatcher::addErrorToGui,
53         this, &User::slotAddErrorToGui);
54 
55     connect(&_notificationCheckTimer, &QTimer::timeout,
56         this, &User::slotRefresh);
57 
58     connect(&_expiredActivitiesCheckTimer, &QTimer::timeout,
59         this, &User::slotCheckExpiredActivities);
60 
61     connect(_account.data(), &AccountState::stateChanged,
62             [=]() { if (isConnected()) {slotRefreshImmediately();} });
63     connect(_account.data(), &AccountState::stateChanged, this, &User::accountStateChanged);
64     connect(_account.data(), &AccountState::hasFetchedNavigationApps,
65         this, &User::slotRebuildNavigationAppList);
66     connect(_account->account().data(), &Account::accountChangedDisplayName, this, &User::nameChanged);
67 
68     connect(FolderMan::instance(), &FolderMan::folderListChanged, this, &User::hasLocalFolderChanged);
69 
70     connect(this, &User::guiLog, Logger::instance(), &Logger::guiLog);
71 
72     connect(_account->account().data(), &Account::accountChangedAvatar, this, &User::avatarChanged);
73     connect(_account->account().data(), &Account::userStatusChanged, this, &User::statusChanged);
74     connect(_account.data(), &AccountState::desktopNotificationsAllowedChanged, this, &User::desktopNotificationsAllowedChanged);
75 
76     connect(_activityModel, &ActivityListModel::sendNotificationRequest, this, &User::slotSendNotificationRequest);
77 }
78 
showDesktopNotification(const QString & title,const QString & message)79 void User::showDesktopNotification(const QString &title, const QString &message)
80 {
81     ConfigFile cfg;
82     if (!cfg.optionalServerNotifications() || !isDesktopNotificationsAllowed()) {
83         return;
84     }
85 
86     // after one hour, clear the gui log notification store
87     constexpr qint64 clearGuiLogInterval = 60 * 60 * 1000;
88     if (_guiLogTimer.elapsed() > clearGuiLogInterval) {
89         _notificationCache.clear();
90     }
91 
92     const NotificationCache::Notification notification { title, message };
93     if (_notificationCache.contains(notification)) {
94         return;
95     }
96 
97     _notificationCache.insert(notification);
98     emit guiLog(notification.title, notification.message);
99     // restart the gui log timer now that we show a new notification
100     _guiLogTimer.start();
101 }
102 
slotBuildNotificationDisplay(const ActivityList & list)103 void User::slotBuildNotificationDisplay(const ActivityList &list)
104 {
105     _activityModel->clearNotifications();
106 
107     foreach (auto activity, list) {
108         if (_blacklistedNotifications.contains(activity)) {
109             qCInfo(lcActivity) << "Activity in blacklist, skip";
110             continue;
111         }
112         const auto message = AccountManager::instance()->accounts().count() == 1 ? "" : activity._accName;
113         showDesktopNotification(activity._subject, message);
114         _activityModel->addNotificationToActivityList(activity);
115     }
116 }
117 
setNotificationRefreshInterval(std::chrono::milliseconds interval)118 void User::setNotificationRefreshInterval(std::chrono::milliseconds interval)
119 {
120     if (!checkPushNotificationsAreReady()) {
121         qCDebug(lcActivity) << "Starting Notification refresh timer with " << interval.count() / 1000 << " sec interval";
122         _notificationCheckTimer.start(interval.count());
123     }
124 }
125 
slotPushNotificationsReady()126 void User::slotPushNotificationsReady()
127 {
128     qCInfo(lcActivity) << "Push notifications are ready";
129 
130     if (_notificationCheckTimer.isActive()) {
131         // as we are now able to use push notifications - let's stop the polling timer
132         _notificationCheckTimer.stop();
133     }
134 
135     connectPushNotifications();
136 }
137 
slotDisconnectPushNotifications()138 void User::slotDisconnectPushNotifications()
139 {
140     disconnect(_account->account()->pushNotifications(), &PushNotifications::notificationsChanged, this, &User::slotReceivedPushNotification);
141     disconnect(_account->account()->pushNotifications(), &PushNotifications::activitiesChanged, this, &User::slotReceivedPushActivity);
142 
143     disconnect(_account->account().data(), &Account::pushNotificationsDisabled, this, &User::slotDisconnectPushNotifications);
144 
145     // 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
146     setNotificationRefreshInterval(ConfigFile().notificationRefreshInterval());
147 }
148 
slotReceivedPushNotification(Account * account)149 void User::slotReceivedPushNotification(Account *account)
150 {
151     if (account->id() == _account->account()->id()) {
152         slotRefreshNotifications();
153     }
154 }
155 
slotReceivedPushActivity(Account * account)156 void User::slotReceivedPushActivity(Account *account)
157 {
158     if (account->id() == _account->account()->id()) {
159         slotRefreshActivities();
160     }
161 }
162 
slotCheckExpiredActivities()163 void User::slotCheckExpiredActivities()
164 {
165     for (const Activity &activity : _activityModel->errorsList()) {
166         if (activity._expireAtMsecs > 0 && QDateTime::currentDateTime().toMSecsSinceEpoch() >= activity._expireAtMsecs) {
167             _activityModel->removeActivityFromActivityList(activity);
168         }
169     }
170 
171     if (_activityModel->errorsList().size() == 0) {
172         _expiredActivitiesCheckTimer.stop();
173     }
174 }
175 
connectPushNotifications() const176 void User::connectPushNotifications() const
177 {
178     connect(_account->account().data(), &Account::pushNotificationsDisabled, this, &User::slotDisconnectPushNotifications, Qt::UniqueConnection);
179 
180     connect(_account->account()->pushNotifications(), &PushNotifications::notificationsChanged, this, &User::slotReceivedPushNotification, Qt::UniqueConnection);
181     connect(_account->account()->pushNotifications(), &PushNotifications::activitiesChanged, this, &User::slotReceivedPushActivity, Qt::UniqueConnection);
182 }
183 
checkPushNotificationsAreReady() const184 bool User::checkPushNotificationsAreReady() const
185 {
186     const auto pushNotifications = _account->account()->pushNotifications();
187 
188     const auto pushActivitiesAvailable = _account->account()->capabilities().availablePushNotifications() & PushNotificationType::Activities;
189     const auto pushNotificationsAvailable = _account->account()->capabilities().availablePushNotifications() & PushNotificationType::Notifications;
190 
191     const auto pushActivitiesAndNotificationsAvailable = pushActivitiesAvailable && pushNotificationsAvailable;
192 
193     if (pushActivitiesAndNotificationsAvailable && pushNotifications && pushNotifications->isReady()) {
194         connectPushNotifications();
195         return true;
196     } else {
197         connect(_account->account().data(), &Account::pushNotificationsReady, this, &User::slotPushNotificationsReady, Qt::UniqueConnection);
198         return false;
199     }
200 }
201 
slotRefreshImmediately()202 void User::slotRefreshImmediately() {
203     if (_account.data() && _account.data()->isConnected()) {
204         slotRefreshActivities();
205     }
206     slotRefreshNotifications();
207 }
208 
slotRefresh()209 void User::slotRefresh()
210 {
211     slotRefreshUserStatus();
212 
213     if (checkPushNotificationsAreReady()) {
214         // we are relying on WebSocket push notifications - ignore refresh attempts from UI
215         _timeSinceLastCheck[_account.data()].invalidate();
216         return;
217     }
218 
219     // QElapsedTimer isn't actually constructed as invalid.
220     if (!_timeSinceLastCheck.contains(_account.data())) {
221         _timeSinceLastCheck[_account.data()].invalidate();
222     }
223     QElapsedTimer &timer = _timeSinceLastCheck[_account.data()];
224 
225     // Fetch Activities only if visible and if last check is longer than 15 secs ago
226     if (timer.isValid() && timer.elapsed() < NOTIFICATION_REQUEST_FREE_PERIOD) {
227         qCDebug(lcActivity) << "Do not check as last check is only secs ago: " << timer.elapsed() / 1000;
228         return;
229     }
230     if (_account.data() && _account.data()->isConnected()) {
231         if (!timer.isValid()) {
232             slotRefreshActivities();
233         }
234         slotRefreshNotifications();
235         timer.start();
236     }
237 }
238 
slotRefreshActivities()239 void User::slotRefreshActivities()
240 {
241     _activityModel->slotRefreshActivity();
242 }
243 
slotRefreshUserStatus()244 void User::slotRefreshUserStatus()
245 {
246     if (_account.data() && _account.data()->isConnected()) {
247         _account->account()->userStatusConnector()->fetchUserStatus();
248     }
249 }
250 
slotRefreshNotifications()251 void User::slotRefreshNotifications()
252 {
253     // start a server notification handler if no notification requests
254     // are running
255     if (_notificationRequestsRunning == 0) {
256         auto *snh = new ServerNotificationHandler(_account.data());
257         connect(snh, &ServerNotificationHandler::newNotificationList,
258             this, &User::slotBuildNotificationDisplay);
259 
260         snh->slotFetchNotifications();
261     } else {
262         qCWarning(lcActivity) << "Notification request counter not zero.";
263     }
264 }
265 
slotRebuildNavigationAppList()266 void User::slotRebuildNavigationAppList()
267 {
268     emit serverHasTalkChanged();
269     // Rebuild App list
270     UserAppsModel::instance()->buildAppList();
271 }
272 
slotNotificationRequestFinished(int statusCode)273 void User::slotNotificationRequestFinished(int statusCode)
274 {
275     int row = sender()->property("activityRow").toInt();
276 
277     // the ocs API returns stat code 100 or 200 inside the xml if it succeeded.
278     if (statusCode != OCS_SUCCESS_STATUS_CODE && statusCode != OCS_SUCCESS_STATUS_CODE_V2) {
279         qCWarning(lcActivity) << "Notification Request to Server failed, leave notification visible.";
280     } else {
281         // to do use the model to rebuild the list or remove the item
282         qCWarning(lcActivity) << "Notification Request to Server successed, rebuilding list.";
283         _activityModel->removeActivityFromActivityList(row);
284     }
285 }
286 
slotEndNotificationRequest(int replyCode)287 void User::slotEndNotificationRequest(int replyCode)
288 {
289     _notificationRequestsRunning--;
290     slotNotificationRequestFinished(replyCode);
291 }
292 
slotSendNotificationRequest(const QString & accountName,const QString & link,const QByteArray & verb,int row)293 void User::slotSendNotificationRequest(const QString &accountName, const QString &link, const QByteArray &verb, int row)
294 {
295     qCInfo(lcActivity) << "Server Notification Request " << verb << link << "on account" << accountName;
296 
297     const QStringList validVerbs = QStringList() << "GET"
298                                                  << "PUT"
299                                                  << "POST"
300                                                  << "DELETE";
301 
302     if (validVerbs.contains(verb)) {
303         AccountStatePtr acc = AccountManager::instance()->account(accountName);
304         if (acc) {
305             auto *job = new NotificationConfirmJob(acc->account());
306             QUrl l(link);
307             job->setLinkAndVerb(l, verb);
308             job->setProperty("activityRow", QVariant::fromValue(row));
309             connect(job, &AbstractNetworkJob::networkError,
310                 this, &User::slotNotifyNetworkError);
311             connect(job, &NotificationConfirmJob::jobFinished,
312                 this, &User::slotNotifyServerFinished);
313             job->start();
314 
315             // count the number of running notification requests. If this member var
316             // is larger than zero, no new fetching of notifications is started
317             _notificationRequestsRunning++;
318         }
319     } else {
320         qCWarning(lcActivity) << "Notification Links: Invalid verb:" << verb;
321     }
322 }
323 
slotNotifyNetworkError(QNetworkReply * reply)324 void User::slotNotifyNetworkError(QNetworkReply *reply)
325 {
326     auto *job = qobject_cast<NotificationConfirmJob *>(sender());
327     if (!job) {
328         return;
329     }
330 
331     int resultCode = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt();
332 
333     slotEndNotificationRequest(resultCode);
334     qCWarning(lcActivity) << "Server notify job failed with code " << resultCode;
335 }
336 
slotNotifyServerFinished(const QString & reply,int replyCode)337 void User::slotNotifyServerFinished(const QString &reply, int replyCode)
338 {
339     auto *job = qobject_cast<NotificationConfirmJob *>(sender());
340     if (!job) {
341         return;
342     }
343 
344     slotEndNotificationRequest(replyCode);
345     qCInfo(lcActivity) << "Server Notification reply code" << replyCode << reply;
346 }
347 
slotProgressInfo(const QString & folder,const ProgressInfo & progress)348 void User::slotProgressInfo(const QString &folder, const ProgressInfo &progress)
349 {
350     if (progress.status() == ProgressInfo::Reconcile) {
351         // Wipe all non-persistent entries - as well as the persistent ones
352         // in cases where a local discovery was done.
353         auto f = FolderMan::instance()->folder(folder);
354         if (!f)
355             return;
356         const auto &engine = f->syncEngine();
357         const auto style = engine.lastLocalDiscoveryStyle();
358         foreach (Activity activity, _activityModel->errorsList()) {
359             if (activity._expireAtMsecs != -1) {
360                 // we process expired activities in a different slot
361                 continue;
362             }
363             if (activity._folder != folder) {
364                 continue;
365             }
366 
367             if (style == LocalDiscoveryStyle::FilesystemOnly) {
368                 _activityModel->removeActivityFromActivityList(activity);
369                 continue;
370             }
371 
372             if (activity._status == SyncFileItem::Conflict && !QFileInfo(f->path() + activity._file).exists()) {
373                 _activityModel->removeActivityFromActivityList(activity);
374                 continue;
375             }
376 
377             if (activity._status == SyncFileItem::FileLocked && !QFileInfo(f->path() + activity._file).exists()) {
378                 _activityModel->removeActivityFromActivityList(activity);
379                 continue;
380             }
381 
382 
383             if (activity._status == SyncFileItem::FileIgnored && !QFileInfo(f->path() + activity._file).exists()) {
384                 _activityModel->removeActivityFromActivityList(activity);
385                 continue;
386             }
387 
388 
389             if (!QFileInfo(f->path() + activity._file).exists()) {
390                 _activityModel->removeActivityFromActivityList(activity);
391                 continue;
392             }
393 
394             auto path = QFileInfo(activity._file).dir().path().toUtf8();
395             if (path == ".")
396                 path.clear();
397 
398             if (engine.shouldDiscoverLocally(path))
399                 _activityModel->removeActivityFromActivityList(activity);
400         }
401     }
402 
403     if (progress.status() == ProgressInfo::Done) {
404         // We keep track very well of pending conflicts.
405         // Inform other components about them.
406         QStringList conflicts;
407         foreach (Activity activity, _activityModel->errorsList()) {
408             if (activity._folder == folder
409                 && activity._status == SyncFileItem::Conflict) {
410                 conflicts.append(activity._file);
411             }
412         }
413 
414         emit ProgressDispatcher::instance()->folderConflicts(folder, conflicts);
415     }
416 }
417 
slotAddError(const QString & folderAlias,const QString & message,ErrorCategory category)418 void User::slotAddError(const QString &folderAlias, const QString &message, ErrorCategory category)
419 {
420     auto folderInstance = FolderMan::instance()->folder(folderAlias);
421     if (!folderInstance)
422         return;
423 
424     if (folderInstance->accountState() == _account.data()) {
425         qCWarning(lcActivity) << "Item " << folderInstance->shortGuiLocalPath() << " retrieved resulted in " << message;
426 
427         Activity activity;
428         activity._type = Activity::SyncResultType;
429         activity._status = SyncResult::Error;
430         activity._dateTime = QDateTime::fromString(QDateTime::currentDateTime().toString(), Qt::ISODate);
431         activity._subject = message;
432         activity._message = folderInstance->shortGuiLocalPath();
433         activity._link = folderInstance->shortGuiLocalPath();
434         activity._accName = folderInstance->accountState()->account()->displayName();
435         activity._folder = folderAlias;
436 
437 
438         if (category == ErrorCategory::InsufficientRemoteStorage) {
439             ActivityLink link;
440             link._label = tr("Retry all uploads");
441             link._link = folderInstance->path();
442             link._verb = "";
443             link._primary = true;
444             activity._links.append(link);
445         }
446 
447         // add 'other errors' to activity list
448         _activityModel->addErrorToActivityList(activity);
449     }
450 }
451 
slotAddErrorToGui(const QString & folderAlias,SyncFileItem::Status status,const QString & errorMessage,const QString & subject)452 void User::slotAddErrorToGui(const QString &folderAlias, SyncFileItem::Status status, const QString &errorMessage, const QString &subject)
453 {
454     const auto folderInstance = FolderMan::instance()->folder(folderAlias);
455     if (!folderInstance) {
456         return;
457     }
458 
459     if (folderInstance->accountState() == _account.data()) {
460         qCWarning(lcActivity) << "Item " << folderInstance->shortGuiLocalPath() << " retrieved resulted in " << errorMessage;
461 
462         Activity activity;
463         activity._type = Activity::SyncFileItemType;
464         activity._status = status;
465         const auto currentDateTime = QDateTime::currentDateTime();
466         activity._dateTime = QDateTime::fromString(currentDateTime.toString(), Qt::ISODate);
467         activity._expireAtMsecs = currentDateTime.addMSecs(activityDefaultExpirationTimeMsecs).toMSecsSinceEpoch();
468         activity._subject = !subject.isEmpty() ? subject : folderInstance->shortGuiLocalPath();
469         activity._message = errorMessage;
470         activity._link = folderInstance->shortGuiLocalPath();
471         activity._accName = folderInstance->accountState()->account()->displayName();
472         activity._folder = folderAlias;
473 
474         // add 'other errors' to activity list
475         _activityModel->addErrorToActivityList(activity);
476 
477         showDesktopNotification(activity._subject, activity._message);
478 
479         if (!_expiredActivitiesCheckTimer.isActive()) {
480             _expiredActivitiesCheckTimer.start(expiredActivitiesCheckIntervalMsecs);
481         }
482     }
483 }
484 
isActivityOfCurrentAccount(const Folder * folder) const485 bool User::isActivityOfCurrentAccount(const Folder *folder) const
486 {
487     return folder->accountState() == _account.data();
488 }
489 
isUnsolvableConflict(const SyncFileItemPtr & item) const490 bool User::isUnsolvableConflict(const SyncFileItemPtr &item) const
491 {
492     // We just care about conflict issues that we are able to resolve
493     return item->_status == SyncFileItem::Conflict && !Utility::isConflictFile(item->_file);
494 }
495 
processCompletedSyncItem(const Folder * folder,const SyncFileItemPtr & item)496 void User::processCompletedSyncItem(const Folder *folder, const SyncFileItemPtr &item)
497 {
498     Activity activity;
499     activity._type = Activity::SyncFileItemType; //client activity
500     activity._status = item->_status;
501     activity._dateTime = QDateTime::currentDateTime();
502     activity._message = item->_originalFile;
503     activity._link = folder->accountState()->account()->url();
504     activity._accName = folder->accountState()->account()->displayName();
505     activity._file = item->_file;
506     activity._folder = folder->alias();
507     activity._fileAction = "";
508 
509     if (item->_instruction == CSYNC_INSTRUCTION_REMOVE) {
510         activity._fileAction = "file_deleted";
511     } else if (item->_instruction == CSYNC_INSTRUCTION_NEW) {
512         activity._fileAction = "file_created";
513     } else if (item->_instruction == CSYNC_INSTRUCTION_RENAME) {
514         activity._fileAction = "file_renamed";
515     } else {
516         activity._fileAction = "file_changed";
517     }
518 
519     if (item->_status == SyncFileItem::NoStatus || item->_status == SyncFileItem::Success) {
520         qCWarning(lcActivity) << "Item " << item->_file << " retrieved successfully.";
521 
522         if (item->_direction != SyncFileItem::Up) {
523             activity._message = tr("Synced %1").arg(item->_originalFile);
524         } else if (activity._fileAction == "file_renamed") {
525             activity._message = tr("You renamed %1").arg(item->_originalFile);
526         } else if (activity._fileAction == "file_deleted") {
527             activity._message = tr("You deleted %1").arg(item->_originalFile);
528         } else if (activity._fileAction == "file_created") {
529             activity._message = tr("You created %1").arg(item->_originalFile);
530         } else {
531             activity._message = tr("You changed %1").arg(item->_originalFile);
532         }
533 
534         _activityModel->addSyncFileItemToActivityList(activity);
535     } else {
536         qCWarning(lcActivity) << "Item " << item->_file << " retrieved resulted in error " << item->_errorString;
537         activity._subject = item->_errorString;
538 
539         if (item->_status == SyncFileItem::Status::FileIgnored) {
540             _activityModel->addIgnoredFileToList(activity);
541         } else {
542             // add 'protocol error' to activity list
543             if (item->_status == SyncFileItem::Status::FileNameInvalid) {
544                 showDesktopNotification(item->_file, activity._subject);
545             }
546             _activityModel->addErrorToActivityList(activity);
547         }
548     }
549 }
550 
slotItemCompleted(const QString & folder,const SyncFileItemPtr & item)551 void User::slotItemCompleted(const QString &folder, const SyncFileItemPtr &item)
552 {
553     auto folderInstance = FolderMan::instance()->folder(folder);
554 
555     if (!folderInstance || !isActivityOfCurrentAccount(folderInstance) || isUnsolvableConflict(item)) {
556         return;
557     }
558 
559     qCWarning(lcActivity) << "Item " << item->_file << " retrieved resulted in " << item->_errorString;
560     processCompletedSyncItem(folderInstance, item);
561 }
562 
account() const563 AccountPtr User::account() const
564 {
565     return _account->account();
566 }
567 
accountState() const568 AccountStatePtr User::accountState() const
569 {
570     return _account;
571 }
572 
setCurrentUser(const bool & isCurrent)573 void User::setCurrentUser(const bool &isCurrent)
574 {
575     _isCurrentUser = isCurrent;
576 }
577 
getFolder() const578 Folder *User::getFolder() const
579 {
580     foreach (Folder *folder, FolderMan::instance()->map()) {
581         if (folder->accountState() == _account.data()) {
582             return folder;
583         }
584     }
585 
586     return nullptr;
587 }
588 
getActivityModel()589 ActivityListModel *User::getActivityModel()
590 {
591     return _activityModel;
592 }
593 
getUnifiedSearchResultsListModel() const594 UnifiedSearchResultsListModel *User::getUnifiedSearchResultsListModel() const
595 {
596     return _unifiedSearchResultsModel;
597 }
598 
openLocalFolder()599 void User::openLocalFolder()
600 {
601     const auto folder = getFolder();
602 
603     if (folder) {
604         QDesktopServices::openUrl(QUrl::fromLocalFile(folder->path()));
605     }
606 }
607 
login() const608 void User::login() const
609 {
610     _account->account()->resetRejectedCertificates();
611     _account->signIn();
612 }
613 
logout() const614 void User::logout() const
615 {
616     _account->signOutByUi();
617 }
618 
name() const619 QString User::name() const
620 {
621     // If davDisplayName is empty (can be several reasons, simplest is missing login at startup), fall back to username
622     QString name = _account->account()->davDisplayName();
623     if (name == "") {
624         name = _account->account()->credentials()->user();
625     }
626     return name;
627 }
628 
server(bool shortened) const629 QString User::server(bool shortened) const
630 {
631     QString serverUrl = _account->account()->url().toString();
632     if (shortened) {
633         serverUrl.replace(QLatin1String("https://"), QLatin1String(""));
634         serverUrl.replace(QLatin1String("http://"), QLatin1String(""));
635     }
636     return serverUrl;
637 }
638 
status() const639 UserStatus::OnlineStatus User::status() const
640 {
641     return _account->account()->userStatusConnector()->userStatus().state();
642 }
643 
statusMessage() const644 QString User::statusMessage() const
645 {
646     return _account->account()->userStatusConnector()->userStatus().message();
647 }
648 
statusIcon() const649 QUrl User::statusIcon() const
650 {
651     return _account->account()->userStatusConnector()->userStatus().stateIcon();
652 }
653 
statusEmoji() const654 QString User::statusEmoji() const
655 {
656     return _account->account()->userStatusConnector()->userStatus().icon();
657 }
658 
serverHasUserStatus() const659 bool User::serverHasUserStatus() const
660 {
661     return _account->account()->capabilities().userStatus();
662 }
663 
avatar() const664 QImage User::avatar() const
665 {
666     return AvatarJob::makeCircularAvatar(_account->account()->avatar());
667 }
668 
avatarUrl() const669 QString User::avatarUrl() const
670 {
671     if (avatar().isNull()) {
672         return QString();
673     }
674 
675     return QStringLiteral("image://avatars/") + _account->account()->id();
676 }
677 
hasLocalFolder() const678 bool User::hasLocalFolder() const
679 {
680     return getFolder() != nullptr;
681 }
682 
serverHasTalk() const683 bool User::serverHasTalk() const
684 {
685     return talkApp() != nullptr;
686 }
687 
talkApp() const688 AccountApp *User::talkApp() const
689 {
690     return _account->findApp(QStringLiteral("spreed"));
691 }
692 
hasActivities() const693 bool User::hasActivities() const
694 {
695     return _account->account()->capabilities().hasActivities();
696 }
697 
appList() const698 AccountAppList User::appList() const
699 {
700     return _account->appList();
701 }
702 
isCurrentUser() const703 bool User::isCurrentUser() const
704 {
705     return _isCurrentUser;
706 }
707 
isConnected() const708 bool User::isConnected() const
709 {
710     return (_account->connectionStatus() == AccountState::ConnectionStatus::Connected);
711 }
712 
713 
isDesktopNotificationsAllowed() const714 bool User::isDesktopNotificationsAllowed() const
715 {
716     return _account.data()->isDesktopNotificationsAllowed();
717 }
718 
removeAccount() const719 void User::removeAccount() const
720 {
721     AccountManager::instance()->deleteAccount(_account.data());
722     AccountManager::instance()->save();
723 }
724 
725 /*-------------------------------------------------------------------------------------*/
726 
727 UserModel *UserModel::_instance = nullptr;
728 
instance()729 UserModel *UserModel::instance()
730 {
731     if (!_instance) {
732         _instance = new UserModel();
733     }
734     return _instance;
735 }
736 
UserModel(QObject * parent)737 UserModel::UserModel(QObject *parent)
738     : QAbstractListModel(parent)
739 {
740     // TODO: Remember selected user from last quit via settings file
741     if (AccountManager::instance()->accounts().size() > 0) {
742         buildUserList();
743     }
744 
745     connect(AccountManager::instance(), &AccountManager::accountAdded,
746         this, &UserModel::buildUserList);
747 }
748 
buildUserList()749 void UserModel::buildUserList()
750 {
751     for (int i = 0; i < AccountManager::instance()->accounts().size(); i++) {
752         auto user = AccountManager::instance()->accounts().at(i);
753         addUser(user);
754     }
755     if (_init) {
756         _users.first()->setCurrentUser(true);
757         _init = false;
758     }
759 }
760 
numUsers()761 Q_INVOKABLE int UserModel::numUsers()
762 {
763     return _users.size();
764 }
765 
currentUserId() const766 Q_INVOKABLE int UserModel::currentUserId() const
767 {
768     return _currentUserId;
769 }
770 
isUserConnected(const int & id)771 Q_INVOKABLE bool UserModel::isUserConnected(const int &id)
772 {
773     if (id < 0 || id >= _users.size())
774         return false;
775 
776     return _users[id]->isConnected();
777 }
778 
avatarById(const int & id)779 QImage UserModel::avatarById(const int &id)
780 {
781     if (id < 0 || id >= _users.size())
782         return {};
783 
784     return _users[id]->avatar();
785 }
786 
currentUserServer()787 Q_INVOKABLE QString UserModel::currentUserServer()
788 {
789     if (_currentUserId < 0 || _currentUserId >= _users.size())
790         return {};
791 
792     return _users[_currentUserId]->server();
793 }
794 
addUser(AccountStatePtr & user,const bool & isCurrent)795 void UserModel::addUser(AccountStatePtr &user, const bool &isCurrent)
796 {
797     bool containsUser = false;
798     for (const auto &u : qAsConst(_users)) {
799         if (u->account() == user->account()) {
800             containsUser = true;
801             continue;
802         }
803     }
804 
805     if (!containsUser) {
806         int row = rowCount();
807         beginInsertRows(QModelIndex(), row, row);
808 
809         User *u = new User(user, isCurrent);
810 
811         connect(u, &User::avatarChanged, this, [this, row] {
812            emit dataChanged(index(row, 0), index(row, 0), {UserModel::AvatarRole});
813         });
814 
815         connect(u, &User::statusChanged, this, [this, row] {
816             emit dataChanged(index(row, 0), index(row, 0), {UserModel::StatusIconRole,
817 			    				    UserModel::StatusEmojiRole,
818                                                             UserModel::StatusMessageRole});
819         });
820 
821         connect(u, &User::desktopNotificationsAllowedChanged, this, [this, row] {
822             emit dataChanged(index(row, 0), index(row, 0), { UserModel::DesktopNotificationsAllowedRole });
823         });
824 
825         connect(u, &User::accountStateChanged, this, [this, row] {
826             emit dataChanged(index(row, 0), index(row, 0), { UserModel::IsConnectedRole });
827         });
828 
829         _users << u;
830         if (isCurrent) {
831             _currentUserId = _users.indexOf(_users.last());
832         }
833 
834         endInsertRows();
835         ConfigFile cfg;
836         _users.last()->setNotificationRefreshInterval(cfg.notificationRefreshInterval());
837         emit newUserSelected();
838     }
839 }
840 
currentUserIndex()841 int UserModel::currentUserIndex()
842 {
843     return _currentUserId;
844 }
845 
openCurrentAccountLocalFolder()846 Q_INVOKABLE void UserModel::openCurrentAccountLocalFolder()
847 {
848     if (_currentUserId < 0 || _currentUserId >= _users.size())
849         return;
850 
851     _users[_currentUserId]->openLocalFolder();
852 }
853 
openCurrentAccountTalk()854 Q_INVOKABLE void UserModel::openCurrentAccountTalk()
855 {
856     if (!currentUser())
857         return;
858 
859     const auto talkApp = currentUser()->talkApp();
860     if (talkApp) {
861         Utility::openBrowser(talkApp->url());
862     } else {
863         qCWarning(lcActivity) << "The Talk app is not enabled on" << currentUser()->server();
864     }
865 }
866 
openCurrentAccountServer()867 Q_INVOKABLE void UserModel::openCurrentAccountServer()
868 {
869     if (_currentUserId < 0 || _currentUserId >= _users.size())
870         return;
871 
872     QString url = _users[_currentUserId]->server(false);
873     if (!url.startsWith("http://") && !url.startsWith("https://")) {
874         url = "https://" + _users[_currentUserId]->server(false);
875     }
876 
877     QDesktopServices::openUrl(url);
878 }
879 
switchCurrentUser(const int & id)880 Q_INVOKABLE void UserModel::switchCurrentUser(const int &id)
881 {
882     if (_currentUserId < 0 || _currentUserId >= _users.size())
883         return;
884 
885     _users[_currentUserId]->setCurrentUser(false);
886     _users[id]->setCurrentUser(true);
887     _currentUserId = id;
888     emit newUserSelected();
889 }
890 
login(const int & id)891 Q_INVOKABLE void UserModel::login(const int &id)
892 {
893     if (id < 0 || id >= _users.size())
894         return;
895 
896     _users[id]->login();
897 }
898 
logout(const int & id)899 Q_INVOKABLE void UserModel::logout(const int &id)
900 {
901     if (id < 0 || id >= _users.size())
902         return;
903 
904     _users[id]->logout();
905 }
906 
removeAccount(const int & id)907 Q_INVOKABLE void UserModel::removeAccount(const int &id)
908 {
909     if (id < 0 || id >= _users.size())
910         return;
911 
912     QMessageBox messageBox(QMessageBox::Question,
913         tr("Confirm Account Removal"),
914         tr("<p>Do you really want to remove the connection to the account <i>%1</i>?</p>"
915            "<p><b>Note:</b> This will <b>not</b> delete any files.</p>")
916             .arg(_users[id]->name()),
917         QMessageBox::NoButton);
918     QPushButton *yesButton =
919         messageBox.addButton(tr("Remove connection"), QMessageBox::YesRole);
920     messageBox.addButton(tr("Cancel"), QMessageBox::NoRole);
921 
922     messageBox.exec();
923     if (messageBox.clickedButton() != yesButton) {
924         return;
925     }
926 
927     if (_users[id]->isCurrentUser() && _users.count() > 1) {
928         id == 0 ? switchCurrentUser(1) : switchCurrentUser(0);
929     }
930 
931     _users[id]->logout();
932     _users[id]->removeAccount();
933 
934     beginRemoveRows(QModelIndex(), id, id);
935     _users.removeAt(id);
936     endRemoveRows();
937 }
938 
userStatusConnector(int id)939 std::shared_ptr<OCC::UserStatusConnector> UserModel::userStatusConnector(int id)
940 {
941     if (id < 0 || id >= _users.size()) {
942         return nullptr;
943     }
944 
945     return _users[id]->account()->userStatusConnector();
946 }
947 
rowCount(const QModelIndex & parent) const948 int UserModel::rowCount(const QModelIndex &parent) const
949 {
950     Q_UNUSED(parent);
951     return _users.count();
952 }
953 
data(const QModelIndex & index,int role) const954 QVariant UserModel::data(const QModelIndex &index, int role) const
955 {
956     if (index.row() < 0 || index.row() >= _users.count()) {
957         return QVariant();
958     }
959 
960     if (role == NameRole) {
961         return _users[index.row()]->name();
962     } else if (role == ServerRole) {
963         return _users[index.row()]->server();
964     } else if (role == ServerHasUserStatusRole) {
965         return _users[index.row()]->serverHasUserStatus();
966     } else if (role == StatusIconRole) {
967         return _users[index.row()]->statusIcon();
968     } else if (role == StatusEmojiRole) {
969         return _users[index.row()]->statusEmoji();
970     } else if (role == StatusMessageRole) {
971         return _users[index.row()]->statusMessage();
972     } else if (role == DesktopNotificationsAllowedRole) {
973         return _users[index.row()]->isDesktopNotificationsAllowed();
974     } else if (role == AvatarRole) {
975         return _users[index.row()]->avatarUrl();
976     } else if (role == IsCurrentUserRole) {
977         return _users[index.row()]->isCurrentUser();
978     } else if (role == IsConnectedRole) {
979         return _users[index.row()]->isConnected();
980     } else if (role == IdRole) {
981         return index.row();
982     }
983     return QVariant();
984 }
985 
roleNames() const986 QHash<int, QByteArray> UserModel::roleNames() const
987 {
988     QHash<int, QByteArray> roles;
989     roles[NameRole] = "name";
990     roles[ServerRole] = "server";
991     roles[ServerHasUserStatusRole] = "serverHasUserStatus";
992     roles[StatusIconRole] = "statusIcon";
993     roles[StatusEmojiRole] = "statusEmoji";
994     roles[StatusMessageRole] = "statusMessage";
995     roles[DesktopNotificationsAllowedRole] = "desktopNotificationsAllowed";
996     roles[AvatarRole] = "avatar";
997     roles[IsCurrentUserRole] = "isCurrentUser";
998     roles[IsConnectedRole] = "isConnected";
999     roles[IdRole] = "id";
1000     return roles;
1001 }
1002 
currentActivityModel()1003 ActivityListModel *UserModel::currentActivityModel()
1004 {
1005     if (currentUserIndex() < 0 || currentUserIndex() >= _users.size())
1006         return nullptr;
1007 
1008     return _users[currentUserIndex()]->getActivityModel();
1009 }
1010 
fetchCurrentActivityModel()1011 void UserModel::fetchCurrentActivityModel()
1012 {
1013     if (currentUserId() < 0 || currentUserId() >= _users.size())
1014         return;
1015 
1016     _users[currentUserId()]->slotRefresh();
1017 }
1018 
appList() const1019 AccountAppList UserModel::appList() const
1020 {
1021     if (_currentUserId < 0 || _currentUserId >= _users.size())
1022         return {};
1023 
1024     return _users[_currentUserId]->appList();
1025 }
1026 
currentUser() const1027 User *UserModel::currentUser() const
1028 {
1029     if (currentUserId() < 0 || currentUserId() >= _users.size())
1030         return nullptr;
1031 
1032     return _users[currentUserId()];
1033 }
1034 
findUserIdForAccount(AccountState * account) const1035 int UserModel::findUserIdForAccount(AccountState *account) const
1036 {
1037     const auto it = std::find_if(std::cbegin(_users), std::cend(_users), [=](const User *user) {
1038         return user->account()->id() == account->account()->id();
1039     });
1040 
1041     if (it == std::cend(_users)) {
1042         return -1;
1043     }
1044 
1045     const auto id = std::distance(std::cbegin(_users), it);
1046     return id;
1047 }
1048 
1049 /*-------------------------------------------------------------------------------------*/
1050 
ImageProvider()1051 ImageProvider::ImageProvider()
1052     : QQuickImageProvider(QQuickImageProvider::Image)
1053 {
1054 }
1055 
requestImage(const QString & id,QSize * size,const QSize & requestedSize)1056 QImage ImageProvider::requestImage(const QString &id, QSize *size, const QSize &requestedSize)
1057 {
1058     Q_UNUSED(size)
1059     Q_UNUSED(requestedSize)
1060 
1061     const auto makeIcon = [](const QString &path) {
1062         QImage image(128, 128, QImage::Format_ARGB32);
1063         image.fill(Qt::GlobalColor::transparent);
1064         QPainter painter(&image);
1065         QSvgRenderer renderer(path);
1066         renderer.render(&painter);
1067         return image;
1068     };
1069 
1070     if (id == QLatin1String("fallbackWhite")) {
1071         return makeIcon(QStringLiteral(":/client/theme/white/user.svg"));
1072     }
1073 
1074     if (id == QLatin1String("fallbackBlack")) {
1075         return makeIcon(QStringLiteral(":/client/theme/black/user.svg"));
1076     }
1077 
1078     const int uid = id.toInt();
1079     return UserModel::instance()->avatarById(uid);
1080 }
1081 
1082 /*-------------------------------------------------------------------------------------*/
1083 
1084 UserAppsModel *UserAppsModel::_instance = nullptr;
1085 
instance()1086 UserAppsModel *UserAppsModel::instance()
1087 {
1088     if (!_instance) {
1089         _instance = new UserAppsModel();
1090     }
1091     return _instance;
1092 }
1093 
UserAppsModel(QObject * parent)1094 UserAppsModel::UserAppsModel(QObject *parent)
1095     : QAbstractListModel(parent)
1096 {
1097 }
1098 
buildAppList()1099 void UserAppsModel::buildAppList()
1100 {
1101     if (rowCount() > 0) {
1102         beginRemoveRows(QModelIndex(), 0, rowCount() - 1);
1103         _apps.clear();
1104         endRemoveRows();
1105     }
1106 
1107     if (UserModel::instance()->appList().count() > 0) {
1108         const auto talkApp = UserModel::instance()->currentUser()->talkApp();
1109         foreach (AccountApp *app, UserModel::instance()->appList()) {
1110             // Filter out Talk because we have a dedicated button for it
1111             if (talkApp && app->id() == talkApp->id())
1112                 continue;
1113 
1114             beginInsertRows(QModelIndex(), rowCount(), rowCount());
1115             _apps << app;
1116             endInsertRows();
1117         }
1118     }
1119 }
1120 
openAppUrl(const QUrl & url)1121 void UserAppsModel::openAppUrl(const QUrl &url)
1122 {
1123     Utility::openBrowser(url);
1124 }
1125 
rowCount(const QModelIndex & parent) const1126 int UserAppsModel::rowCount(const QModelIndex &parent) const
1127 {
1128     Q_UNUSED(parent);
1129     return _apps.count();
1130 }
1131 
data(const QModelIndex & index,int role) const1132 QVariant UserAppsModel::data(const QModelIndex &index, int role) const
1133 {
1134     if (index.row() < 0 || index.row() >= _apps.count()) {
1135         return QVariant();
1136     }
1137 
1138     if (role == NameRole) {
1139         return _apps[index.row()]->name();
1140     } else if (role == UrlRole) {
1141         return _apps[index.row()]->url();
1142     } else if (role == IconUrlRole) {
1143         return _apps[index.row()]->iconUrl().toString();
1144     }
1145     return QVariant();
1146 }
1147 
roleNames() const1148 QHash<int, QByteArray> UserAppsModel::roleNames() const
1149 {
1150     QHash<int, QByteArray> roles;
1151     roles[NameRole] = "appName";
1152     roles[UrlRole] = "appUrl";
1153     roles[IconUrlRole] = "appIconUrl";
1154     return roles;
1155 }
1156 
1157 }
1158