summary refs log tree commit diff
path: root/src
diff options
context:
space:
mode:
authorJoe Donofry <rubberduckie3554@gmail.com>2022-11-04 16:42:09 +0000
committerJoe Donofry <rubberduckie3554@gmail.com>2022-11-04 16:42:09 +0000
commit07e8f64903a6d4c496c7eb53f95325c92a427286 (patch)
tree1ba12259465d119621055bcd256ac2ebf1d66574 /src
parentFix reactions matching displayname condition by accident (diff)
downloadnheko-07e8f64903a6d4c496c7eb53f95325c92a427286.tar.xz
Add ability to respond to notifications on macOS
Diffstat (limited to 'src')
-rw-r--r--src/ChatPage.cpp24
-rw-r--r--src/ChatPage.h1
-rw-r--r--src/main.cpp5
-rw-r--r--src/notifications/MacNotificationDelegate.h20
-rw-r--r--src/notifications/MacNotificationDelegate.mm47
-rw-r--r--src/notifications/Manager.h8
-rw-r--r--src/notifications/ManagerMac.cpp37
-rw-r--r--src/notifications/ManagerMac.mm229
-rw-r--r--src/notifications/NotificationManagerProxy.h22
9 files changed, 299 insertions, 94 deletions
diff --git a/src/ChatPage.cpp b/src/ChatPage.cpp
index e40274cb..f87c2738 100644
--- a/src/ChatPage.cpp
+++ b/src/ChatPage.cpp
@@ -152,16 +152,7 @@ ChatPage::ChatPage(QSharedPointer<UserSettings> userSettings, QObject *parent)
     connect(notificationsManager,
             &NotificationsManager::sendNotificationReply,
             this,
-            [this](const QString &roomid, const QString &eventid, const QString &body) {
-                view_manager_->queueReply(roomid, eventid, body);
-                auto exWin = MainWindow::instance()->windowForRoom(roomid);
-                if (exWin) {
-                    exWin->requestActivate();
-                } else {
-                    view_manager_->rooms()->setCurrentRoom(roomid);
-                    MainWindow::instance()->requestActivate();
-                }
-            });
+            &ChatPage::sendNotificationReply);
 
     connect(
       this,
@@ -1583,6 +1574,19 @@ ChatPage::handleMatrixUri(QString uri)
     return false;
 }
 
