917 lines
44 KiB
Objective-C
917 lines
44 KiB
Objective-C
//
|
|
// MLNotificationManager.m
|
|
// Monal
|
|
//
|
|
// Created by Anurodh Pokharel on 7/20/13.
|
|
//
|
|
//
|
|
|
|
#import "HelperTools.h"
|
|
#import "MLNotificationManager.h"
|
|
#import "MLImageManager.h"
|
|
#import "MLMessage.h"
|
|
#import "MLXEPSlashMeHandler.h"
|
|
#import "MLConstants.h"
|
|
#import "xmpp.h"
|
|
#import "MLFiletransfer.h"
|
|
#import "MLNotificationQueue.h"
|
|
#import "MLXMPPManager.h"
|
|
#import <monalxmpp/monalxmpp-Swift.h>
|
|
|
|
@import UserNotifications;
|
|
@import CoreServices;
|
|
@import Intents;
|
|
@import AVFoundation;
|
|
@import UniformTypeIdentifiers;
|
|
|
|
typedef NS_ENUM(NSUInteger, MLNotificationState) {
|
|
MLNotificationStateNone,
|
|
MLNotificationStatePending,
|
|
MLNotificationStateDelivered,
|
|
};
|
|
|
|
@interface MLNotificationManager ()
|
|
@property (nonatomic, readonly) NotificationPrivacySettingOption notificationPrivacySetting;
|
|
@end
|
|
|
|
@implementation MLNotificationManager
|
|
|
|
+(MLNotificationManager*) sharedInstance
|
|
{
|
|
static dispatch_once_t once;
|
|
static MLNotificationManager* sharedInstance;
|
|
dispatch_once(&once, ^{
|
|
sharedInstance = [MLNotificationManager new] ;
|
|
});
|
|
return sharedInstance;
|
|
}
|
|
|
|
-(id) init
|
|
{
|
|
self = [super init];
|
|
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(handleNewMessage:) name:kMonalNewMessageNotice object:nil];
|
|
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(handleFiletransferUpdate:) name:kMonalMessageFiletransferUpdateNotice object:nil];
|
|
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(handleDeletedMessage:) name:kMonalDeletedMessageNotice object:nil];
|
|
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(handleDisplayedMessages:) name:kMonalDisplayedMessagesNotice object:nil];
|
|
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(handleXMPPError:) name:kXMPPError object:nil];
|
|
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(handleContactRefresh:) name:kMonalContactRefresh object:nil];
|
|
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(handleContactRefresh:) name:kMonalContactRemoved object:nil];
|
|
return self;
|
|
}
|
|
|
|
-(NotificationPrivacySettingOption) notificationPrivacySetting
|
|
{
|
|
NotificationPrivacySettingOption value = (NotificationPrivacySettingOption)[[HelperTools defaultsDB] integerForKey:@"NotificationPrivacySetting"];
|
|
DDLogVerbose(@"Current NotificationPrivacySettingOption: %d", (int)value);
|
|
return value;
|
|
}
|
|
|
|
-(void) handleContactRefresh:(NSNotification*) notification
|
|
{
|
|
//these will not survive process switches, but that's enough for now
|
|
static NSMutableSet* displayed;
|
|
static NSMutableSet* removed;
|
|
static dispatch_once_t onceToken;
|
|
dispatch_once(&onceToken, ^{
|
|
displayed = [NSMutableSet new];
|
|
removed = [NSMutableSet new];
|
|
});
|
|
xmpp* xmppAccount = notification.object;
|
|
MLContact* contact = notification.userInfo[@"contact"];
|
|
NSString* idval = [NSString stringWithFormat:@"subscription(%@, %@)", contact.accountID, contact.contactJid];
|
|
|
|
//remove contact requests notification once the contact request has been accepted
|
|
if(!contact.hasIncomingContactRequest)
|
|
{
|
|
monal_void_block_t block = ^{
|
|
[[UNUserNotificationCenter currentNotificationCenter] getPendingNotificationRequestsWithCompletionHandler:^(NSArray* requests) {
|
|
for(UNNotificationRequest* request in requests)
|
|
if([request.identifier isEqualToString:idval])
|
|
{
|
|
DDLogVerbose(@"Removing pending handled subscription request notification with identifier '%@'...", idval);
|
|
[[UNUserNotificationCenter currentNotificationCenter] removePendingNotificationRequestsWithIdentifiers:@[idval]];
|
|
}
|
|
}];
|
|
[[UNUserNotificationCenter currentNotificationCenter] getDeliveredNotificationsWithCompletionHandler:^(NSArray* notifications) {
|
|
for(UNNotification* notification in notifications)
|
|
if([notification.request.identifier isEqualToString:idval])
|
|
{
|
|
DDLogVerbose(@"Removing delivered handled subscription request notification with identifier '%@'...", idval);
|
|
[[UNUserNotificationCenter currentNotificationCenter] removeDeliveredNotificationsWithIdentifiers:@[idval]];
|
|
}
|
|
}];
|
|
@synchronized(removed) {
|
|
[removed addObject:idval];
|
|
}
|
|
};
|
|
|
|
//only try to remove once
|
|
BOOL isContained = NO;
|
|
@synchronized(removed) {
|
|
isContained = [removed containsObject:idval];
|
|
}
|
|
if(!isContained)
|
|
{
|
|
//do this in its own thread because we don't want to block the main thread or other threads here (the removal can take ~50ms)
|
|
//but DON'T do this in the appex because this can try to mess with notifications after the parse queue was frozen (see appex code for explanation what this means)
|
|
if([HelperTools isAppExtension])
|
|
block();
|
|
else
|
|
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_LOW, 0), block);
|
|
}
|
|
|
|
//return because we don't want to display any contact request notification
|
|
return;
|
|
}
|
|
|
|
//don't alert twice
|
|
@synchronized(displayed) {
|
|
if([displayed containsObject:idval])
|
|
return;
|
|
}
|
|
|
|
UNMutableNotificationContent* content = [UNMutableNotificationContent new];
|
|
content.title = xmppAccount.connectionProperties.identity.jid;
|
|
content.body = [NSString stringWithFormat:NSLocalizedString(@"The user %@ (%@) wants to add you to their contact list", @""), contact.contactDisplayName, contact.contactJid];
|
|
content.threadIdentifier = [self threadIdentifierWithContact:contact];
|
|
content.categoryIdentifier = @"subscription";
|
|
//don't simply use contact directly to make sure we always use a freshly created up to date contact when unpacking the userInfo dict
|
|
content.userInfo = @{
|
|
@"fromContactJid": contact.contactJid,
|
|
@"fromContactAccountID": contact.accountID,
|
|
};
|
|
|
|
DDLogDebug(@"Publishing notification with id %@", idval);
|
|
[self publishNotificationContent:content withID:idval];
|
|
@synchronized(displayed) {
|
|
[displayed addObject:idval];
|
|
}
|
|
}
|
|
|
|
-(void) handleXMPPError:(NSNotification*) notification
|
|
{
|
|
//severe errors will be shown as notification (in addition to the banner shown if the app is in foreground)
|
|
if([notification.userInfo[@"isSevere"] boolValue])
|
|
{
|
|
xmpp* xmppAccount = notification.object;
|
|
DDLogError(@"SEVERE XMPP Error(%@): %@", xmppAccount.connectionProperties.identity.jid, notification.userInfo[@"message"]);
|
|
#ifdef IS_ALPHA
|
|
NSString* idval = [[NSUUID UUID] UUIDString];
|
|
#else
|
|
NSString* idval = xmppAccount.connectionProperties.identity.jid; //use this to only show the newest error notification per account
|
|
#endif
|
|
UNMutableNotificationContent* content = [UNMutableNotificationContent new];
|
|
content.title = xmppAccount.connectionProperties.identity.jid;
|
|
content.body = notification.userInfo[@"message"];
|
|
content.sound = [UNNotificationSound defaultSound];
|
|
UNNotificationRequest* request = [UNNotificationRequest requestWithIdentifier:idval content:content trigger:nil];
|
|
NSError* error = [HelperTools postUserNotificationRequest:request];
|
|
if(error)
|
|
DDLogError(@"Error posting xmppError notification: %@", error);
|
|
}
|
|
}
|
|
|
|
#pragma mark message signals
|
|
|
|
-(AnyPromise*) notificationStateForMessage:(MLMessage*) message
|
|
{
|
|
NSString* idval = [self identifierWithMessage:message];
|
|
NSMutableArray* promises = [NSMutableArray new];
|
|
|
|
[promises addObject:[AnyPromise promiseWithResolverBlock:^(PMKResolver resolve) {
|
|
DDLogVerbose(@"Checking for 'pending' notification state for '%@'...", idval);
|
|
[[UNUserNotificationCenter currentNotificationCenter] getPendingNotificationRequestsWithCompletionHandler:^(NSArray* requests) {
|
|
for(UNNotificationRequest* request in requests)
|
|
if([request.identifier isEqualToString:idval])
|
|
{
|
|
DDLogDebug(@"Notification state 'pending' for: %@", idval);
|
|
resolve(@(MLNotificationStatePending));
|
|
return;
|
|
}
|
|
resolve(@(MLNotificationStateNone));
|
|
}];
|
|
}]];
|
|
|
|
[promises addObject:[AnyPromise promiseWithResolverBlock:^(PMKResolver resolve) {
|
|
DDLogVerbose(@"Checking for 'delivered' notification state for '%@'...", idval);
|
|
[[UNUserNotificationCenter currentNotificationCenter] getDeliveredNotificationsWithCompletionHandler:^(NSArray* notifications) {
|
|
for(UNNotification* notification in notifications)
|
|
if([notification.request.identifier isEqualToString:idval])
|
|
{
|
|
DDLogDebug(@"Notification state 'delivered' for: %@", idval);
|
|
resolve(@(MLNotificationStateDelivered));
|
|
return;
|
|
}
|
|
resolve(@(MLNotificationStateNone));
|
|
}];
|
|
}]];
|
|
|
|
|
|
return PMKWhen(promises).then(^(NSArray* results) {
|
|
DDLogVerbose(@"Notification state check for '%@' completed...", idval);
|
|
for(NSNumber* entry in results)
|
|
if(entry.integerValue != MLNotificationStateNone)
|
|
return entry;
|
|
return @(MLNotificationStateNone);
|
|
});
|
|
}
|
|
|
|
-(void) handleFiletransferUpdate:(NSNotification*) notification
|
|
{
|
|
xmpp* xmppAccount = notification.object;
|
|
MLMessage* message = [notification.userInfo objectForKey:@"message"];
|
|
NSString* idval = [self identifierWithMessage:message];
|
|
//do this asynchronously on a background thread
|
|
[self notificationStateForMessage:message].thenInBackground(^(NSNumber* _state) {
|
|
MLNotificationState state = _state.integerValue;
|
|
if(state == MLNotificationStatePending)
|
|
{
|
|
DDLogDebug(@"Already pending or unknown notification '%@', updating/posting it...", idval);
|
|
[self internalMessageHandlerWithMessage:message andAccount:xmppAccount showAlert:YES andSound:YES andLMCReplaced:NO];
|
|
}
|
|
else if(state == MLNotificationStateDelivered)
|
|
{
|
|
DDLogDebug(@"Already displayed notification '%@', updating it...", idval);
|
|
[self internalMessageHandlerWithMessage:message andAccount:xmppAccount showAlert:YES andSound:NO andLMCReplaced:NO];
|
|
}
|
|
});
|
|
}
|
|
|
|
-(void) handleNewMessage:(NSNotification*) notification
|
|
{
|
|
xmpp* xmppAccount = notification.object;
|
|
MLMessage* message = [notification.userInfo objectForKey:@"message"];
|
|
BOOL showAlert = notification.userInfo[@"showAlert"] ? [notification.userInfo[@"showAlert"] boolValue] : NO;
|
|
BOOL LMCReplaced = notification.userInfo[@"LMCReplaced"] ? [notification.userInfo[@"LMCReplaced"] boolValue] : NO;
|
|
[self internalMessageHandlerWithMessage:message andAccount:xmppAccount showAlert:showAlert andSound:YES andLMCReplaced:LMCReplaced];
|
|
}
|
|
|
|
-(void) internalMessageHandlerWithMessage:(MLMessage*) message andAccount:(xmpp*) xmppAccount showAlert:(BOOL) showAlert andSound:(BOOL) sound andLMCReplaced:(BOOL) LMCReplaced
|
|
{
|
|
if([message.messageType isEqualToString:kMessageTypeStatus])
|
|
return;
|
|
DDLogVerbose(@"notification manager should show notification for: %@", message.messageText);
|
|
if(!showAlert)
|
|
{
|
|
DDLogDebug(@"not showing notification: showAlert is NO");
|
|
return;
|
|
}
|
|
|
|
BOOL muted = [[DataLayer sharedInstance] isMutedJid:message.buddyName onAccount:message.accountID];
|
|
if(!muted && message.isMuc == YES && [[DataLayer sharedInstance] isMucAlertOnMentionOnly:message.buddyName onAccount:message.accountID])
|
|
{
|
|
NSString* displayName = [MLContact ownDisplayNameForAccount:xmppAccount];
|
|
NSString* ownJid = xmppAccount.connectionProperties.identity.jid;
|
|
NSString* userPart = [HelperTools splitJid:ownJid][@"user"];
|
|
NSString* nick = [[DataLayer sharedInstance] ownNickNameforMuc:message.buddyName forAccount:message.accountID];
|
|
if(!(
|
|
[message.messageText localizedCaseInsensitiveContainsString:nick] ||
|
|
[message.messageText localizedCaseInsensitiveContainsString:displayName] ||
|
|
[message.messageText localizedCaseInsensitiveContainsString:userPart] ||
|
|
[message.messageText localizedCaseInsensitiveContainsString:ownJid]
|
|
))
|
|
muted = YES;
|
|
}
|
|
if(muted)
|
|
{
|
|
DDLogDebug(@"not showing notification: this contact got muted");
|
|
return;
|
|
}
|
|
|
|
//check if we need to replace the still displayed notification or ignore this LMC
|
|
if(LMCReplaced)
|
|
{
|
|
NSString* idval = [self identifierWithMessage:message];
|
|
//wait synchronous for completion (needed for appex)
|
|
MLNotificationState state = PMKHangEnum([self notificationStateForMessage:message]);
|
|
DDLogVerbose(@"Notification state for '%@': %@", idval, @(state));
|
|
if(state == MLNotificationStateNone)
|
|
{
|
|
DDLogDebug(@"not showing notification for LMC: this notification was already removed earlier");
|
|
return;
|
|
}
|
|
}
|
|
|
|
if([HelperTools isNotInFocus])
|
|
{
|
|
DDLogVerbose(@"notification manager should show notification in background: %@", message.messageText);
|
|
[self showNotificationForMessage:message withSound:sound andAccount:xmppAccount];
|
|
}
|
|
else
|
|
{
|
|
//don't show notifications for open chats
|
|
if(![message isEqualToContact:self.currentContact])
|
|
{
|
|
DDLogVerbose(@"notification manager should show notification in foreground: %@", message.messageText);
|
|
[self showNotificationForMessage:message withSound:sound andAccount:xmppAccount];
|
|
}
|
|
else
|
|
{
|
|
DDLogDebug(@"not showing notification and only playing sound: chat is open");
|
|
[self playNotificationSoundForMessage:message withSound:sound andAccount:xmppAccount];
|
|
}
|
|
}
|
|
}
|
|
|
|
-(void) handleDisplayedMessages:(NSNotification*) notification
|
|
{
|
|
UNUserNotificationCenter* center = [UNUserNotificationCenter currentNotificationCenter];
|
|
NSArray<MLMessage*>* messages = [notification.userInfo objectForKey:@"messagesArray"];
|
|
DDLogVerbose(@"notification manager got displayed messages notice with %lu entries", [messages count]);
|
|
|
|
monal_void_block_t block = ^{
|
|
for(MLMessage* msg in messages)
|
|
{
|
|
if([msg.messageType isEqualToString:kMessageTypeStatus])
|
|
return;
|
|
|
|
NSString* idval = [self identifierWithMessage:msg];
|
|
|
|
DDLogVerbose(@"Removing pending/delivered notification for message '%@' with identifier '%@'...", msg.messageId, idval);
|
|
[center removePendingNotificationRequestsWithIdentifiers:@[idval]];
|
|
[center removeDeliveredNotificationsWithIdentifiers:@[idval]];
|
|
}
|
|
};
|
|
|
|
//do this in its own thread because we don't want to block the main thread or other threads here (the removal can take ~50ms)
|
|
//but DON'T do this in the appex because this can try to mess with notifications after the parse queue was frozen (see appex code for explanation what this means)
|
|
if([HelperTools isAppExtension])
|
|
block();
|
|
else
|
|
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_LOW, 0), block);
|
|
|
|
//update app badge
|
|
[[MLNotificationQueue currentQueue] postNotificationName:kMonalUpdateUnread object:nil];
|
|
|
|
}
|
|
|
|
-(void) handleDeletedMessage:(NSNotification*) notification
|
|
{
|
|
UNUserNotificationCenter* center = [UNUserNotificationCenter currentNotificationCenter];
|
|
MLMessage* message = [notification.userInfo objectForKey:@"message"];
|
|
|
|
if([message.messageType isEqualToString:kMessageTypeStatus])
|
|
return;
|
|
|
|
NSString* idval = [self identifierWithMessage:message];
|
|
|
|
DDLogVerbose(@"notification manager got deleted message notice: %@", message.messageId);
|
|
[center removePendingNotificationRequestsWithIdentifiers:@[idval]];
|
|
[center removeDeliveredNotificationsWithIdentifiers:@[idval]];
|
|
|
|
//update app badge
|
|
[[MLNotificationQueue currentQueue] postNotificationName:kMonalUpdateUnread object:nil];
|
|
}
|
|
|
|
-(NSString*) identifierWithMessage:(MLMessage*) message
|
|
{
|
|
return [NSString stringWithFormat:@"message(%@, %@)", [self threadIdentifierWithMessage:message], message.messageId];
|
|
}
|
|
|
|
-(NSString*) threadIdentifierWithMessage:(MLMessage*) message
|
|
{
|
|
return [NSString stringWithFormat:@"thread(%@, %@)", message.accountID, message.buddyName];
|
|
}
|
|
|
|
-(NSString*) threadIdentifierWithContact:(MLContact*) contact
|
|
{
|
|
return [NSString stringWithFormat:@"thread(%@, %@)", contact.accountID, contact.contactJid];
|
|
}
|
|
|
|
-(UNMutableNotificationContent*) updateBadgeForContent:(UNMutableNotificationContent*) content
|
|
{
|
|
NSNumber* unreadMsgCnt = [[DataLayer sharedInstance] countUnreadMessages];
|
|
DDLogVerbose(@"Raw badge value: %@", unreadMsgCnt);
|
|
content.badge = unreadMsgCnt;
|
|
return content;
|
|
}
|
|
|
|
-(void) publishNotificationContent:(UNNotificationContent*) content withID:(NSString*) idval
|
|
{
|
|
//scheduling the notification in 2 seconds will make it possible to be deleted by XEP-0333 chat-markers received directly after the message
|
|
//this is useful in catchup scenarios
|
|
DDLogVerbose(@"notification manager: publishing notification in 2 seconds: %@", content);
|
|
UNNotificationRequest* request = [UNNotificationRequest requestWithIdentifier:idval content:content trigger:[UNTimeIntervalNotificationTrigger triggerWithTimeInterval:2 repeats: NO]];
|
|
NSError* error = [HelperTools postUserNotificationRequest:request];
|
|
if(error)
|
|
DDLogError(@"Error posting local notification: %@", error);
|
|
}
|
|
|
|
-(void) playNotificationSoundForMessage:(MLMessage*) message withSound:(BOOL) sound andAccount:(xmpp*) account
|
|
{
|
|
UNMutableNotificationContent* content = [UNMutableNotificationContent new];
|
|
NSString* idval = [self identifierWithMessage:message];
|
|
|
|
if(sound && [[HelperTools defaultsDB] boolForKey:@"Sound"])
|
|
{
|
|
NSString* filename = [[HelperTools defaultsDB] objectForKey:@"AlertSoundFile"];
|
|
if(filename)
|
|
{
|
|
content.sound = [UNNotificationSound soundNamed:[NSString stringWithFormat:@"AlertSounds/%@.aif", filename]];
|
|
DDLogDebug(@"Using user configured alert sound: %@", content.sound);
|
|
}
|
|
else
|
|
{
|
|
content.sound = [UNNotificationSound defaultSound];
|
|
DDLogDebug(@"Using default alert sound: %@", content.sound);
|
|
}
|
|
}
|
|
else
|
|
DDLogDebug(@"Using no alert sound");
|
|
|
|
DDLogDebug(@"Publishing sound-but-no-body notification with id %@", idval);
|
|
[self publishNotificationContent:[self updateBadgeForContent:content] withID:idval];
|
|
}
|
|
|
|
-(void) showNotificationForMessage:(MLMessage*) message withSound:(BOOL) sound andAccount:(xmpp*) account
|
|
{
|
|
// always use legacy notifications if we should only show a generic "New Message" notifiation without name or content
|
|
if(self.notificationPrivacySetting > NotificationPrivacySettingOptionDisplayOnlyName)
|
|
return [self showLegacyNotificationForMessage:message withSound:sound];
|
|
|
|
return [self showModernNotificationForMessage:message withSound:sound andAccount:account];
|
|
}
|
|
|
|
-(void) showModernNotificationForMessage:(MLMessage*) message withSound:(BOOL) sound andAccount:(xmpp*) account
|
|
{
|
|
UNMutableNotificationContent* content = [UNMutableNotificationContent new];
|
|
NSString* idval = [self identifierWithMessage:message];
|
|
|
|
INSendMessageAttachment* audioAttachment = nil;
|
|
NSString* msgText = NSLocalizedString(@"Open app to see more", @"");
|
|
|
|
//only show msgText if allowed
|
|
if(self.notificationPrivacySetting == NotificationPrivacySettingOptionDisplayNameAndMessage)
|
|
{
|
|
//XEP-0245: The slash me Command
|
|
if([message.messageText hasPrefix:@"/me "])
|
|
msgText = [[MLXEPSlashMeHandler sharedInstance] stringSlashMeWithMessage:message];
|
|
else
|
|
msgText = message.messageText;
|
|
|
|
//notification settings
|
|
content.threadIdentifier = [self threadIdentifierWithMessage:message];
|
|
content.categoryIdentifier = @"message";
|
|
|
|
//user info for answer etc.
|
|
//don't simply use contact directly to make sure we always use a freshly created up to date contact when unpacking the userInfo dict
|
|
content.userInfo = @{
|
|
@"fromContactJid": message.buddyName,
|
|
@"fromContactAccountID": message.accountID,
|
|
@"messageId": message.messageId
|
|
};
|
|
|
|
if([message.messageType isEqualToString:kMessageTypeFiletransfer])
|
|
{
|
|
NSDictionary* info = [MLFiletransfer getFileInfoForMessage:message];
|
|
if(info)
|
|
{
|
|
NSString* mimeType = info[@"mimeType"];
|
|
|
|
if([mimeType hasPrefix:@"image/"])
|
|
msgText = NSLocalizedString(@"📷 An Image", @"");
|
|
else if([mimeType hasPrefix:@"audio/"])
|
|
msgText = NSLocalizedString(@"🎵 An Audiomessage", @"");
|
|
else if([mimeType hasPrefix:@"video/"])
|
|
msgText = NSLocalizedString(@"🎥 A Video", @"");
|
|
else if([mimeType isEqualToString:@"application/pdf"])
|
|
msgText = NSLocalizedString(@"📄 A Document", @"");
|
|
else
|
|
msgText = NSLocalizedString(@"📁 A File", @"");
|
|
|
|
if(![info[@"needsDownloading"] boolValue])
|
|
{
|
|
if([mimeType hasPrefix:@"image/"])
|
|
{
|
|
UNNotificationAttachment* attachment;
|
|
UTType* typeHint = [UTType typeWithMIMEType:mimeType];
|
|
if(typeHint == nil)
|
|
typeHint = UTTypeImage;
|
|
attachment = [self createNotificationAttachmentForFileInfo:info havingTypeHint:typeHint];
|
|
if(attachment)
|
|
content.attachments = @[attachment];
|
|
}
|
|
else if([mimeType hasPrefix:@"audio/"])
|
|
{
|
|
UNNotificationAttachment* attachment;
|
|
UTType* typeHint = [UTType typeWithMIMEType:mimeType];
|
|
if(typeHint == nil)
|
|
typeHint = UTTypeAudio;
|
|
audioAttachment = [INSendMessageAttachment attachmentWithAudioMessageFile:[INFile fileWithFileURL:[NSURL fileURLWithPath:info[@"cacheFile"]] filename:info[@"filename"] typeIdentifier:typeHint.identifier]];
|
|
DDLogVerbose(@"Added audio attachment(%@ = %@): %@", mimeType, typeHint, audioAttachment);
|
|
attachment = [self createNotificationAttachmentForFileInfo:info havingTypeHint:typeHint];
|
|
if(attachment)
|
|
content.attachments = @[attachment];
|
|
}
|
|
else if([mimeType hasPrefix:@"video/"])
|
|
{
|
|
UNNotificationAttachment* attachment;
|
|
UTType* typeHint = [UTType typeWithMIMEType:mimeType];
|
|
if(typeHint == nil)
|
|
typeHint = UTTypeMovie;
|
|
attachment = [self createNotificationAttachmentForFileInfo:info havingTypeHint:typeHint];
|
|
if(attachment)
|
|
content.attachments = @[attachment];
|
|
}
|
|
}
|
|
}
|
|
else
|
|
{
|
|
// empty info dict default to "Sent a file"
|
|
DDLogWarn(@"Got filetransfer with unknown type");
|
|
msgText = NSLocalizedString(@"📁 A File", @"");
|
|
}
|
|
}
|
|
else if([message.messageType isEqualToString:kMessageTypeUrl] && [[HelperTools defaultsDB] boolForKey:@"ShowURLPreview"])
|
|
msgText = NSLocalizedString(@"🔗 A Link", @"");
|
|
else if([message.messageType isEqualToString:kMessageTypeGeo])
|
|
msgText = NSLocalizedString(@"📍 A Location", @"");
|
|
}
|
|
content.body = msgText; //save message text to notification content
|
|
|
|
if(sound && [[HelperTools defaultsDB] boolForKey:@"Sound"])
|
|
{
|
|
NSString* filename = [[HelperTools defaultsDB] objectForKey:@"AlertSoundFile"];
|
|
if(filename)
|
|
{
|
|
content.sound = [UNNotificationSound soundNamed:[NSString stringWithFormat:@"AlertSounds/%@.aif", filename]];
|
|
DDLogDebug(@"Using user configured alert sound: %@", content.sound);
|
|
}
|
|
else
|
|
{
|
|
content.sound = [UNNotificationSound defaultSound];
|
|
DDLogDebug(@"Using default alert sound: %@", content.sound);
|
|
}
|
|
}
|
|
else
|
|
DDLogDebug(@"Using no alert sound");
|
|
|
|
// update badge value prior to donating the interaction to sirikit
|
|
[self updateBadgeForContent:content];
|
|
|
|
INSendMessageIntent* intent = [self makeIntentForMessage:message usingText:msgText andAudioAttachment:audioAttachment direction:INInteractionDirectionIncoming];
|
|
|
|
INInteraction* interaction = [[INInteraction alloc] initWithIntent:intent response:nil];
|
|
interaction.direction = INInteractionDirectionIncoming;
|
|
|
|
NSError* error = nil;
|
|
UNNotificationContent* updatedContent = [content contentByUpdatingWithProvider:intent error:&error];
|
|
if(error)
|
|
DDLogError(@"Could not update notification content: %@", error);
|
|
else
|
|
{
|
|
DDLogDebug(@"Publishing communication notification with id %@", idval);
|
|
[self publishNotificationContent:updatedContent withID:idval];
|
|
}
|
|
|
|
//we can donate interactions after posting their notification (see signal source code)
|
|
[interaction donateInteractionWithCompletion:^(NSError *error) {
|
|
if(error)
|
|
DDLogError(@"Could not donate interaction: %@", error);
|
|
}];
|
|
}
|
|
|
|
-(void) donateInteractionForOutgoingDBId:(NSNumber*) messageDBId
|
|
{
|
|
MLMessage* message = [[DataLayer sharedInstance] messageForHistoryID:messageDBId];
|
|
INSendMessageIntent* intent = [self makeIntentForMessage:message usingText:@"dummyText" andAudioAttachment:nil direction:INInteractionDirectionOutgoing];
|
|
INInteraction* interaction = [[INInteraction alloc] initWithIntent:intent response:nil];
|
|
interaction.direction = INInteractionDirectionOutgoing;
|
|
interaction.identifier = [NSString stringWithFormat:@"%@|%@", message.accountID, message.buddyName];
|
|
[interaction donateInteractionWithCompletion:^(NSError *error) {
|
|
if(error)
|
|
DDLogError(@"Could not donate outgoing interaction: %@", error);
|
|
}];
|
|
}
|
|
|
|
-(INSendMessageIntent*) makeIntentForMessage:(MLMessage*) message usingText:(NSString*) msgText andAudioAttachment:(INSendMessageAttachment*) audioAttachment direction:(INInteractionDirection) direction
|
|
{
|
|
// some docu:
|
|
// - https://developer.apple.com/documentation/usernotifications/implementing_communication_notifications?language=objc
|
|
// - https://gist.github.com/Dexwell/dedef7389eae26c5b9db927dc5588905
|
|
// - https://stackoverflow.com/a/68705169/3528174
|
|
xmpp* account = [[MLXMPPManager sharedInstance] getEnabledAccountForID:message.accountID];
|
|
MLContact* contact = [MLContact createContactFromJid:message.buddyName andAccountID:message.accountID];
|
|
INPerson* sender = nil;
|
|
NSString* groupDisplayName = nil;
|
|
NSMutableArray* recipients = [NSMutableArray new];
|
|
if(message.isMuc)
|
|
{
|
|
groupDisplayName = contact.contactDisplayName;
|
|
//we don't need different handling of incoming or outgoing messages for non-anon mucs because sender and receiver always contain the right contacts
|
|
if([kMucTypeGroup isEqualToString:message.mucType] && message.participantJid)
|
|
{
|
|
MLContact* contactInGroup = [MLContact createContactFromJid:message.participantJid andAccountID:message.accountID];
|
|
//use MLMessage's capability to calculate the fallback name using actualFrom
|
|
sender = [self makeINPersonWithContact:contactInGroup andDisplayName:message.contactDisplayName andAccount:account];
|
|
|
|
//add other group members
|
|
for(NSDictionary* member in [[DataLayer sharedInstance] getMembersAndParticipantsOfMuc:message.buddyName forAccountID:message.accountID])
|
|
{
|
|
MLContact* contactInGroup = [MLContact createContactFromJid:emptyDefault(member[@"participant_jid"], @"", member[@"member_jid"]) andAccountID:message.accountID];
|
|
[recipients addObject:[self makeINPersonWithContact:contactInGroup andDisplayName:member[@"room_nick"] andAccount:account]];
|
|
}
|
|
}
|
|
else
|
|
{
|
|
//in anon mucs we have to flip sender and receiver to make sure iOS handles them correctly
|
|
if(direction == INInteractionDirectionIncoming)
|
|
{
|
|
//use MLMessage's capability to calculate the fallback name using actualFrom
|
|
sender = [self makeINPersonWithContact:contact andDisplayName:message.contactDisplayName andAccount:account];
|
|
//the next 2 lines are needed to make iOS show the group name in notifications
|
|
[recipients addObject:[self makeINPersonForOwnAccount:account]];
|
|
[recipients addObject:sender];
|
|
}
|
|
else
|
|
{
|
|
//we always need a sender (that's us in the outgoing case)
|
|
sender = [self makeINPersonForOwnAccount:account];
|
|
//use MLMessage's capability to calculate the fallback name using actualFrom
|
|
[recipients addObject:[self makeINPersonWithContact:contact andDisplayName:message.contactDisplayName andAccount:account]];
|
|
[recipients addObject:sender]; //match the recipients array for the incoming case above
|
|
}
|
|
}
|
|
}
|
|
else
|
|
{
|
|
//in 1:1 messages we have to flip sender and receiver to make sure iOS adds the correct share suggestions to its list
|
|
if(direction == INInteractionDirectionIncoming)
|
|
{
|
|
sender = [self makeINPersonWithContact:contact andDisplayName:nil andAccount:account];
|
|
[recipients addObject:[self makeINPersonForOwnAccount:account]];
|
|
}
|
|
else
|
|
{
|
|
sender = [self makeINPersonForOwnAccount:account];
|
|
[recipients addObject:[self makeINPersonWithContact:contact andDisplayName:nil andAccount:account]];
|
|
}
|
|
}
|
|
|
|
//DDLogDebug(@"Creating INSendMessageIntent with recipients=%@, speakableGroupName=%@, sender=%@", recipients, groupDisplayName, sender);
|
|
INSendMessageIntent* intent = [[INSendMessageIntent alloc] initWithRecipients:recipients
|
|
outgoingMessageType:(audioAttachment ? INOutgoingMessageTypeOutgoingMessageAudio : INOutgoingMessageTypeOutgoingMessageText)
|
|
content:msgText
|
|
speakableGroupName:(groupDisplayName ? [[INSpeakableString alloc] initWithSpokenPhrase:groupDisplayName] : nil)
|
|
conversationIdentifier:[[NSString alloc] initWithData:[HelperTools serializeObject:contact] encoding:NSISOLatin1StringEncoding]
|
|
serviceName:message.accountID.stringValue
|
|
sender:sender
|
|
attachments:(audioAttachment ? @[audioAttachment] : @[])];
|
|
//DDLogDebug(@"Intent is now: %@", intent);
|
|
if(message.isMuc)
|
|
{
|
|
if(contact.avatar != nil)
|
|
{
|
|
DDLogDebug(@"Using muc avatar image: %@", contact.avatar);
|
|
[intent setImage:[INImage imageWithImageData:UIImagePNGRepresentation(contact.avatar)] forParameterNamed:@"speakableGroupName"];
|
|
}
|
|
else
|
|
DDLogDebug(@"NOT using avatar image...");
|
|
}
|
|
|
|
return intent;
|
|
|
|
/*
|
|
if(message.isMuc)
|
|
{
|
|
[intent setImage:avatar forParameterNamed:"speakableGroupName"];
|
|
[intent setImage:avatar forParameterNamed:"sender"];
|
|
}
|
|
else
|
|
[intent setImage:avatar forParameterNamed:"sender"];
|
|
*/
|
|
|
|
/*
|
|
INCallRecord* callRecord = [[INCallRecord alloc] initWithIdentifier:[self threadIdentifierWithMessage:message]
|
|
dateCreated:[NSDate date]
|
|
callRecordType:INCallRecordTypeOutgoing
|
|
callCapability:INCallCapabilityAudioCall
|
|
callDuration:@0
|
|
unseen:@YES];
|
|
INStartCallIntent* intent = [[INStartCallIntent alloc] initWithCallRecordFilter:nil
|
|
callRecordToCallBack:callRecord
|
|
audioRoute:INCallAudioRouteUnknown
|
|
destinationType:INCallDestinationTypeNormal
|
|
contacts:@[sender]
|
|
callCapability:INCallCapabilityAudioCall];
|
|
*/
|
|
}
|
|
|
|
-(INPerson*) makeINPersonForOwnAccount:(xmpp*) account
|
|
{
|
|
DDLogDebug(@"Building INPerson for self contact...");
|
|
INPersonHandle* personHandle = [[INPersonHandle alloc] initWithValue:account.connectionProperties.identity.jid type:INPersonHandleTypeUnknown label:@"Monal IM"];
|
|
NSPersonNameComponents* nameComponents = [NSPersonNameComponents new];
|
|
nameComponents.nickname = [MLContact ownDisplayNameForAccount:account];
|
|
MLContact* ownContact = [MLContact createContactFromJid:account.connectionProperties.identity.jid andAccountID:account.accountID];
|
|
INImage* contactImage = nil;
|
|
if(ownContact.avatar != nil)
|
|
{
|
|
DDLogDebug(@"Using own avatar image: %@", ownContact.avatar);
|
|
NSData* avatarData = UIImagePNGRepresentation(ownContact.avatar);
|
|
contactImage = [INImage imageWithImageData:avatarData];
|
|
}
|
|
else
|
|
DDLogDebug(@"NOT using own avatar image...");
|
|
INPerson* person = [[INPerson alloc] initWithPersonHandle:personHandle
|
|
nameComponents:nameComponents
|
|
displayName:nameComponents.nickname
|
|
image:contactImage
|
|
contactIdentifier:nil
|
|
customIdentifier:nil
|
|
isMe:YES
|
|
suggestionType:INPersonSuggestionTypeInstantMessageAddress];
|
|
return person;
|
|
}
|
|
|
|
-(INPerson*) makeINPersonWithContact:(MLContact*) contact andDisplayName:(NSString* _Nullable) displayName andAccount:(xmpp*) account
|
|
{
|
|
DDLogDebug(@"Building INPerson for contact: %@ using display name: %@", contact, displayName);
|
|
if(displayName == nil)
|
|
displayName = contact.contactDisplayName;
|
|
INPersonHandle* personHandle = [[INPersonHandle alloc] initWithValue:contact.contactJid type:INPersonHandleTypeUnknown label:@"Monal IM"];
|
|
NSPersonNameComponents* nameComponents = [NSPersonNameComponents new];
|
|
nameComponents.nickname = displayName;
|
|
INImage* contactImage = nil;
|
|
if(contact.avatar != nil)
|
|
{
|
|
DDLogDebug(@"Using avatar image: %@", contact.avatar);
|
|
NSData* avatarData = UIImagePNGRepresentation(contact.avatar);
|
|
contactImage = [INImage imageWithImageData:avatarData];
|
|
}
|
|
else
|
|
DDLogDebug(@"NOT using avatar image...");
|
|
INPerson* person = [[INPerson alloc] initWithPersonHandle:personHandle
|
|
nameComponents:nameComponents
|
|
displayName:nameComponents.nickname
|
|
image:contactImage
|
|
contactIdentifier:nil
|
|
customIdentifier:nil
|
|
isMe:account.connectionProperties.identity.jid == contact.contactJid
|
|
suggestionType:INPersonSuggestionTypeInstantMessageAddress];
|
|
/*
|
|
if(contact.isInRoster)
|
|
person.relationship = INPersonRelationshipFriend;
|
|
*/
|
|
return person;
|
|
}
|
|
|
|
-(void) showLegacyNotificationForMessage:(MLMessage*) message withSound:(BOOL) sound
|
|
{
|
|
UNMutableNotificationContent* content = [UNMutableNotificationContent new];
|
|
MLContact* contact = [MLContact createContactFromJid:message.buddyName andAccountID:message.accountID];
|
|
NSString* idval = [self identifierWithMessage:message];
|
|
|
|
//Only show contact name if allowed
|
|
if(self.notificationPrivacySetting <= NotificationPrivacySettingOptionDisplayOnlyName)
|
|
{
|
|
content.title = [contact contactDisplayName];
|
|
if(message.isMuc)
|
|
content.subtitle = [NSString stringWithFormat:NSLocalizedString(@"%@ says:", @""), message.contactDisplayName];
|
|
}
|
|
else
|
|
content.title = NSLocalizedString(@"New Message", @"");
|
|
|
|
//only show msgText if allowed
|
|
if(self.notificationPrivacySetting == NotificationPrivacySettingOptionDisplayNameAndMessage)
|
|
{
|
|
NSString* msgText = message.messageText;
|
|
|
|
//XEP-0245: The slash me Command
|
|
if([message.messageText hasPrefix:@"/me "])
|
|
msgText = [[MLXEPSlashMeHandler sharedInstance] stringSlashMeWithMessage:message];
|
|
|
|
content.body = msgText;
|
|
content.threadIdentifier = [self threadIdentifierWithMessage:message];
|
|
content.categoryIdentifier = @"message";
|
|
//don't simply use contact directly to make sure we always use a freshly created up to date contact when unpacking the userInfo dict
|
|
content.userInfo = @{
|
|
@"fromContactJid": message.buddyName,
|
|
@"fromContactAccountID": message.accountID,
|
|
@"messageId": message.messageId
|
|
};
|
|
|
|
if(sound && [[HelperTools defaultsDB] boolForKey:@"Sound"])
|
|
{
|
|
NSString* filename = [[HelperTools defaultsDB] objectForKey:@"AlertSoundFile"];
|
|
if(filename)
|
|
{
|
|
content.sound = [UNNotificationSound soundNamed:[NSString stringWithFormat:@"AlertSounds/%@.aif", filename]];
|
|
DDLogDebug(@"Using user configured alert sound: %@", content.sound);
|
|
}
|
|
else
|
|
{
|
|
content.sound = [UNNotificationSound defaultSound];
|
|
DDLogDebug(@"Using default alert sound: %@", content.sound);
|
|
}
|
|
}
|
|
else
|
|
DDLogDebug(@"Using no alert sound");
|
|
|
|
if([message.messageType isEqualToString:kMessageTypeFiletransfer])
|
|
{
|
|
NSDictionary* info = [MLFiletransfer getFileInfoForMessage:message];
|
|
if(info)
|
|
{
|
|
NSString* mimeType = info[@"mimeType"];
|
|
if([mimeType hasPrefix:@"image/"])
|
|
{
|
|
content.body = NSLocalizedString(@"Sent an Image 📷", @"");
|
|
|
|
UNNotificationAttachment* attachment;
|
|
if(![info[@"needsDownloading"] boolValue])
|
|
{
|
|
UTType* typeHint = [UTType typeWithMIMEType:mimeType];
|
|
if(typeHint == nil)
|
|
typeHint = UTTypeImage;
|
|
attachment = [self createNotificationAttachmentForFileInfo:info havingTypeHint:typeHint];
|
|
if(attachment)
|
|
{
|
|
content.attachments = @[attachment];
|
|
content.body = @"";
|
|
}
|
|
}
|
|
}
|
|
else if([mimeType hasPrefix:@"image/"])
|
|
content.body = NSLocalizedString(@"📷 An Image", @"");
|
|
else if([mimeType hasPrefix:@"audio/"])
|
|
content.body = NSLocalizedString(@"🎵 An Audiomessage", @"");
|
|
else if([mimeType hasPrefix:@"video/"])
|
|
content.body = NSLocalizedString(@"🎥 A Video", @"");
|
|
else if([mimeType isEqualToString:@"application/pdf"])
|
|
content.body = NSLocalizedString(@"📄 A Document", @"");
|
|
else
|
|
content.body = NSLocalizedString(@"Sent a File 📁", @"");
|
|
}
|
|
else
|
|
{
|
|
// empty info dict default to "Sent a file"
|
|
content.body = NSLocalizedString(@"Sent a File 📁", @"");
|
|
}
|
|
}
|
|
else if([message.messageType isEqualToString:kMessageTypeUrl] && [[HelperTools defaultsDB] boolForKey:@"ShowURLPreview"])
|
|
content.body = NSLocalizedString(@"Sent a Link 🔗", @"");
|
|
else if([message.messageType isEqualToString:kMessageTypeGeo])
|
|
content.body = NSLocalizedString(@"Sent a Location 📍", @"");
|
|
}
|
|
else
|
|
content.body = NSLocalizedString(@"Open app to see more", @"");
|
|
|
|
DDLogDebug(@"Publishing notification with id %@", idval);
|
|
[self publishNotificationContent:[self updateBadgeForContent:content] withID:idval];
|
|
}
|
|
|
|
-(UNNotificationAttachment* _Nullable) createNotificationAttachmentForFileInfo:(NSDictionary*) info havingTypeHint:(UTType*) typeHint
|
|
{
|
|
NSError* error;
|
|
NSString* attachmentDir = [[HelperTools getContainerURLForPathComponents:@[@"documentCache"]] path];
|
|
//use "tmp." prefix to make sure this file will be garbage collected should the ios notification attachment implementation leave it behind
|
|
NSString* attachmentBasename = [NSString stringWithFormat:@"tmp.%@", info[@"cacheId"]];
|
|
NSString* notificationAttachment = [attachmentDir stringByAppendingPathComponent:[attachmentBasename stringByAppendingPathExtensionForType:typeHint]];
|
|
//using stringByAppendingPathExtensionForType: does not produce playable audio notifications for audios sent by conversations,
|
|
//but seems to work for other types
|
|
//--> use info[@"fileExtension"] for audio files and stringByAppendingPathExtensionForType: for all other types
|
|
if([typeHint conformsToType:UTTypeAudio])
|
|
notificationAttachment = [notificationAttachment stringByAppendingPathComponent:[attachmentBasename stringByAppendingPathExtension:info[@"fileExtension"]]];
|
|
UIImage* image = nil;
|
|
if([info[@"mimeType"] hasPrefix:@"image/svg"])
|
|
{
|
|
NSString* pngAttachment = [attachmentDir stringByAppendingPathComponent:[attachmentBasename stringByAppendingPathExtensionForType:UTTypePNG]];
|
|
DDLogVerbose(@"Preparing for notification attachment(%@): converting downloaded file from svg at '%@' to png at '%@'...", typeHint, info[@"cacheFile"], pngAttachment);
|
|
//we want our code to run synchronously --> use PMKHang
|
|
//this code should never run in the main queue to not provoke a deadlock
|
|
if([NSThread isMainThread])
|
|
@throw [NSException exceptionWithName:@"InvalidThread" reason:@"PMKHang on renderUIImageFromSVGURL must never be called on the main thread!" userInfo:nil];
|
|
image = (UIImage*)nilExtractor(PMKHang([HelperTools renderUIImageFromSVGURL:[NSURL fileURLWithPath:info[@"cacheFile"]]]));
|
|
if(image != nil)
|
|
{
|
|
[UIImagePNGRepresentation(image) writeToFile:pngAttachment atomically:YES];
|
|
typeHint = UTTypePNG;
|
|
notificationAttachment = pngAttachment;
|
|
}
|
|
}
|
|
//fallback if svg extraction failed OR it wasn't an SVG image in the first place
|
|
if(image == nil)
|
|
{
|
|
DDLogVerbose(@"Preparing for notification attachment(%@): hardlinking downloaded file from '%@' to '%@'...", typeHint, info[@"cacheFile"], notificationAttachment);
|
|
error = [HelperTools hardLinkOrCopyFile:info[@"cacheFile"] to:notificationAttachment];
|
|
if(error)
|
|
{
|
|
DDLogError(@"Could not hardlink cache file to notification image temp file!");
|
|
return nil;
|
|
}
|
|
}
|
|
[HelperTools configureFileProtectionFor:notificationAttachment];
|
|
UNNotificationAttachment* attachment = [UNNotificationAttachment attachmentWithIdentifier:info[@"cacheId"] URL:[NSURL fileURLWithPath:notificationAttachment] options:@{UNNotificationAttachmentOptionsTypeHintKey:typeHint} error:&error];
|
|
if(error != nil)
|
|
DDLogError(@"Could not create UNNotificationAttachment: %@", error);
|
|
return attachment;
|
|
}
|
|
|
|
-(void) dealloc
|
|
{
|
|
[[NSNotificationCenter defaultCenter] removeObserver:self];
|
|
}
|
|
|
|
@end
|