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