+void
+ChatPage::sendNotificationReply(const QString &roomid, const QString &eventid, const QString &body)
+{
+    view_manager_->queueReply(roomid, eventid, body);
+    auto exWin = MainWindow::instance()->windowForRoom(roomid);
+    if (exWin) {
+        exWin->requestActivate();
+    } else {
+        view_manager_->rooms()->setCurrentRoom(roomid);
+        MainWindow::instance()->requestActivate();
+    }
+}
+
 bool
 ChatPage::handleMatrixUri(const QUrl &uri)
 {
diff --git a/src/ChatPage.h b/src/ChatPage.h
index 1bb25dc2..bae4401f 100644
--- a/src/ChatPage.h
+++ b/src/ChatPage.h
@@ -105,6 +105,7 @@ public slots:
     void receivedSessionKey(const std::string &room_id, const std::string &session_id);
     void decryptDownloadedSecrets(mtx::secret_storage::AesHmacSha2KeyDescription keyDesc,
                                   const SecretsToDecrypt &secrets);
+    void sendNotificationReply(const QString &roomid, const QString &eventid, const QString &body);
 signals:
     void connectionLost();
     void connectionRestored();
diff --git a/src/main.cpp b/src/main.cpp
index 3937c6b7..d1b4b769 100644
--- a/src/main.cpp
+++ b/src/main.cpp
@@ -33,6 +33,7 @@
 
 #if defined(Q_OS_MAC)
 #include "emoji/MacHelper.h"
+#include "notifications/Manager.h"
 #endif
 
 #if defined(GSTREAMER_AVAILABLE) && (defined(Q_OS_MAC) || defined(Q_OS_WINDOWS))
@@ -389,6 +390,10 @@ main(int argc, char *argv[])
     // Temporary solution for the emoji picker until
     // nheko has a proper menu bar with more functionality.
     MacHelper::initializeMenus();
+
+    // Need to set up notification delegate so users can respond to messages from within the
+    // notification itself.
+    NotificationsManager::attachToMacNotifCenter();
 #endif
 
     nhlog::ui()->info("starting nheko {}", nheko::version);
diff --git a/src/notifications/MacNotificationDelegate.h b/src/notifications/MacNotificationDelegate.h
new file mode 100644
index 00000000..e5bbe23b
--- /dev/null
+++ b/src/notifications/MacNotificationDelegate.h
@@ -0,0 +1,20 @@
+// SPDX-FileCopyrightText: 2021 Nheko Contributors
+// SPDX-FileCopyrightText: 2022 Nheko Contributors
+//
+// SPDX-License-Identifier: GPL-3.0-or-later
+
+#pragma once
+
+#include "notifications/Manager.h"
+#include "notifications/NotificationManagerProxy.h"
+#include <mtx/responses/notifications.hpp>
+
+#import <Foundation/Foundation.h>
+#import <UserNotifications/UserNotifications.h>
+
+@interface MacNotificationDelegate : NSObject <UNUserNotificationCenterDelegate> {
+    std::unique_ptr<NotificationManagerProxy> mProxy;
+}
+
+- (id)initWithProxy:(std::unique_ptr<NotificationManagerProxy>&&)proxy;
+@end
diff --git a/src/notifications/MacNotificationDelegate.mm b/src/notifications/MacNotificationDelegate.mm
new file mode 100644
index 00000000..9047efe3
--- /dev/null
+++ b/src/notifications/MacNotificationDelegate.mm
@@ -0,0 +1,47 @@
+// SPDX-FileCopyrightText: 2021 Nheko Contributors
+// SPDX-FileCopyrightText: 2022 Nheko Contributors
+//
+// SPDX-License-Identifier: GPL-3.0-or-later
+
+#import "notifications/MacNotificationDelegate.h"
+
+#include <QString.h>
+
+#include "ChatPage.h"
+
+@implementation MacNotificationDelegate
+
+- (id)initWithProxy: (std::unique_ptr<NotificationManagerProxy>&&)proxy
+{
+    if(self = [super init]) {
+        mProxy = std::move(proxy);
+    }
+
+    return self;
+}
+
+- (void)userNotificationCenter:(UNUserNotificationCenter*)center
+    didReceiveNotificationResponse:(UNNotificationResponse*)response
+             withCompletionHandler:(void (^)())completionHandler
+{
+    if ([response.actionIdentifier isEqualToString:@"ReplyAction"]) {
+        if ([response respondsToSelector:@selector(userText)]) {
+            UNTextInputNotificationResponse* textResponse = (UNTextInputNotificationResponse*)response;
+            NSString* textValue = [textResponse userText];
+            NSString* eventId = [[[textResponse notification] request] identifier];
+            NSString* roomId = [[[[textResponse notification] request] content] threadIdentifier];
+            mProxy->notificationReplied(QString::fromNSString(roomId), QString::fromNSString(eventId), QString::fromNSString(textValue));
+        }
+    }
+    completionHandler();
+}
+
+- (void)userNotificationCenter:(UNUserNotificationCenter*)center
+       willPresentNotification:(UNNotification*)notification
+         withCompletionHandler:(void (^)(UNNotificationPresentationOptions))completionHandler
+{
+
+    completionHandler(UNAuthorizationOptionAlert | UNAuthorizationOptionBadge | UNAuthorizationOptionSound);
+}
+
+@end
\ No newline at end of file
diff --git a/src/notifications/Manager.h b/src/notifications/Manager.h
index 8a5f1725..de678738 100644
--- a/src/notifications/Manager.h
+++ b/src/notifications/Manager.h
@@ -78,7 +78,13 @@ private:
                                 const QString &event_id,
                                 const QString &subtitle,
                                 const QString &informativeText,
-                                const QString &bodyImagePath);
+                                const QString &bodyImagePath,
+                                const QString &respondStr,
+                                const QString &sendStr,
+                                const QString &placeholder);
+
+public:
+    static void attachToMacNotifCenter();
 #endif
 
 #if defined(Q_OS_WINDOWS)
