diff --git a/src/gui/CMakeLists.txt b/src/gui/CMakeLists.txt index 8b47d33ff..c7fb8bc59 100644 --- a/src/gui/CMakeLists.txt +++ b/src/gui/CMakeLists.txt @@ -665,7 +665,7 @@ endif() if (APPLE) find_package(Qt5 COMPONENTS MacExtras) - target_link_libraries(nextcloudCore PUBLIC Qt5::MacExtras) + target_link_libraries(nextcloudCore PUBLIC Qt5::MacExtras "-framework UserNotifications") endif() if(WITH_CRASHREPORTER) diff --git a/src/gui/application.cpp b/src/gui/application.cpp index b1a3561ea..323d87c91 100644 --- a/src/gui/application.cpp +++ b/src/gui/application.cpp @@ -390,7 +390,7 @@ Application::Application(int &argc, char **argv) // Update checks auto *updaterScheduler = new UpdaterScheduler(this); connect(updaterScheduler, &UpdaterScheduler::updaterAnnouncement, - _gui.data(), &ownCloudGui::slotShowTrayMessage); + _gui.data(), &ownCloudGui::slotShowTrayUpdateMessage); connect(updaterScheduler, &UpdaterScheduler::requestRestart, _folderManager.data(), &FolderMan::slotScheduleAppRestart); #endif diff --git a/src/gui/owncloudgui.cpp b/src/gui/owncloudgui.cpp index d21a7446b..402e3801d 100644 --- a/src/gui/owncloudgui.cpp +++ b/src/gui/owncloudgui.cpp @@ -379,6 +379,15 @@ void ownCloudGui::slotShowTrayMessage(const QString &title, const QString &msg) qCWarning(lcApplication) << "Tray not ready: " << msg; } +void ownCloudGui::slotShowTrayUpdateMessage(const QString &title, const QString &msg, const QUrl &webUrl) +{ + if(_tray) { + _tray->showUpdateMessage(title, msg, webUrl); + } else { + qCWarning(lcApplication) << "Tray not ready: " << msg; + } +} + void ownCloudGui::slotShowOptionalTrayMessage(const QString &title, const QString &msg) { slotShowTrayMessage(title, msg); diff --git a/src/gui/owncloudgui.h b/src/gui/owncloudgui.h index 312c43748..3ffa57cd1 100644 --- a/src/gui/owncloudgui.h +++ b/src/gui/owncloudgui.h @@ -75,6 +75,7 @@ signals: public slots: void slotComputeOverallSyncStatus(); void slotShowTrayMessage(const QString &title, const QString &msg); + void slotShowTrayUpdateMessage(const QString &title, const QString &msg, const QUrl &webUrl); void slotShowOptionalTrayMessage(const QString &title, const QString &msg); void slotFolderOpenAction(const QString &alias); void slotUpdateProgress(const QString &folder, const ProgressInfo &progress); diff --git a/src/gui/systray.cpp b/src/gui/systray.cpp index 60d9a0e75..56ee9b9e7 100644 --- a/src/gui/systray.cpp +++ b/src/gui/systray.cpp @@ -99,7 +99,11 @@ Systray::Systray() qmlRegisterType("com.nextcloud.desktopclient", 1, 0, "WheelHandler"); -#ifndef Q_OS_MAC +#ifdef Q_OS_MACOS + setUserNotificationCenterDelegate(); + checkNotificationAuth(); + registerNotificationCategories(QString(tr("Download"))); +#else auto contextMenu = new QMenu(); if (AccountManager::instance()->accounts().isEmpty()) { contextMenu->addAction(tr("Add account"), this, &Systray::openAccountWizard); @@ -296,6 +300,16 @@ void Systray::showMessage(const QString &title, const QString &message, MessageI } } +void Systray::showUpdateMessage(const QString &title, const QString &message, const QUrl &webUrl) +{ +#ifdef Q_OS_MACOS + sendOsXUpdateNotification(title, message, webUrl); +#else // TODO: Implement custom notifications (i.e. actionable) for other OSes + Q_UNUSED(webUrl); + showMessage(title, message); +#endif +} + void Systray::setToolTip(const QString &tip) { QSystemTrayIcon::setToolTip(tr("%1: %2").arg(Theme::instance()->appNameGUI(), tip)); diff --git a/src/gui/systray.h b/src/gui/systray.h index 85978467a..5bfbe5068 100644 --- a/src/gui/systray.h +++ b/src/gui/systray.h @@ -39,9 +39,13 @@ public: QNetworkAccessManager* create(QObject *parent) override; }; -#ifdef Q_OS_OSX +#ifdef Q_OS_MACOS +void setUserNotificationCenterDelegate(); +void checkNotificationAuth(); +void registerNotificationCategories(const QString &localizedDownloadString); bool canOsXSendUserNotification(); void sendOsXUserNotification(const QString &title, const QString &message); +void sendOsXUpdateNotification(const QString &title, const QString &message, const QUrl &webUrl); void setTrayWindowLevelAndVisibleOnAllSpaces(QWindow *window); double statusBarThickness(); #endif @@ -71,6 +75,7 @@ public: void setTrayEngine(QQmlApplicationEngine *trayEngine); void create(); void showMessage(const QString &title, const QString &message, MessageIcon icon = Information); + void showUpdateMessage(const QString &title, const QString &message, const QUrl &webUrl); void setToolTip(const QString &tip); bool isOpen(); QString windowTitle() const; diff --git a/src/gui/systray.mm b/src/gui/systray.mm index 7f564fcd2..392f5fde6 100644 --- a/src/gui/systray.mm +++ b/src/gui/systray.mm @@ -1,17 +1,43 @@ +#include "QtCore/qurl.h" +#include "config.h" #include #include +#include + #import +#import + +Q_LOGGING_CATEGORY(lcMacSystray, "nextcloud.gui.macsystray") @interface NotificationCenterDelegate : NSObject @end @implementation NotificationCenterDelegate + // Always show, even if app is active at the moment. -- (BOOL)userNotificationCenter:(NSUserNotificationCenter *)center - shouldPresentNotification:(NSUserNotification *)notification +- (void)userNotificationCenter:(UNUserNotificationCenter *)center + willPresentNotification:(UNNotification *)notification + withCompletionHandler:(void (^)(UNNotificationPresentationOptions options))completionHandler { - Q_UNUSED(center); - Q_UNUSED(notification); - return YES; + completionHandler(UNNotificationPresentationOptionSound + UNNotificationPresentationOptionBanner); +} + +- (void)userNotificationCenter:(UNUserNotificationCenter *)center + didReceiveNotificationResponse:(UNNotificationResponse *)response + withCompletionHandler:(void (^)(void))completionHandler +{ + qCDebug(lcMacSystray()) << "Received notification with category identifier:" << response.notification.request.content.categoryIdentifier + << "and action identifier" << response.actionIdentifier; + UNNotificationContent* content = response.notification.request.content; + if ([content.categoryIdentifier isEqualToString:@"UPDATE"]) { + + if ([response.actionIdentifier isEqualToString:@"DOWNLOAD_ACTION"] || [response.actionIdentifier isEqualToString:UNNotificationDefaultActionIdentifier]) + { + qCDebug(lcMacSystray()) << "Opening update download url in browser."; + [[NSWorkspace sharedWorkspace] openURL:[NSURL URLWithString:[content.userInfo objectForKey:@"webUrl"]]]; + } + } + + completionHandler(); } @end @@ -22,29 +48,102 @@ double statusBarThickness() return [NSStatusBar systemStatusBar].thickness; } +// TODO: Get this to actually check for permissions bool canOsXSendUserNotification() { - return NSClassFromString(@"NSUserNotificationCenter") != nil; + UNUserNotificationCenter* center = [UNUserNotificationCenter currentNotificationCenter]; + return center != nil; } -void sendOsXUserNotification(const QString &title, const QString &message) +void registerNotificationCategories(const QString &localisedDownloadString) { + UNNotificationCategory* generalCategory = [UNNotificationCategory + categoryWithIdentifier:@"GENERAL" + actions:@[] + intentIdentifiers:@[] + options:UNNotificationCategoryOptionCustomDismissAction]; + + // Create the custom actions for update notifications. + UNNotificationAction* downloadAction = [UNNotificationAction + actionWithIdentifier:@"DOWNLOAD_ACTION" + title:localisedDownloadString.toNSString() + options:UNNotificationActionOptionNone]; + + // Create the category with the custom actions. + UNNotificationCategory* updateCategory = [UNNotificationCategory + categoryWithIdentifier:@"UPDATE" + actions:@[downloadAction] + intentIdentifiers:@[] + options:UNNotificationCategoryOptionNone]; + + [[UNUserNotificationCenter currentNotificationCenter] setNotificationCategories:[NSSet setWithObjects:generalCategory, updateCategory, nil]]; +} + +void checkNotificationAuth() { - Class cuserNotificationCenter = NSClassFromString(@"NSUserNotificationCenter"); - id userNotificationCenter = [cuserNotificationCenter defaultUserNotificationCenter]; + UNUserNotificationCenter* center = [UNUserNotificationCenter currentNotificationCenter]; + [center requestAuthorizationWithOptions:(UNAuthorizationOptionAlert + UNAuthorizationOptionSound + UNAuthorizationOptionProvisional) + completionHandler:^(BOOL granted, NSError * _Nullable error) { + // Enable or disable features based on authorization. + if(granted) { + qCDebug(lcMacSystray) << "Authorization for notifications has been granted, can display notifications."; + } else { + qCDebug(lcMacSystray) << "Authorization for notifications not granted."; + if(error) { + QString errorDescription([error.localizedDescription UTF8String]); + qCDebug(lcMacSystray) << "Error from notification center: " << errorDescription; + } + } + }]; +} + +void setUserNotificationCenterDelegate() +{ + UNUserNotificationCenter* center = [UNUserNotificationCenter currentNotificationCenter]; static dispatch_once_t once; dispatch_once(&once, ^{ id delegate = [[NotificationCenterDelegate alloc] init]; - [userNotificationCenter setDelegate:delegate]; + [center setDelegate:delegate]; }); +} - Class cuserNotification = NSClassFromString(@"NSUserNotification"); - id notification = [[cuserNotification alloc] init]; - [notification setTitle:[NSString stringWithUTF8String:title.toUtf8().data()]]; - [notification setInformativeText:[NSString stringWithUTF8String:message.toUtf8().data()]]; +UNMutableNotificationContent* basicNotificationContent(const QString &title, const QString &message) +{ + UNMutableNotificationContent* content = [[UNMutableNotificationContent alloc] init]; + content.title = title.toNSString(); + content.body = message.toNSString(); + content.sound = [UNNotificationSound defaultSound]; - [userNotificationCenter deliverNotification:notification]; - [notification release]; + return content; +} + +void sendOsXUserNotification(const QString &title, const QString &message) +{ + UNUserNotificationCenter* center = [UNUserNotificationCenter currentNotificationCenter]; + checkNotificationAuth(); + + UNMutableNotificationContent* content = basicNotificationContent(title, message); + content.categoryIdentifier = @"GENERAL"; + + UNTimeIntervalNotificationTrigger* trigger = [UNTimeIntervalNotificationTrigger triggerWithTimeInterval:1 repeats: NO]; + UNNotificationRequest* request = [UNNotificationRequest requestWithIdentifier:@"NCUserNotification" content:content trigger:trigger]; + + [center addNotificationRequest:request withCompletionHandler:nil]; +} + +void sendOsXUpdateNotification(const QString &title, const QString &message, const QUrl &webUrl) +{ + UNUserNotificationCenter* center = [UNUserNotificationCenter currentNotificationCenter]; + checkNotificationAuth(); + + UNMutableNotificationContent* content = basicNotificationContent(title, message); + content.categoryIdentifier = @"UPDATE"; + content.userInfo = [NSDictionary dictionaryWithObject:[webUrl.toNSURL() absoluteString] forKey:@"webUrl"]; + + UNTimeIntervalNotificationTrigger* trigger = [UNTimeIntervalNotificationTrigger triggerWithTimeInterval:1 repeats: NO]; + UNNotificationRequest* request = [UNNotificationRequest requestWithIdentifier:@"NCUpdateNotification" content:content trigger:trigger]; + + [center addNotificationRequest:request withCompletionHandler:nil]; } void setTrayWindowLevelAndVisibleOnAllSpaces(QWindow *window) @@ -63,3 +162,4 @@ bool osXInDarkMode() } } + diff --git a/src/gui/updater/ocupdater.cpp b/src/gui/updater/ocupdater.cpp index 6bcf726d0..aecc66c90 100644 --- a/src/gui/updater/ocupdater.cpp +++ b/src/gui/updater/ocupdater.cpp @@ -193,7 +193,7 @@ void OCUpdater::setDownloadState(DownloadState state) // or once for system based updates. if (_state == OCUpdater::DownloadComplete || (oldState != OCUpdater::UpdateOnlyAvailableThroughSystem && _state == OCUpdater::UpdateOnlyAvailableThroughSystem)) { - emit newUpdateAvailable(tr("Update Check"), statusString()); + emit newUpdateAvailable(tr("Update Check"), statusString(), _updateInfo.web()); } } diff --git a/src/gui/updater/ocupdater.h b/src/gui/updater/ocupdater.h index c6c1ad8df..15680f798 100644 --- a/src/gui/updater/ocupdater.h +++ b/src/gui/updater/ocupdater.h @@ -71,7 +71,7 @@ public: UpdaterScheduler(QObject *parent); signals: - void updaterAnnouncement(const QString &title, const QString &msg); + void updaterAnnouncement(const QString &title, const QString &msg, const QUrl &webUrl); void requestRestart(); private slots: @@ -116,7 +116,7 @@ public: signals: void downloadStateChanged(); - void newUpdateAvailable(const QString &header, const QString &message); + void newUpdateAvailable(const QString &header, const QString &message, const QUrl &webUrl); void requestRestart(); public slots: