// // ShareViewController.m // shareSheet // // Created by Anurodh Pokharel on 9/10/18. // Copyright © 2018 Monal.im. All rights reserved. // #import "ShareViewController.h" #import "MLSelectionController.h" #import "MLContact.h" #import "MLConstants.h" #import "HelperTools.h" #import "DataLayer.h" #import "MLFiletransfer.h" #import "IPC.h" #import #import @import Intents; @import UniformTypeIdentifiers; @interface ShareViewController () @property (nonatomic, strong) NSArray* accounts; @property (nonatomic, strong) NSArray* recipients; @property (nonatomic, strong) MLContact* recipient; @property (nonatomic, strong) NSDictionary* account; @property (nonatomic, strong) MLContact* intentContact; @end //TODO: use this approach, but with swiftui: https://diamantidis.github.io/2020/01/11/share-extension-custom-ui @implementation ShareViewController +(void) initialize { [HelperTools initSystem]; //init IPC [IPC initializeForProcess:@"ShareSheetExtension"]; //log startup DDLogInfo(@"Share Sheet Extension started: %@", [HelperTools appBuildVersionInfoFor:MLVersionTypeLog]); [DDLog flushLog]; } -(void) viewDidLoad { [super viewDidLoad]; [self.navigationController.navigationBar setTintColor:UIColor.systemBackgroundColor]; [self.navigationController.navigationBar setBackgroundColor:[UIColor colorNamed:@"monalGreen"]]; self.navigationController.navigationItem.title = NSLocalizedString(@"Monal", @""); DDLogInfo(@"Extension context: %@", self.extensionContext); DDLogDebug(@"Raw extension context intent: %@", self.extensionContext.intent); if(self.extensionContext.intent != nil && [self.extensionContext.intent isKindOfClass:[INSendMessageIntent class]]) { INSendMessageIntent* intent = (INSendMessageIntent*)self.extensionContext.intent; DDLogDebug(@"Got usable intent: %@", intent); self.intentContact = [HelperTools unserializeData:[intent.conversationIdentifier dataUsingEncoding:NSISOLatin1StringEncoding]]; DDLogInfo(@"Extracted intent contact: %@", self.intentContact); [self.intentContact refresh]; //make sure we are up to date } } - (void) presentationAnimationDidFinish { // list all contacts, not only active chats // that will clutter the list of selectable contacts, but you can always use sirikit interactions // to get the recently used contacts listed self.recipients = [[DataLayer sharedInstance] contactList]; self.accounts = [[DataLayer sharedInstance] enabledAccountList]; if(self.intentContact != nil) { DDLogInfo(@"Intent contact given: %@", self.intentContact); //check if intentContact is in enabled account list for(NSDictionary* accountToCheck in self.accounts) { NSNumber* accountID = [accountToCheck objectForKey:@"account_id"]; if(accountID.intValue == self.intentContact.accountID.intValue) { self.recipient = self.intentContact; self.account = accountToCheck; break; } } } //no intent given or intent contact not found --> select initial recipient (contact with most recent interaction) if(!self.account || !self.recipient) { DDLogInfo(@"No recipient given, selecting the one with the most recent interaction..."); BOOL recipientFound = NO; for(MLContact* recipient in self.recipients) { for(NSDictionary* accountToCheck in self.accounts) { NSNumber* accountID = [accountToCheck objectForKey:@"account_id"]; if(accountID.intValue == recipient.accountID.intValue) { self.recipient = recipient; self.account = accountToCheck; recipientFound = YES; break; } if(recipientFound == YES) break; } } } [self reloadConfigurationItems]; } -(MLContact* _Nullable) getLastContactForAccount:(NSNumber*) accountID { for(MLContact* recipient in self.recipients) { if(recipient.accountID.intValue == accountID.intValue) { return recipient; } } return nil; } -(BOOL) isContentValid { if(self.recipient != nil && self.account != nil) return YES; return NO; } -(void) didSelectPost { DDLogVerbose(@"input items: %@", self.extensionContext.inputItems); NSExtensionItem* item = self.extensionContext.inputItems.firstObject; DDLogVerbose(@"Attachments = %@", item.attachments); __block uint32_t loading = 0; //no need for @synchronized etc., because we access this var exclusively from the main thread __block uint32_t saved = 0; //no need for @synchronized etc., because we access this var exclusively from the main thread monal_void_block_t checkIfDone = ^{ if(loading == 0) { if(self.contentText && [self.contentText length] > 0) { NSMutableDictionary* payload = [NSMutableDictionary new]; payload[@"account_id"] = self.recipient.accountID; payload[@"recipient"] = self.recipient.contactJid; payload[@"type"] = @"text"; payload[@"data"] = self.contentText; DDLogDebug(@"Adding shareSheet comment payload: %@", payload); [[DataLayer sharedInstance] addShareSheetPayload:payload]; saved++; } [self.extensionContext completeRequestReturningItems:@[] completionHandler:^(BOOL expired __unused) { if(saved > 0) [self openMainApp]; }]; } }; for(NSItemProvider* provider in item.attachments) { // //text shares are also shared via comment field, so ignore them // if([provider hasItemConformingToTypeIdentifier:UTTypePlainText.identifier]) // continue; DDLogVerbose(@"handling(%u) %@", loading, provider); loading++; [HelperTools handleUploadItemProvider:provider withCompletionHandler:^(NSMutableDictionary* payload) { DDLogVerbose(@"Got handleUploadItemProvider callback with payload: %@", payload); dispatch_async(dispatch_get_main_queue(), ^{ if(payload == nil || payload[@"error"] != nil) { DDLogError(@"Could not save payload for sending: %@", payload[@"error"]); NSString* message = NSLocalizedString(@"Monal was not able to send your attachment!", @""); if(payload[@"error"] != nil) message = [NSString stringWithFormat:NSLocalizedString(@"Monal was not able to send your attachment: %@", @""), [payload[@"error"] localizedDescription]]; UIAlertController* unknownItemWarning = [UIAlertController alertControllerWithTitle:NSLocalizedString(@"Could not send", @"") message:message preferredStyle:UIAlertControllerStyleAlert]; [unknownItemWarning addAction:[UIAlertAction actionWithTitle:NSLocalizedString(@"Abort", @"") style:UIAlertActionStyleCancel handler:^(UIAlertAction * _Nonnull action) { [unknownItemWarning dismissViewControllerAnimated:YES completion:nil]; [self.extensionContext completeRequestReturningItems:@[] completionHandler:nil]; loading--; checkIfDone(); }]]; [self presentViewController:unknownItemWarning animated:YES completion:nil]; return; } //text shares are also shared via comment field, so ignore them, if they contain the same contents if([provider hasItemConformingToTypeIdentifier:UTTypePlainText.identifier] && self.contentText && [self.contentText length] > 0 && [payload[@"data"] isKindOfClass:[NSString class]] && [self.contentText isEqualToString:payload[@"data"]]) { DDLogWarn(@"Ignoring text payload because already sent via comment field"); loading--; checkIfDone(); return; } payload[@"account_id"] = self.recipient.accountID; payload[@"recipient"] = self.recipient.contactJid; DDLogDebug(@"Adding shareSheet payload(%u): %@", loading, payload); [[DataLayer sharedInstance] addShareSheetPayload:payload]; saved++; loading--; checkIfDone(); }); }]; } checkIfDone(); } -(NSArray*) configurationItems { NSMutableArray* toreturn = [NSMutableArray new]; if(self.accounts.count > 1) { SLComposeSheetConfigurationItem* accountSelector = [SLComposeSheetConfigurationItem new]; accountSelector.title = NSLocalizedString(@"Account", @"ShareViewController: Account"); accountSelector.value = [NSString stringWithFormat:@"%@@%@", [self.account objectForKey:@"username"], [self.account objectForKey:@"domain"]]; accountSelector.tapHandler = ^{ UIStoryboard* iosShareStoryboard = [UIStoryboard storyboardWithName:@"iosShare" bundle:nil]; MLSelectionController* controller = (MLSelectionController*)[iosShareStoryboard instantiateViewControllerWithIdentifier:@"accounts"]; controller.options = self.accounts; controller.completion = ^(NSDictionary* selectedAccount) { if(selectedAccount != nil) { self.account = selectedAccount; } else { self.account = self.accounts[0]; // at least one account is present (count > 0) } self.recipient = [self getLastContactForAccount:[self.account objectForKey:@"account_id"]]; [self reloadConfigurationItems]; }; [self pushConfigurationViewController:controller]; }; [toreturn addObject:accountSelector]; } if(!self.account && self.accounts.count > 0) self.account = [self.accounts objectAtIndex:0]; SLComposeSheetConfigurationItem* recipient = [SLComposeSheetConfigurationItem new]; recipient.title = NSLocalizedString(@"Recipient", @"shareViewController: recipient"); recipient.value = [NSString stringWithFormat:@"%@ (%@)", self.recipient.contactDisplayName, self.recipient.contactJid]; recipient.tapHandler = ^{ UIStoryboard* iosShareStoryboard = [UIStoryboard storyboardWithName:@"iosShare" bundle:nil]; MLSelectionController* controller = (MLSelectionController *)[iosShareStoryboard instantiateViewControllerWithIdentifier:@"contacts"]; // Create list of recipients for the selected account NSMutableArray* recipientsToShow = [NSMutableArray new]; for (MLContact* contact in self.recipients) { // only show contacts from the selected account NSNumber* accountID = [self.account objectForKey:@"account_id"]; if(contact.accountID.intValue == accountID.intValue) [recipientsToShow addObject:@{@"contact": contact}]; } controller.options = recipientsToShow; controller.completion = ^(NSDictionary* selectedRecipient) { MLContact* contact = [selectedRecipient objectForKey:@"contact"]; if(contact) self.recipient = contact; else self.recipient = nil; [self reloadConfigurationItems]; }; [self pushConfigurationViewController:controller]; }; [toreturn addObject:recipient]; [self validateContent]; return toreturn; } -(void) openURL:(NSURL*) url { UInt16 iterations = 0; SEL openURLSelector = NSSelectorFromString(@"openURL:"); UIResponder* responder = self; while((responder = [responder nextResponder]) != nil && iterations++ < 16) if([responder respondsToSelector:openURLSelector] == YES) { UIApplication* app = (UIApplication*)responder; if(app != nil) { [app performSelector:@selector(openURL:) withObject:url]; break; } } } -(void) openMainApp { DDLogInfo(@"Now opening mainapp via %@...", kMonalOpenURL); NSURL* mainAppUrl = kMonalOpenURL; [self openURL:mainAppUrl]; } @end