Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[stable-3.5] Revamp notifications for macOS and add support for actionable update notifications #4515

Merged
merged 1 commit into from
May 12, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion src/gui/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
2 changes: 1 addition & 1 deletion src/gui/application.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
9 changes: 9 additions & 0 deletions src/gui/owncloudgui.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
1 change: 1 addition & 0 deletions src/gui/owncloudgui.h
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,7 @@ class ownCloudGui : public QObject
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);
Expand Down
16 changes: 15 additions & 1 deletion src/gui/systray.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -99,7 +99,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);
Expand Down Expand Up @@ -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));
Expand Down
7 changes: 6 additions & 1 deletion src/gui/systray.h
Original file line number Diff line number Diff line change
Expand Up @@ -39,9 +39,13 @@ class AccessManagerFactory : public QQmlNetworkAccessManagerFactory
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
Expand Down Expand Up @@ -71,6 +75,7 @@ class Systray
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;
Expand Down
132 changes: 116 additions & 16 deletions src/gui/systray.mm
Original file line number Diff line number Diff line change
@@ -1,17 +1,43 @@
#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

Expand All @@ -22,29 +48,102 @@ - (BOOL)userNotificationCenter:(NSUserNotificationCenter *)center
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();

Class cuserNotification = NSClassFromString(@"NSUserNotification");
id notification = [[cuserNotification alloc] init];
[notification setTitle:[NSString stringWithUTF8String:title.toUtf8().data()]];
[notification setInformativeText:[NSString stringWithUTF8String:message.toUtf8().data()]];
UNMutableNotificationContent* content = basicNotificationContent(title, message);
content.categoryIdentifier = @"GENERAL";

[userNotificationCenter deliverNotification:notification];
[notification release];
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)
Expand All @@ -63,3 +162,4 @@ bool osXInDarkMode()
}

}

2 changes: 1 addition & 1 deletion src/gui/updater/ocupdater.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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());
}
}

Expand Down
4 changes: 2 additions & 2 deletions src/gui/updater/ocupdater.h
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@ class UpdaterScheduler : public QObject
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:
Expand Down Expand Up @@ -116,7 +116,7 @@ class OCUpdater : public Updater

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:
Expand Down