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 6e1095b9..4dc219b4 100644
--- a/src/ChatPage.h
+++ b/src/ChatPage.h
@@ -110,6 +110,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 ¬if
const auto isEncrypted = std::get_if<mtx::events::EncryptedEvent<mtx::events::msg::Encrypted>>(
¬ification.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 ¬if
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
|