Skip to content
Snippets Groups Projects
Commit 07e8f649 authored by Joe Donofry's avatar Joe Donofry
Browse files

Add ability to respond to notifications on macOS

parent 9138119d
No related branches found
No related tags found
1 merge request!21Add ability to respond to notifications on macOS
......@@ -13,3 +13,6 @@ KeepEmptyLinesAtTheStartOfBlocks: false
PointerAlignment: Right
Cpp11BracedListStyle: true
PenaltyReturnTypeOnItsOwnLine: 0
---
BasedOnStyle: WebKit
Language: ObjC
\ No newline at end of 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
......
......@@ -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)
{
......
......@@ -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();
......
......@@ -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);
......
// 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
// 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
......@@ -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)
......
......@@ -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);
}
}
// 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&) { }
// 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
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment