Add ability to respond to notifications on macOS

rnhmjoj
Joe Donofry 2022-11-04 16:42:09 +00:00
parent 9138119dc4
commit 07e8f64903
11 changed files with 307 additions and 99 deletions

View File

@ -13,3 +13,6 @@ KeepEmptyLinesAtTheStartOfBlocks: false
PointerAlignment: Right
Cpp11BracedListStyle: true
PenaltyReturnTypeOnItsOwnLine: 0
---
BasedOnStyle: WebKit
Language: ObjC

View File

@ -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/NotificationManagerProxy.h 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/NotificationManagerProxy.h 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

View File

@ -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)
{

View File

@ -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();

View File

@ -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);

View File

@ -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

View File

@ -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

View File

@ -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)

View File

@ -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);
}
}

View File

@ -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;
}
}
UNNotificationRequest* notificationRequest =
[UNNotificationRequest requestWithIdentifier:identifier
content:content
trigger:nil];
[center addNotificationRequest:notificationRequest
withCompletionHandler:^(NSError* _Nullable error) {
if (error != nil) {
NSLog(@"Unable to Add Notification Request: %@", [error localizedDescription]);
}
}];
[content autorelease];
}
}];
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:event_id.toNSString() content:content trigger:nil];
[center addNotificationRequest:notificationRequest withCompletionHandler:^(NSError * _Nullable error) {
if (error != nil) {
NSLog(@"Unable to Add Notification Request");
}
}];
[content autorelease];
}
//unused
void
NotificationsManager::actionInvoked(uint, QString)
void NotificationsManager::attachToMacNotifCenter()
{
UNUserNotificationCenter* center =
[UNUserNotificationCenter currentNotificationCenter];
std::unique_ptr<NotificationManagerProxy> proxy = std::make_unique<NotificationManagerProxy>();
connect(proxy.get(), &NotificationManagerProxy::notificationReplied, ChatPage::instance(), &ChatPage::sendNotificationReply);
MacNotificationDelegate* notifDelegate = [[MacNotificationDelegate alloc] initWithProxy:std::move(proxy)];
center.delegate = notifDelegate;
}
void
NotificationsManager::notificationReplied(uint, QString)
{
}
// unused
void NotificationsManager::actionInvoked(uint, QString) { }
void
NotificationsManager::notificationClosed(uint, uint)
{
}
void NotificationsManager::notificationReplied(uint, QString) { }
void
NotificationsManager::removeNotification(const QString &, const QString &)
{}
void NotificationsManager::notificationClosed(uint, uint) { }
void NotificationsManager::removeNotification(const QString&, const QString&) { }

View File

@ -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);
};