Merge pull request #4512 from nextcloud/feature/macos-notifications

Revamp notifications for macOS and add support for actionable update notifications
This commit is contained in:
Claudio Cambra 2022-05-11 18:22:05 +02:00 committed by GitHub
commit c2f72b55f7
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
9 changed files with 152 additions and 23 deletions

View file

@ -665,7 +665,7 @@ endif()
if (APPLE) if (APPLE)
find_package(Qt5 COMPONENTS MacExtras) find_package(Qt5 COMPONENTS MacExtras)
target_link_libraries(nextcloudCore PUBLIC Qt5::MacExtras) target_link_libraries(nextcloudCore PUBLIC Qt5::MacExtras "-framework UserNotifications")
endif() endif()
if(WITH_CRASHREPORTER) if(WITH_CRASHREPORTER)

View file

@ -390,7 +390,7 @@ Application::Application(int &argc, char **argv)
// Update checks // Update checks
auto *updaterScheduler = new UpdaterScheduler(this); auto *updaterScheduler = new UpdaterScheduler(this);
connect(updaterScheduler, &UpdaterScheduler::updaterAnnouncement, connect(updaterScheduler, &UpdaterScheduler::updaterAnnouncement,
_gui.data(), &ownCloudGui::slotShowTrayMessage); _gui.data(), &ownCloudGui::slotShowTrayUpdateMessage);
connect(updaterScheduler, &UpdaterScheduler::requestRestart, connect(updaterScheduler, &UpdaterScheduler::requestRestart,
_folderManager.data(), &FolderMan::slotScheduleAppRestart); _folderManager.data(), &FolderMan::slotScheduleAppRestart);
#endif #endif

View file

