Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add preview of files to notifications #1306

Merged
merged 3 commits into from
Jul 24, 2023
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
164 changes: 124 additions & 40 deletions NotificationServiceExtension/NotificationService.m
Original file line number Diff line number Diff line change
Expand Up @@ -25,21 +25,26 @@
#import "NCAPISessionManager.h"
#import "NCAppBranding.h"
#import "NCDatabaseManager.h"
#import "NCImageSessionManager.h"
#import "NCIntentController.h"
#import "NCRoom.h"
#import "NCKeyChainController.h"
#import "NCNotification.h"
#import "NCPushNotification.h"
#import "NCPushNotificationsUtils.h"

#import "AFImageDownloader.h"
#import "NextcloudTalk-Swift.h"

#import <SDWebImage/SDWebImage.h>

typedef void (^CreateConversationNotificationCompletionBlock)(void);

@interface NotificationService ()

@property (nonatomic, strong) void (^contentHandler)(UNNotificationContent *contentToDeliver);
@property (nonatomic, strong) UNMutableNotificationContent *bestAttemptContent;
@property (nonatomic, strong) INSendMessageIntent *sendMessageIntent;

@end

Expand All @@ -48,25 +53,26 @@ @implementation NotificationService
- (void)didReceiveNotificationRequest:(UNNotificationRequest *)request withContentHandler:(void (^)(UNNotificationContent * _Nonnull))contentHandler {
self.contentHandler = contentHandler;
self.bestAttemptContent = [request.content mutableCopy];

self.sendMessageIntent = nil;

self.bestAttemptContent.title = @"";
self.bestAttemptContent.body = NSLocalizedString(@"You received a new notification", nil);

// Configure database
NSString *path = [[[[NSFileManager defaultManager] containerURLForSecurityApplicationGroupIdentifier:groupIdentifier] URLByAppendingPathComponent:kTalkDatabaseFolder] path];
NSURL *databaseURL = [[NSURL fileURLWithPath:path] URLByAppendingPathComponent:kTalkDatabaseFileName];

if ([[NSFileManager defaultManager] fileExistsAtPath:databaseURL.path]) {
@try {
NSError *error = nil;

// schemaVersionAtURL throws an exception when file is not readable
uint64_t currentSchemaVersion = [RLMRealm schemaVersionAtURL:databaseURL encryptionKey:nil error:&error];

if (error || currentSchemaVersion != kTalkDatabaseSchemaVersion) {
NSLog(@"Current schemaVersion is %llu app schemaVersion is %llu", currentSchemaVersion, kTalkDatabaseSchemaVersion);
NSLog(@"Database needs migration -> don't open database from extension");

self.contentHandler(self.bestAttemptContent);
return;
} else {
Expand All @@ -83,7 +89,7 @@ - (void)didReceiveNotificationRequest:(UNNotificationRequest *)request withConte
self.contentHandler(self.bestAttemptContent);
return;
}

RLMRealmConfiguration *configuration = [RLMRealmConfiguration defaultConfiguration];
configuration.fileURL = databaseURL;
configuration.schemaVersion= kTalkDatabaseSchemaVersion;
Expand All @@ -95,9 +101,9 @@ - (void)didReceiveNotificationRequest:(UNNotificationRequest *)request withConte

// We don't want to use a memory cache in NSE, because we only have a total of 24MB before we get killed by the OS
SDImageCache.sharedImageCache.config.shouldCacheImagesInMemory = NO;

BOOL foundDecryptableMessage = NO;

// Decrypt message
NSString *message = [self.bestAttemptContent.userInfo objectForKey:@"subject"];
for (TalkAccount *talkAccount in [TalkAccount allObjects]) {
Expand All @@ -108,16 +114,16 @@ - (void)didReceiveNotificationRequest:(UNNotificationRequest *)request withConte
NSString *decryptedMessage = [NCPushNotificationsUtils decryptPushNotification:message withDevicePrivateKey:pushNotificationPrivateKey];
if (decryptedMessage) {
NCPushNotification *pushNotification = [NCPushNotification pushNotificationFromDecryptedString:decryptedMessage withAccountId:account.accountId];

if (pushNotification.type == NCPushNotificationTypeAdminNotification) {
// Test notification send through "occ notification:test-push --talk <userid>"
// No need to increase the badge or query the server about it

self.bestAttemptContent.body = pushNotification.subject;
self.contentHandler(self.bestAttemptContent);
return;
}

foundDecryptableMessage = YES;

[[RLMRealm defaultRealm] transactionWithBlock:^{
Expand All @@ -133,23 +139,23 @@ - (void)didReceiveNotificationRequest:(UNNotificationRequest *)request withConte
managedAccount.lastNotificationId = pushNotification.notificationId;
}
}];

// Get the total number of unread notifications
NSInteger unreadNotifications = 0;
for (TalkAccount *user in [TalkAccount allObjects]) {
unreadNotifications += user.unreadBadgeNumber;
}

self.bestAttemptContent.body = pushNotification.bodyForRemoteAlerts;
self.bestAttemptContent.threadIdentifier = pushNotification.roomToken;
self.bestAttemptContent.sound = [UNNotificationSound defaultSound];
self.bestAttemptContent.badge = @(unreadNotifications);

if (pushNotification.type == NCPushNotificationTypeChat) {
// Set category for chat messages to allow interactive notifications
self.bestAttemptContent.categoryIdentifier = @"CATEGORY_CHAT";
}

NSMutableDictionary *userInfo = [[NSMutableDictionary alloc] init];
[userInfo setObject:pushNotification.jsonString forKey:@"pushNotification"];
[userInfo setObject:pushNotification.accountId forKey:@"accountId"];
Expand Down Expand Up @@ -177,7 +183,9 @@ - (void)didReceiveNotificationRequest:(UNNotificationRequest *)request withConte
NSString *userTokenString = [NSString stringWithFormat:@"%@:%@", account.user, [[NCKeyChainController sharedInstance] tokenForAccountId:account.accountId]];
NSData *data = [userTokenString dataUsingEncoding:NSUTF8StringEncoding];
NSString *base64Encoded = [data base64EncodedStringWithOptions:0];
[apiSessionManager.requestSerializer setValue:[[NSString alloc]initWithFormat:@"Basic %@",base64Encoded] forHTTPHeaderField:@"Authorization"];
NSString *authorizationHeader = [[NSString alloc] initWithFormat:@"Basic %@", base64Encoded];
[apiSessionManager.requestSerializer setValue:authorizationHeader forHTTPHeaderField:@"Authorization"];
[apiSessionManager.requestSerializer setTimeoutInterval:25];

[apiSessionManager GET:URLString parameters:nil progress:nil success:^(NSURLSessionDataTask * _Nonnull task, id _Nullable responseObject) {
NSDictionary *notification = [[responseObject objectForKey:@"ocs"] objectForKey:@"data"];
Expand All @@ -197,18 +205,55 @@ - (void)didReceiveNotificationRequest:(UNNotificationRequest *)request withConte

self.bestAttemptContent.title = serverNotification.chatMessageTitle;
self.bestAttemptContent.body = markdownMessage.string;
self.bestAttemptContent.summaryArgument = serverNotification.chatMessageAuthor;

NSDictionary *fileDict = [serverNotification.messageRichParameters objectForKey:@"file"];
if (fileDict) {
// First try to create the conversation notification, and only afterwards try to retrieve the image preview
[self createConversationNotificationWithPushNotification:pushNotification withCompletionBlock:^{
NSString *fileId = [fileDict objectForKey:@"id"];
NSString *urlString = [NSString stringWithFormat:@"%@/index.php/core/preview?fileId=%@&x=-1&y=%ld&a=1&forceIcon=1", account.server, fileId, 512L];

AFImageDownloader *downloader = [[AFImageDownloader alloc]
initWithSessionManager:[NCImageSessionManager sharedInstance]
downloadPrioritization:AFImageDownloadPrioritizationFIFO
maximumActiveDownloads:1
imageCache:nil];

NSString *userAgent = [NSString stringWithFormat:@"Mozilla/5.0 (iOS) Nextcloud-Talk v%@",
[[[NSBundle mainBundle] infoDictionary] objectForKey:@"CFBundleShortVersionString"]];

NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:[NSURL URLWithString:urlString]];
[request setValue:authorizationHeader forHTTPHeaderField:@"Authorization"];
[request setValue:userAgent forHTTPHeaderField:@"User-Agent"];
[request setTimeoutInterval:25];

[downloader downloadImageForURLRequest:request success:^(NSURLRequest * _Nonnull request, NSHTTPURLResponse * _Nullable response, UIImage * _Nonnull image) {
UNNotificationAttachment *attachment = [self getNotificationAttachmentFromImage:image forAccountId:account.accountId];

if (attachment) {
self.bestAttemptContent.attachments = @[attachment];
}

[self showBestAttemptNotification];
} failure:^(NSURLRequest * _Nonnull request, NSHTTPURLResponse * _Nullable response, NSError * _Nonnull error) {
[self showBestAttemptNotification];
}];
}];

// Stop here because the downloader completion blocks will take care of creating the conversation notification
return;
}

} else if (serverNotification.notificationType == kNCNotificationTypeRecording) {
self.bestAttemptContent.categoryIdentifier = @"CATEGORY_RECORDING";
self.bestAttemptContent.title = serverNotification.subject;
self.bestAttemptContent.body = serverNotification.message;
self.bestAttemptContent.summaryArgument = serverNotification.objectId;
}

[self createConversationNotificationWithPushNotification:pushNotification];
[self createConversationNotificationWithPushNotificationAndShow:pushNotification];
} failure:^(NSURLSessionDataTask * _Nullable task, NSError * _Nonnull error) {
// Even if the server request fails, we should try to create a conversation notifications
[self createConversationNotificationWithPushNotification:pushNotification];
[self createConversationNotificationWithPushNotificationAndShow:pushNotification];
}];
}
} @catch (NSException *exception) {
Expand All @@ -217,35 +262,74 @@ - (void)didReceiveNotificationRequest:(UNNotificationRequest *)request withConte
}
}
}

