diff --git a/.clang-format b/.clang-format index e9ebcb6dfab781bbf1bc885805d6607ee235cbe0..adb43177111b4ad781be71f7e8097060abf49043 100644 --- a/.clang-format +++ b/.clang-format @@ -13,3 +13,6 @@ KeepEmptyLinesAtTheStartOfBlocks: false PointerAlignment: Right Cpp11BracedListStyle: true PenaltyReturnTypeOnItsOwnLine: 0 +--- +BasedOnStyle: WebKit +Language: ObjC \ No newline at end of file diff --git a/CMakeLists.txt b/CMakeLists.txt index 2b456b1a9f7cd97a4cf6d8eeafbae3bee912072b..8818fcaea37a56282d67650c885e98d379b529d0 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -629,9 +629,9 @@ set(TRANSLATION_DEPS ${LANG_QRC} ${QRC} ${QM_SRC}) if (APPLE) set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} -framework Foundation -framework Cocoa -framework UserNotifications") - set(SRC_FILES ${SRC_FILES} src/notifications/ManagerMac.mm src/notifications/ManagerMac.cpp src/emoji/MacHelper.mm src/emoji/MacHelper.h) + set(SRC_FILES ${SRC_FILES} src/notifications/MacNotificationDelegate.h src/notifications/MacNotificationDelegate.mm src/notifications/ManagerMac.mm src/notifications/ManagerMac.cpp src/emoji/MacHelper.mm src/emoji/MacHelper.h) if(${CMAKE_VERSION} VERSION_GREATER_EQUAL "3.16.0") - set_source_files_properties( src/notifications/ManagerMac.mm src/emoji/MacHelper.mm src/emoji/MacHelper.h PROPERTIES SKIP_PRECOMPILE_HEADERS ON) + set_source_files_properties( src/notifications/MacNotificationDelegate.h src/notifications/MacNotificationDelegate.mm src/notifications/ManagerMac.mm src/emoji/MacHelper.mm src/emoji/MacHelper.h PROPERTIES SKIP_PRECOMPILE_HEADERS ON) endif() elseif (WIN32) file(DOWNLOAD diff --git a/src/main.cpp b/src/main.cpp index 3937c6b77056896e35173b1a5280a6d720f8db48..d1b4b7699d5085181966db17c28fac5349c24d58 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 0000000000000000000000000000000000000000..8441d1981666021d20025a5db73357e71dd3f082 --- /dev/null +++ b/src/notifications/MacNotificationDelegate.h @@ -0,0 +1,10 @@ +#pragma once + +#include "notifications/Manager.h" +#include <mtx/responses/notifications.hpp> + +#import <Foundation/Foundation.h> +#import <UserNotifications/UserNotifications.h> + +@interface MacNotificationDelegate : NSObject <UNUserNotificationCenterDelegate> +@end diff --git a/src/notifications/MacNotificationDelegate.mm b/src/notifications/MacNotificationDelegate.mm new file mode 100644 index 0000000000000000000000000000000000000000..f81a29d15fc3d0da85e840627d4c81ec4f0a8bbe --- /dev/null +++ b/src/notifications/MacNotificationDelegate.mm @@ -0,0 +1,26 @@ +#import "notifications/MacNotificationDelegate.h" + +@implementation MacNotificationDelegate + +- (void)userNotificationCenter:(UNUserNotificationCenter*)center + didReceiveNotificationResponse:(UNNotificationResponse*)response + withCompletionHandler:(void (^)())completionHandler +{ + if ([response.actionIdentifier isEqualToString:@"ReplyAction"]) { + if ([response respondsToSelector:@selector(userText)]) { + NSString* textValue = [(UNTextInputNotificationResponse*)response userText]; + NSLog(@"%@", 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 8a5f172541f90f289c59b45159c4f899acaa7704..de678738cb383bc490cbf43a54a4515a9530e594 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 d5faaf59db8db15ac53444b73fc1612bf0547e1d..75ea838cbe2687444b474b202c003786e34afcf8 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 d5d900a7e4257839140a7d28dbb7296e2edbb0cc..7d76181793f95e521302dc7a862c09a4ca8166d4 100644 --- a/src/notifications/ManagerMac.mm +++ b/src/notifications/ManagerMac.mm @@ -1,112 +1,175 @@ +#include "notifications/MacNotificationDelegate.h" #include "notifications/Manager.h" -#import <Foundation/Foundation.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) -{ -} + MacNotificationDelegate* notifDelegate = [MacNotificationDelegate new]; -void -NotificationsManager::notificationClosed(uint, uint) -{ + 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&) { }