diff --git a/src/notifications/ManagerMac.cpp b/src/notifications/ManagerMac.cpp
index d5faaf59..75ea838c 100644
--- a/src/notifications/ManagerMac.cpp
+++ b/src/notifications/ManagerMac.cpp
@@ -40,12 +40,20 @@ NotificationsManager::postNotification(const mtx::responses::Notification &notif
     const auto isEncrypted = std::get_if<mtx::events::EncryptedEvent<mtx::events::msg::Encrypted>>(
                                &notification.event) != nullptr;
     const auto isReply = utils::isReply(notification.event);
+
+    // Putting these here to pass along since I'm not sure how
+    // our translate step interacts with .mm files
+    const auto respondStr  = QObject::tr("Respond");
+    const auto sendStr     = QObject::tr("Send");
+    const auto placeholder = QObject::tr("Write a message...");
+
     if (isEncrypted) {
         // TODO: decrypt this message if the decryption setting is on in the UserSettings
         const QString messageInfo = (isReply ? tr("%1 replied with an encrypted message")
                                              : tr("%1 sent an encrypted message"))
                                       .arg(sender);
-        objCxxPostNotification(room_name, room_id, event_id, messageInfo, "", "");
+        objCxxPostNotification(
+          room_name, room_id, event_id, messageInfo, "", "", respondStr, sendStr, placeholder);
     } else {
         const QString messageInfo =
           (isReply ? tr("%1 replied to a message") : tr("%1 sent a message")).arg(sender);
@@ -53,17 +61,34 @@ NotificationsManager::postNotification(const mtx::responses::Notification &notif
             MxcImageProvider::download(
               QString::fromStdString(mtx::accessors::url(notification.event)).remove("mxc://"),
               QSize(200, 80),
-              [this, notification, room_name, room_id, event_id, messageInfo](
-                QString, QSize, QImage, QString imgPath) {
+              [this,
+               notification,
+               room_name,
+               room_id,
+               event_id,
+               messageInfo,
+               respondStr,
+               sendStr,
+               placeholder](QString, QSize, QImage, QString imgPath) {
                   objCxxPostNotification(room_name,
                                          room_id,
                                          event_id,
                                          messageInfo,
                                          formatNotification(notification),
-                                         imgPath);
+                                         imgPath,
+                                         respondStr,
+                                         sendStr,
+                                         placeholder);
               });
         else
-            objCxxPostNotification(
-              room_name, room_id, event_id, messageInfo, formatNotification(notification), "");
+            objCxxPostNotification(room_name,
+                                   room_id,
+                                   event_id,
+                                   messageInfo,
+                                   formatNotification(notification),
+                                   "",
+                                   respondStr,
+                                   sendStr,
+                                   placeholder);
     }
 }
diff --git a/src/notifications/ManagerMac.mm b/src/notifications/ManagerMac.mm
index d5d900a7..4865e30d 100644
--- a/src/notifications/ManagerMac.mm
+++ b/src/notifications/ManagerMac.mm
@@ -1,112 +1,187 @@
+// SPDX-FileCopyrightText: 2021 Nheko Contributors
+// SPDX-FileCopyrightText: 2022 Nheko Contributors
+//
+// SPDX-License-Identifier: GPL-3.0-or-later
+
+#include "notifications/NotificationManagerProxy.h"
+#include "notifications/MacNotificationDelegate.h"
 #include "notifications/Manager.h"
 
-#import <Foundation/Foundation.h>
+#include "ChatPage.h"
+
 #import <AppKit/NSImage.h>
+#import <Foundation/Foundation.h>
 #import <UserNotifications/UserNotifications.h>
 
-#include <QtMac>
 #include <QImage>
+#include <QtMac>
 
 @interface UNNotificationAttachment (UNNotificationAttachmentAdditions)
-    + (UNNotificationAttachment *) createFromImageData:(NSData*)imgData identifier:(NSString *)imageFileIdentifier options:(NSDictionary*)attachmentOptions;
++ (UNNotificationAttachment*)createFromImageData:(NSData*)imgData
+                                      identifier:(NSString*)imageFileIdentifier
+                                         options:
+                                             (NSDictionary*)attachmentOptions;
 @end
 
 @implementation UNNotificationAttachment (UNNotificationAttachmentAdditions)
-    + (UNNotificationAttachment *) createFromImageData:(NSData*)imgData identifier:(NSString *)imageFileIdentifier options:(NSDictionary*)attachmentOptions {
-        NSFileManager *fileManager = [NSFileManager defaultManager];
-        NSString *tmpSubFolderName = [[NSProcessInfo processInfo] globallyUniqueString];
-        NSURL *tmpSubFolderURL = [NSURL fileURLWithPath:[NSTemporaryDirectory() stringByAppendingPathComponent:tmpSubFolderName] isDirectory:true];
-        NSError *error = nil;
-        [fileManager createDirectoryAtURL:tmpSubFolderURL withIntermediateDirectories:true attributes:nil error:&error];
-        if(error) {
-            NSLog(@"%@",[error localizedDescription]);
-            return nil;
-        }
-        NSURL *fileURL = [tmpSubFolderURL URLByAppendingPathComponent:imageFileIdentifier];
-        [imgData writeToURL:fileURL atomically:true];
-        UNNotificationAttachment *imageAttachment = [UNNotificationAttachment attachmentWithIdentifier:@"" URL:fileURL options:attachmentOptions error:&error];
-        if(error) {
-            NSLog(@"%@",[error localizedDescription]);
-            return nil;
-        }
-        return imageAttachment;
-
++ (UNNotificationAttachment*)createFromImageData:(NSData*)imgData
+                                      identifier:(NSString*)imageFileIdentifier
+                                         options:
+                                             (NSDictionary*)attachmentOptions
+{
+    NSFileManager* fileManager = [NSFileManager defaultManager];
+    NSString* tmpSubFolderName =
+        [[NSProcessInfo processInfo] globallyUniqueString];
+    NSURL* tmpSubFolderURL = [NSURL
+        fileURLWithPath:[NSTemporaryDirectory()
+                            stringByAppendingPathComponent:tmpSubFolderName]
+            isDirectory:true];
+    NSError* error = nil;
+    [fileManager createDirectoryAtURL:tmpSubFolderURL
+          withIntermediateDirectories:true
+                           attributes:nil
+                                error:&error];
+    if (error) {
+        NSLog(@"%@", [error localizedDescription]);
+        return nil;
     }
+    NSURL* fileURL =
+        [tmpSubFolderURL URLByAppendingPathComponent:imageFileIdentifier];
+    [imgData writeToURL:fileURL atomically:true];
+    UNNotificationAttachment* imageAttachment =
+        [UNNotificationAttachment attachmentWithIdentifier:@""
+                                                       URL:fileURL
+                                                   options:attachmentOptions
+                                                     error:&error];
+    if (error) {
+        NSLog(@"%@", [error localizedDescription]);
+        return nil;
+    }
+    return imageAttachment;
+}
 @end
 
-NotificationsManager::NotificationsManager(QObject *parent): QObject(parent)
+NotificationsManager::NotificationsManager(QObject* parent)
+    : QObject(parent)
 {
-
 }
 
-void
-NotificationsManager::objCxxPostNotification(const QString &room_name,
-                                             const QString &room_id,
-                                             const QString &event_id,
-                                             const QString &subtitle,
-                                             const QString &informativeText,
-                                             const QString &bodyImagePath)
+void NotificationsManager::objCxxPostNotification(
+    const QString& room_name,
+    const QString& room_id,
+    const QString& event_id,
+    const QString& subtitle,
+    const QString& informativeText,
+    const QString& bodyImagePath,
+    const QString& respondStr,
+    const QString& sendStr,
+    const QString& placeholder)
 {
+    // Request permissions for alerts (the generic type of notification), sound playback,
+    // and badges (which allows the Nheko app icon to show the little red bubble with unread count).
+    // NOTE: Possible macOS bug... the 'Play sound for notification checkbox' doesn't appear in
+    // the Notifications and Focus settings unless UNAuthorizationOptionBadges is also
+    // specified
     UNAuthorizationOptions options = UNAuthorizationOptionAlert + UNAuthorizationOptionSound + UNAuthorizationOptionBadge;
-    UNUserNotificationCenter *center = [UNUserNotificationCenter currentNotificationCenter];
+    UNUserNotificationCenter* center =
+        [UNUserNotificationCenter currentNotificationCenter];
 
+    // TODO: Move this somewhere that isn't dependent on receiving a notification
+    // to actually request notification access.
     [center requestAuthorizationWithOptions:options
-    completionHandler:^(BOOL granted, NSError * _Nullable error) {
-        if (!granted) {
-            NSLog(@"No notification access");
-            if (error) {
-                NSLog(@"%@",[error localizedDescription]);
+                          completionHandler:^(BOOL granted,
+                              NSError* _Nullable error) {
+                              if (!granted) {
+                                  NSLog(@"No notification access");
+                                  if (error) {
+                                      NSLog(@"%@", [error localizedDescription]);
+                                  }
+                              }
+                          }];
+
+    UNTextInputNotificationAction* replyAction = [UNTextInputNotificationAction actionWithIdentifier:@"ReplyAction"
+                                                                                               title:respondStr.toNSString()
+                                                                                             options:UNNotificationActionOptionNone
+                                                                                textInputButtonTitle:sendStr.toNSString()
+                                                                                textInputPlaceholder:placeholder.toNSString()];
+
+    UNNotificationCategory* category = [UNNotificationCategory categoryWithIdentifier:@"ReplyCategory"
+                                                                              actions:@[ replyAction ]
+                                                                    intentIdentifiers:@[]
+                                                                              options:UNNotificationCategoryOptionNone];
+
+    NSString* title = room_name.toNSString();
+    NSString* sub = subtitle.toNSString();
+    NSString* body = informativeText.toNSString();
+    NSString* threadIdentifier = room_id.toNSString();
+    NSString* identifier = event_id.toNSString();
+    NSString* imgUrl = bodyImagePath.toNSString();
+
+    NSSet* categories = [NSSet setWithObject:category];
+    [center setNotificationCategories:categories];
+    [center getNotificationSettingsWithCompletionHandler:^(
+        UNNotificationSettings* _Nonnull settings) {
+        if (settings.authorizationStatus == UNAuthorizationStatusAuthorized) {
+            UNMutableNotificationContent* content =
+                [[UNMutableNotificationContent alloc] init];
+
+            content.title = title;
+            content.subtitle = sub;
+            content.body = body;
+            content.sound = [UNNotificationSound defaultSound];
+            content.threadIdentifier = threadIdentifier;
+            content.categoryIdentifier = @"ReplyCategory";
+
+            if ([imgUrl length] != 0) {
+                NSURL* imageURL = [NSURL fileURLWithPath:imgUrl];
+                NSData* img = [NSData dataWithContentsOfURL:imageURL];
+                NSArray* attachments = [NSMutableArray array];
+                UNNotificationAttachment* attachment = [UNNotificationAttachment
+                    createFromImageData:img
+                             identifier:@"attachment_image.jpeg"
+                                options:nil];
+                if (attachment) {
+                    attachments = [NSMutableArray arrayWithObjects:attachment, nil];
+                    content.attachments = attachments;
+                }
             }
-        }
-    }];
 
-    UNMutableNotificationContent *content = [[UNMutableNotificationContent alloc] init];
-
-    content.title             = room_name.toNSString();
-    content.subtitle          = subtitle.toNSString();
-    content.body              = informativeText.toNSString();
-    content.sound             = [UNNotificationSound defaultSound];
-    content.threadIdentifier  = room_id.toNSString();
-
-    if (!bodyImagePath.isEmpty()) {
-        NSURL *imageURL = [NSURL fileURLWithPath:bodyImagePath.toNSString()];
-        NSData *img = [NSData dataWithContentsOfURL:imageURL];
-        NSArray *attachments = [NSMutableArray array];
-        UNNotificationAttachment *attachment = [UNNotificationAttachment createFromImageData:img identifier:@"attachment_image.jpeg" options:nil];
-        if (attachment) {
-            attachments = [NSMutableArray arrayWithObjects: attachment, nil];
-            content.attachments = attachments;
-        }
-    }
+            UNNotificationRequest* notificationRequest =
+                [UNNotificationRequest requestWithIdentifier:identifier
+                                                     content:content
+                                                     trigger:nil];
 
-    UNNotificationRequest *notificationRequest = [UNNotificationRequest requestWithIdentifier:event_id.toNSString() content:content trigger:nil];
+            [center addNotificationRequest:notificationRequest
+                     withCompletionHandler:^(NSError* _Nullable error) {
+                         if (error != nil) {
+                             NSLog(@"Unable to Add Notification Request: %@", [error localizedDescription]);
+                         }
+                     }];
 
-    [center addNotificationRequest:notificationRequest withCompletionHandler:^(NSError * _Nullable error) {
-        if (error != nil) {
-            NSLog(@"Unable to Add Notification Request");
+            [content autorelease];
         }
     }];
-
-    [content autorelease];
 }
 
-//unused
-void
-NotificationsManager::actionInvoked(uint, QString)
+void NotificationsManager::attachToMacNotifCenter()
 {
-}
+    UNUserNotificationCenter* center =
+        [UNUserNotificationCenter currentNotificationCenter];
 
-void
-NotificationsManager::notificationReplied(uint, QString)
-{
-}
+    std::unique_ptr<NotificationManagerProxy> proxy = std::make_unique<NotificationManagerProxy>();
 
-void
-NotificationsManager::notificationClosed(uint, uint)
-{
+    connect(proxy.get(), &NotificationManagerProxy::notificationReplied, ChatPage::instance(), &ChatPage::sendNotificationReply);
+
+    MacNotificationDelegate* notifDelegate = [[MacNotificationDelegate alloc] initWithProxy:std::move(proxy)];
+
+    center.delegate = notifDelegate;
 }
 
-void
-NotificationsManager::removeNotification(const QString &, const QString &)
-{}
+// unused
+void NotificationsManager::actionInvoked(uint, QString) { }
+
+void NotificationsManager::notificationReplied(uint, QString) { }
+
+void NotificationsManager::notificationClosed(uint, uint) { }
 
+void NotificationsManager::removeNotification(const QString&, const QString&) { }
diff --git a/src/notifications/NotificationManagerProxy.h b/src/notifications/NotificationManagerProxy.h
new file mode 100644
index 00000000..c7a2e234
--- /dev/null
+++ b/src/notifications/NotificationManagerProxy.h
@@ -0,0 +1,22 @@
+// SPDX-FileCopyrightText: 2021 Nheko Contributors
+// SPDX-FileCopyrightText: 2022 Nheko Contributors
+//
+// SPDX-License-Identifier: GPL-3.0-or-later
+
+#pragma once
+
+#include <QObject>
+#include <QString>
+
+class NotificationManagerProxy final : public QObject
+{
+    Q_OBJECT
+public:
+    NotificationManagerProxy(QObject *parent = nullptr)
+      : QObject(parent)
+    {
+    }
+
+signals:
+    void notificationReplied(const QString &room, const QString &event, const QString &reply);
+};
\ No newline at end of file