if (!foundDecryptableMessage) {
// At this point we tried everything to decrypt the received message
// No need to wait for the extension timeout, nothing is happening anymore
self.contentHandler(self.bestAttemptContent);
}
}

- (void)createConversationNotificationWithPushNotification:(NCPushNotification *)pushNotification {
if (@available(iOS 15.0, *)) {
NCRoom *room = [self roomWithToken:pushNotification.roomToken forAccountId:pushNotification.accountId];
- (void)createConversationNotificationWithPushNotification:(NCPushNotification *)pushNotification withCompletionBlock:(CreateConversationNotificationCompletionBlock)block {
// There's no reason to create a conversation notification, if we can't ever do something with it
if (!block) {
return;
}

if (room) {
[[NCIntentController sharedInstance] getInteractionForRoom:room withTitle:self.bestAttemptContent.title withCompletionBlock:^(INSendMessageIntent *sendMessageIntent) {
__block NSError *error;
NCRoom *room = [self roomWithToken:pushNotification.roomToken forAccountId:pushNotification.accountId];

if (sendMessageIntent) {
self.contentHandler([self.bestAttemptContent contentByUpdatingWithProvider:sendMessageIntent error:&error]);
} else {
NSLog(@"Did not receive sendMessageIntent -> showing non-communication notification");
self.contentHandler(self.bestAttemptContent);
}
}];

return;
}
if (room) {
[[NCIntentController sharedInstance] getInteractionForRoom:room withTitle:self.bestAttemptContent.title withCompletionBlock:^(INSendMessageIntent *sendMessageIntent) {
self.sendMessageIntent = sendMessageIntent;
block();
}];

return;
}

block();
}

- (void)createConversationNotificationWithPushNotificationAndShow:(NCPushNotification *)pushNotification
{
[self createConversationNotificationWithPushNotification:pushNotification withCompletionBlock:^{
[self showBestAttemptNotification];
}];
}

- (void)showBestAttemptNotification
{
// When we have a send message intent, we use it, otherwise we fall back to the non-conversation-notification one
if (self.sendMessageIntent) {
__block NSError *error;
self.contentHandler([self.bestAttemptContent contentByUpdatingWithProvider:self.sendMessageIntent error:&error]);
} else {
self.contentHandler(self.bestAttemptContent);
}
}

- (UNNotificationAttachment *)getNotificationAttachmentFromImage:(UIImage *)image forAccountId:(NSString *)accountId
{
NSString *encodedAccountId = [accountId stringByAddingPercentEncodingWithAllowedCharacters:NSCharacterSet.URLHostAllowedCharacterSet];
NSFileManager *fileManager = [NSFileManager defaultManager];
NSString *tempDirectoryPath = [NSTemporaryDirectory() stringByAppendingPathComponent:@"/download/"];
tempDirectoryPath = [tempDirectoryPath stringByAppendingPathComponent:encodedAccountId];

if (![fileManager fileExistsAtPath:tempDirectoryPath]) {
// Make sure our download directory exists
[fileManager createDirectoryAtPath:tempDirectoryPath withIntermediateDirectories:YES attributes:nil error:nil];
}

NSString *fileName = [NSString stringWithFormat:@"NotificationPreview_%@.jpg", [[NSUUID UUID] UUIDString]];
NSString *filePath = [tempDirectoryPath stringByAppendingPathComponent:fileName];

// Write the received image to the temporary directory and create the corresponding attachment object
if ([UIImageJPEGRepresentation(image, 1.0) writeToFile:filePath atomically:YES]) {
UNNotificationAttachment *attachment = [UNNotificationAttachment attachmentWithIdentifier:fileName URL:[NSURL fileURLWithPath:filePath] options:nil error:nil];
return attachment;
}

self.contentHandler(self.bestAttemptContent);
return nil;
}

- (NCRoom *)roomWithToken:(NSString *)token forAccountId:(NSString *)accountId
Expand All @@ -264,7 +348,7 @@ - (NCRoom *)roomWithToken:(NSString *)token forAccountId:(NSString *)accountId
- (void)serviceExtensionTimeWillExpire {
// Called just before the extension will be terminated by the system.
// Use this as an opportunity to deliver your "best attempt" at modified content, otherwise the original push payload will be used.
self.contentHandler(self.bestAttemptContent);
[self showBestAttemptNotification];
}

@end
Loading