@ -379,6 +379,15 @@ void ownCloudGui::slotShowTrayMessage(const QString &title, const QString &msg)
qCWarning(lcApplication) << "Tray not ready: " << 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) void ownCloudGui::slotShowOptionalTrayMessage(const QString &title, const QString &msg)
{ {
slotShowTrayMessage(title, msg); slotShowTrayMessage(title, msg);

View file

@ -75,6 +75,7 @@ signals:
public slots: public slots:
void slotComputeOverallSyncStatus(); void slotComputeOverallSyncStatus();
void slotShowTrayMessage(const QString &title, const QString &msg); 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 slotShowOptionalTrayMessage(const QString &title, const QString &msg);
void slotFolderOpenAction(const QString &alias); void slotFolderOpenAction(const QString &alias);
void slotUpdateProgress(const QString &folder, const ProgressInfo &progress); void slotUpdateProgress(const QString &folder, const ProgressInfo &progress);

View file

@ -99,7 +99,11 @@ Systray::Systray()
qmlRegisterType<WheelHandler>("com.nextcloud.desktopclient", 1, 0, "WheelHandler"); 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(); auto contextMenu = new QMenu();
if (AccountManager::instance()->accounts().isEmpty()) { if (AccountManager::instance()->accounts().isEmpty()) {
contextMenu->addAction(tr("Add account"), this, &Systray::openAccountWizard); 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) void Systray::setToolTip(const QString &tip)
{ {
QSystemTrayIcon::setToolTip(tr("%1: %2").arg(Theme::instance()->appNameGUI(), tip)); QSystemTrayIcon::setToolTip(tr("%1: %2").arg(Theme::instance()->appNameGUI(), tip));

View file

@ -39,9 +39,13 @@ public:
QNetworkAccessManager* create(QObject *parent) override; QNetworkAccessManager* create(QObject *parent) override;
}; };
#ifdef Q_OS_OSX #ifdef Q_OS_MACOS
void setUserNotificationCenterDelegate();
void checkNotificationAuth();
void registerNotificationCategories(const QString &localizedDownloadString);
bool canOsXSendUserNotification(); bool canOsXSendUserNotification();
void sendOsXUserNotification(const QString &title, const QString &message); void sendOsXUserNotification(const QString &title, const QString &message);
void sendOsXUpdateNotification(const QString &title, const QString &message, const QUrl &webUrl);
void setTrayWindowLevelAndVisibleOnAllSpaces(QWindow *window); void setTrayWindowLevelAndVisibleOnAllSpaces(QWindow *window);
double statusBarThickness(); double statusBarThickness();
#endif #endif
@ -71,6 +75,7 @@ public:
void setTrayEngine(QQmlApplicationEngine *trayEngine); void setTrayEngine(QQmlApplicationEngine *trayEngine);
void create(); void create();
void showMessage(const QString &title, const QString &message, MessageIcon icon = Information); 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); void setToolTip(const QString &tip);
bool isOpen(); bool isOpen();
QString windowTitle() const; QString windowTitle() const;

View file

@ -1,17 +1,43 @@
#include "QtCore/qurl.h"
#include "config.h"
#include <QString> #include <QString>
#include <QWindow> #include <QWindow>
#include <QLoggingCategory>
#import <Cocoa/Cocoa.h> #import <Cocoa/Cocoa.h>
#import <UserNotifications/UserNotifications.h>
Q_LOGGING_CATEGORY(lcMacSystray, "nextcloud.gui.macsystray")
@interface NotificationCenterDelegate : NSObject @interface NotificationCenterDelegate : NSObject
@end @end
@implementation NotificationCenterDelegate @implementation NotificationCenterDelegate
// Always show, even if app is active at the moment. // Always show, even if app is active at the moment.
- (BOOL)userNotificationCenter:(NSUserNotificationCenter *)center - (void)userNotificationCenter:(UNUserNotificationCenter *)center
shouldPresentNotification:(NSUserNotification *)notification willPresentNotification:(UNNotification *)notification
withCompletionHandler:(void (^)(UNNotificationPresentationOptions options))completionHandler
{ {
Q_UNUSED(center); completionHandler(UNNotificationPresentationOptionSound + UNNotificationPresentationOptionBanner);
Q_UNUSED(notification); }
return YES;
- (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 @end
@ -22,29 +48,102 @@ double statusBarThickness()
return [NSStatusBar systemStatusBar].thickness; return [NSStatusBar systemStatusBar].thickness;
} }
// TODO: Get this to actually check for permissions
bool canOsXSendUserNotification() 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"); UNUserNotificationCenter* center = [UNUserNotificationCenter currentNotificationCenter];
id userNotificationCenter = [cuserNotificationCenter defaultUserNotificationCenter]; [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; static dispatch_once_t once;
dispatch_once(&once, ^{ dispatch_once(&once, ^{
id delegate = [[NotificationCenterDelegate alloc] init]; id delegate = [[NotificationCenterDelegate alloc] init];
[userNotificationCenter setDelegate:delegate]; [center setDelegate:delegate];
}); });
}
Class cuserNotification = NSClassFromString(@"NSUserNotification"); UNMutableNotificationContent* basicNotificationContent(const QString &title, const QString &message)
id notification = [[cuserNotification alloc] init]; {
[notification setTitle:[NSString stringWithUTF8String:title.toUtf8().data()]]; UNMutableNotificationContent* content = [[UNMutableNotificationContent alloc] init];
[notification setInformativeText:[NSString stringWithUTF8String:message.toUtf8().data()]]; content.title = title.toNSString();
content.body = message.toNSString();
content.sound = [UNNotificationSound defaultSound];
[userNotificationCenter deliverNotification:notification]; return content;
[notification release]; }
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) void setTrayWindowLevelAndVisibleOnAllSpaces(QWindow *window)
@ -63,3 +162,4 @@ bool osXInDarkMode()
} }
} }

View file

@ -193,7 +193,7 @@ void OCUpdater::setDownloadState(DownloadState state)
// or once for system based updates. // or once for system based updates.
if (_state == OCUpdater::DownloadComplete || (oldState != OCUpdater::UpdateOnlyAvailableThroughSystem if (_state == OCUpdater::DownloadComplete || (oldState != OCUpdater::UpdateOnlyAvailableThroughSystem
&& _state == OCUpdater::UpdateOnlyAvailableThroughSystem)) { && _state == OCUpdater::UpdateOnlyAvailableThroughSystem)) {
emit newUpdateAvailable(tr("Update Check"), statusString()); emit newUpdateAvailable(tr("Update Check"), statusString(), _updateInfo.web());
} }
} }

View file

@ -71,7 +71,7 @@ public:
UpdaterScheduler(QObject *parent); UpdaterScheduler(QObject *parent);
signals: signals:
void updaterAnnouncement(const QString &title, const QString &msg); void updaterAnnouncement(const QString &title, const QString &msg, const QUrl &webUrl);
void requestRestart(); void requestRestart();
private slots: private slots:
@ -116,7 +116,7 @@ public:
signals: signals:
void downloadStateChanged(); void downloadStateChanged();
void newUpdateAvailable(const QString &header, const QString &message); void newUpdateAvailable(const QString &header, const QString &message, const QUrl &webUrl);
void requestRestart(); void requestRestart();
public slots: public slots: