diff options
author | Claudio Cambra <claudio.cambra@gmail.com> | 2022-05-10 17:12:15 +0300 |
---|---|---|
committer | Claudio Cambra <claudio.cambra@gmail.com> | 2022-05-12 13:09:39 +0300 |
commit | 99420fd7ff44d4aab221390a03f22e07418c308b (patch) | |
tree | 1df6967f6a462a4efaed2937ffa097b20716efba | |
parent | 4aac60814da6555be7e9cdc70fddd3c9270cbd87 (diff) |
Reimplement notifications for macOS and add support for actionable update notificationsfeature/mac-notifications-3.4
Signed-off-by: Claudio Cambra <claudio.cambra@gmail.com>
-rw-r--r-- | src/gui/CMakeLists.txt | 2 | ||||
-rw-r--r-- | src/gui/application.cpp | 2 | ||||
-rw-r--r-- | src/gui/owncloudgui.cpp | 9 | ||||
-rw-r--r-- | src/gui/owncloudgui.h | 1 | ||||
-rw-r--r-- | src/gui/systray.cpp | 16 | ||||
-rw-r--r-- | src/gui/systray.h | 7 | ||||
-rw-r--r-- | src/gui/systray.mm | 137 | ||||
-rw-r--r-- | src/gui/updater/ocupdater.cpp | 2 | ||||
-rw-r--r-- | src/gui/updater/ocupdater.h | 4 |
9 files changed, 157 insertions, 23 deletions
diff --git a/src/gui/CMakeLists.txt b/src/gui/CMakeLists.txt index 6b63f4630..0fe4e5c6b 100644 --- a/src/gui/CMakeLists.txt +++ b/src/gui/CMakeLists.txt @@ -565,7 +565,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 e6dd39142..f43b2a598 100644 --- a/src/gui/application.cpp +++ b/src/gui/application.cpp @@ -372,7 +372,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 3941d9b0f..61739799e 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 707609612..dd0eed2ed 100644 --- a/src/gui/systray.cpp +++ b/src/gui/systray.cpp @@ -97,7 +97,11 @@ Systray::Systray() qmlRegisterType<WheelHandler>("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); @@ -255,6 +259,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 31251d01e..9eabdf4a1 100644 --- a/src/gui/systray.h +++ b/src/gui/systray.h @@ -38,9 +38,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); #endif @@ -66,6 +70,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 a4d35eb23..dfdc009cf 100644 --- a/src/gui/systray.mm +++ b/src/gui/systray.mm @@ -1,45 +1,149 @@ +#include "QtCore/qurl.h" +#include "config.h" #include <QString> #include <QWindow> +#include <QLoggingCategory> + #import <Cocoa/Cocoa.h> +#import <UserNotifications/UserNotifications.h> + +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 namespace OCC { +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() +{ + 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() { - Class cuserNotificationCenter = NSClassFromString(@"NSUserNotificationCenter"); - id userNotificationCenter = [cuserNotificationCenter defaultUserNotificationCenter]; + UNUserNotificationCenter* center = [UNUserNotificationCenter currentNotificationCenter]; static dispatch_once_t once; dispatch_once(&once, ^{ id delegate = [[NotificationCenterDelegate alloc] init]; - [userNotificationCenter setDelegate:delegate]; + [center setDelegate:delegate]; }); +} + +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]; + + return content; +} + +void sendOsXUserNotification(const QString &title, const QString &message) +{ + UNUserNotificationCenter* center = [UNUserNotificationCenter currentNotificationCenter]; + checkNotificationAuth(); + + UNMutableNotificationContent* content = basicNotificationContent(title, message); + content.categoryIdentifier = @"GENERAL"; - Class cuserNotification = NSClassFromString(@"NSUserNotification"); - id notification = [[cuserNotification alloc] init]; - [notification setTitle:[NSString stringWithUTF8String:title.toUtf8().data()]]; - [notification setInformativeText:[NSString stringWithUTF8String:message.toUtf8().data()]]; + UNTimeIntervalNotificationTrigger* trigger = [UNTimeIntervalNotificationTrigger triggerWithTimeInterval:1 repeats: NO]; + UNNotificationRequest* request = [UNNotificationRequest requestWithIdentifier:@"NCUserNotification" content:content trigger:trigger]; - [userNotificationCenter deliverNotification:notification]; - [notification release]; + [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) @@ -52,3 +156,4 @@ void setTrayWindowLevelAndVisibleOnAllSpaces(QWindow *window) } } + 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: |