mirror of
https://github.com/nextcloud/desktop.git
synced 2024-11-26 15:06:08 +03:00
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:
commit
c2f72b55f7
9 changed files with 152 additions and 23 deletions
|
@ -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)
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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);
|
||||||
|
|
|
@ -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);
|
||||||
|
|
|
@ -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));
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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()
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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:
|
||||||
|
|
Loading…
Reference in a new issue