// // Copyright (c) 2019 Open Whisper Systems. All rights reserved. // #import "SharingThreadPickerViewController.h" #import "SignalApp.h" #import "UIColor+OWS.h" #import "UIFont+OWS.h" #import "UIView+OWS.h" #import #import #import #import #import #import NS_ASSUME_NONNULL_BEGIN typedef void (^SendCompletionBlock)(NSError *_Nullable, TSOutgoingMessage *); typedef void (^SendMessageBlock)(SendCompletionBlock completion); @interface SharingThreadPickerViewController () @property (nonatomic, readonly) OWSMessageSender *messageSender; @property (nonatomic) TSThread *thread; @property (nonatomic, readonly, weak) id shareViewDelegate; @property (atomic, nullable) TSOutgoingMessage *outgoingMessage; @end #pragma mark - @implementation SharingThreadPickerViewController - (instancetype)initWithShareViewDelegate:(id)shareViewDelegate { self = [super init]; if (!self) { return self; } _shareViewDelegate = shareViewDelegate; self.selectThreadViewDelegate = self; return self; } #pragma mark - Dependencies - (YapDatabaseConnection *)dbReadWriteConnection { return OWSPrimaryStorage.sharedManager.dbReadWriteConnection; } - (YapDatabaseConnection *)dbReadConnection { return OWSPrimaryStorage.sharedManager.dbReadConnection; } #pragma mark - UIViewController overrides - (void)loadView { [super loadView]; self.title = NSLocalizedString(@"SHARE_EXTENSION_VIEW_TITLE", @"Title for the 'share extension' view."); } - (void)viewDidLoad { [super viewDidLoad]; [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(attachmentUploadProgress:) name:kAttachmentUploadProgressNotification object:nil]; // Loki: Customize title UILabel *titleLabel = [UILabel new]; titleLabel.text = NSLocalizedString(@"Share", @""); titleLabel.textColor = LKColors.text; titleLabel.font = [UIFont boldSystemFontOfSize:25]; self.navigationItem.titleView = titleLabel; } - (BOOL)canSelectBlockedContact { return NO; } - (nullable UIView *)createHeaderWithSearchBar:(UISearchBar *)searchBar { OWSAssertDebug(searchBar); const CGFloat contentVMargin = 0; UIView *header = [UIView new]; header.backgroundColor = LKColors.navigationBarBackground; UIButton *cancelShareButton = [UIButton buttonWithType:UIButtonTypeSystem]; [header addSubview:cancelShareButton]; [cancelShareButton setTitle:[CommonStrings cancelButton] forState:UIControlStateNormal]; cancelShareButton.userInteractionEnabled = YES; [cancelShareButton autoPinEdgeToSuperviewMargin:ALEdgeLeading]; [cancelShareButton autoPinEdgeToSuperviewMargin:ALEdgeBottom]; [cancelShareButton setCompressionResistanceHigh]; [cancelShareButton setContentHuggingHigh]; [cancelShareButton addTarget:self action:@selector(didTapCancelShareButton) forControlEvents:UIControlEventTouchUpInside]; [header addSubview:searchBar]; [searchBar autoPinEdge:ALEdgeLeading toEdge:ALEdgeTrailing ofView:cancelShareButton withOffset:6]; [searchBar autoPinEdgeToSuperviewEdge:ALEdgeTrailing]; [searchBar autoPinEdgeToSuperviewEdge:ALEdgeTop]; [searchBar autoPinEdgeToSuperviewEdge:ALEdgeBottom]; UIView *borderView = [UIView new]; [header addSubview:borderView]; borderView.backgroundColor = [UIColor colorWithRGBHex:0xbbbbbb]; [borderView autoSetDimension:ALDimensionHeight toSize:0.5]; [borderView autoPinWidthToSuperview]; [borderView autoPinEdgeToSuperviewEdge:ALEdgeBottom]; // UITableViewController.tableHeaderView must have its height set. header.frame = CGRectMake(0, 0, 0, (contentVMargin * 2 + searchBar.frame.size.height)); return header; } #pragma mark - SelectThreadViewControllerDelegate - (nullable NSString *)convertAttachmentToMessageTextIfPossible { if (self.attachments.count > 1) { return nil; } OWSAssertDebug(self.attachments.count == 1); SignalAttachment *attachment = self.attachments.firstObject; if (!attachment.isConvertibleToTextMessage) { return nil; } if (attachment.dataLength >= kOversizeTextMessageSizeThreshold) { return nil; } NSData *data = attachment.data; OWSAssertDebug(data.length < kOversizeTextMessageSizeThreshold); NSString *_Nullable messageText = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding]; OWSLogVerbose(@"messageTextForAttachment: %@", messageText); return [messageText filterStringForDisplay]; } - (void)threadWasSelected:(TSThread *)thread { OWSAssertDebug(self.attachments.count > 0); OWSAssertDebug(thread); self.thread = thread; if ([self tryToShareAsMessageText]) { return; } OWSNavigationController *approvalModal = [AttachmentApprovalViewController wrappedInNavControllerWithAttachments:self.attachments approvalDelegate:self]; [self presentViewController:approvalModal animated:YES completion:nil]; } - (BOOL)tryToShareAsMessageText { OWSAssertDebug(self.attachments.count > 0); NSString *_Nullable messageText = [self convertAttachmentToMessageTextIfPossible]; if (!messageText) { return NO; } MessageApprovalViewController *approvalVC = [[MessageApprovalViewController alloc] initWithMessageText:messageText thread:self.thread delegate:self]; [self.navigationController pushViewController:approvalVC animated:YES]; return YES; } // override - (void)dismissPressed:(id)sender { OWSLogDebug(@"tapped dismiss share button"); [self cancelShareExperience]; } - (void)didTapCancelShareButton { OWSLogDebug(@"tapped cancel share button"); [self cancelShareExperience]; } - (void)cancelShareExperience { [self.shareViewDelegate shareViewWasCancelled]; } #pragma mark - AttachmentApprovalViewControllerDelegate - (void)attachmentApproval:(AttachmentApprovalViewController *_Nonnull)attachmentApproval didApproveAttachments:(NSArray *_Nonnull)attachments messageText:(NSString *_Nullable)messageText { [self tryToSendMessageWithBlock:^(SendCompletionBlock sendCompletion) { SNVisibleMessage *message = [SNVisibleMessage new]; message.sentTimestamp = [NSDate millisecondTimestamp]; message.text = messageText; TSOutgoingMessage *tsMessage = [TSOutgoingMessage from:message associatedWith:self.thread]; [LKStorage writeWithBlock:^(YapDatabaseReadWriteTransaction *transaction) { [tsMessage saveWithTransaction:transaction]; }]; [LKStorage writeWithBlock:^(YapDatabaseReadWriteTransaction *transaction) { [SNMessageSender sendNonDurably:message withAttachments:attachments inThread:self.thread usingTransaction:transaction] .then(^(id object) { sendCompletion(nil, tsMessage); }).catch(^(NSError *error) { sendCompletion(error, tsMessage); }); }]; // This is necessary to show progress self.outgoingMessage = tsMessage; } fromViewController:attachmentApproval]; } - (void)attachmentApprovalDidCancel:(AttachmentApprovalViewController *)attachmentApproval { [self cancelShareExperience]; } - (void)attachmentApproval:(AttachmentApprovalViewController *)attachmentApproval didChangeMessageText:(nullable NSString *)newMessageText { // no-op } #pragma mark - MessageApprovalViewControllerDelegate - (void)messageApproval:(MessageApprovalViewController *)approvalViewController didApproveMessage:(NSString *)messageText { [self tryToSendMessageWithBlock:^(SendCompletionBlock sendCompletion) { SNVisibleMessage *message = [SNVisibleMessage new]; message.sentTimestamp = [NSDate millisecondTimestamp]; message.text = messageText; TSOutgoingMessage *tsMessage = [TSOutgoingMessage from:message associatedWith:self.thread]; [LKStorage writeWithBlock:^(YapDatabaseReadWriteTransaction *transaction) { [tsMessage saveWithTransaction:transaction]; }]; [LKStorage writeWithBlock:^(YapDatabaseReadWriteTransaction *transaction) { [SNMessageSender sendNonDurably:message withAttachments:@[] inThread:self.thread usingTransaction:transaction] .then(^(id object) { sendCompletion(nil, tsMessage); }).catch(^(NSError *error) { sendCompletion(error, tsMessage); }); }]; // This is necessary to show progress self.outgoingMessage = tsMessage; } fromViewController:approvalViewController]; } - (void)messageApprovalDidCancel:(MessageApprovalViewController *)approvalViewController { [self cancelShareExperience]; } #pragma mark - Helpers - (void)tryToSendMessageWithBlock:(SendMessageBlock)sendMessageBlock fromViewController:(UIViewController *)fromViewController { NSString *progressTitle = NSLocalizedString(@"SHARE_EXTENSION_SENDING_IN_PROGRESS_TITLE", @"Alert title"); UIAlertController *progressAlert = [UIAlertController alertControllerWithTitle:progressTitle message:nil preferredStyle:UIAlertControllerStyleAlert]; UIAlertAction *progressCancelAction = [UIAlertAction actionWithTitle:[CommonStrings cancelButton] style:UIAlertActionStyleCancel handler:^(UIAlertAction *_Nonnull action) { [self.shareViewDelegate shareViewWasCancelled]; }]; [progressAlert addAction:progressCancelAction]; SendCompletionBlock sendCompletion = ^(NSError *_Nullable error, TSOutgoingMessage *message) { dispatch_async(dispatch_get_main_queue(), ^{ if (error) { [fromViewController dismissViewControllerAnimated:YES completion:^{ OWSLogInfo(@"Sending message failed with error: %@", error); [self showSendFailureAlertWithError:error message:message fromViewController:fromViewController]; }]; return; } OWSLogInfo(@"Sending message succeeded."); [self.shareViewDelegate shareViewWasCompleted]; }); }; [fromViewController presentAlert:progressAlert completion:^{ sendMessageBlock(sendCompletion); }]; } - (void)showSendFailureAlertWithError:(NSError *)error message:(TSOutgoingMessage *)message fromViewController:(UIViewController *)fromViewController { OWSAssertIsOnMainThread(); OWSAssertDebug(error); OWSAssertDebug(message); OWSAssertDebug(fromViewController); NSString *failureTitle = NSLocalizedString(@"SHARE_EXTENSION_SENDING_FAILURE_TITLE", @"Alert title"); if ([error.domain isEqual:OWSSignalServiceKitErrorDomain] && error.code == OWSErrorCodeUntrustedIdentity) { NSString *_Nullable untrustedRecipientId = error.userInfo[OWSErrorRecipientIdentifierKey]; NSString *failureFormat = NSLocalizedString(@"SHARE_EXTENSION_FAILED_SENDING_BECAUSE_UNTRUSTED_IDENTITY_FORMAT", @"alert body when sharing file failed because of untrusted/changed identity keys"); NSString *displayName = [SSKEnvironment.shared.profileManager profileNameForRecipientWithID:untrustedRecipientId avoidingWriteTransaction:YES]; NSString *failureMessage = [NSString stringWithFormat:failureFormat, displayName]; UIAlertController *failureAlert = [UIAlertController alertControllerWithTitle:failureTitle message:failureMessage preferredStyle:UIAlertControllerStyleAlert]; UIAlertAction *failureCancelAction = [UIAlertAction actionWithTitle:[CommonStrings cancelButton] style:UIAlertActionStyleCancel handler:^(UIAlertAction *_Nonnull action) { [self.shareViewDelegate shareViewWasCancelled]; }]; [failureAlert addAction:failureCancelAction]; if (untrustedRecipientId.length > 0) { UIAlertAction *confirmAction = [UIAlertAction actionWithTitle:[SafetyNumberStrings confirmSendButton] style:UIAlertActionStyleDefault handler:^(UIAlertAction *action) { [self confirmIdentityAndResendMessage:message recipientId:untrustedRecipientId fromViewController:fromViewController]; }]; [failureAlert addAction:confirmAction]; } else { // This shouldn't happen, but if it does we won't offer the user the ability to confirm. // They may have to return to the main app to accept the identity change. OWSFailDebug(@"Untrusted recipient error is missing recipient id."); } [fromViewController presentAlert:failureAlert]; } else { // Non-identity failure, e.g. network offline, rate limit UIAlertController *failureAlert = [UIAlertController alertControllerWithTitle:failureTitle message:error.localizedDescription preferredStyle:UIAlertControllerStyleAlert]; UIAlertAction *failureCancelAction = [UIAlertAction actionWithTitle:[CommonStrings cancelButton] style:UIAlertActionStyleCancel handler:^(UIAlertAction *_Nonnull action) { [self.shareViewDelegate shareViewWasCancelled]; }]; [failureAlert addAction:failureCancelAction]; UIAlertAction *retryAction = [UIAlertAction actionWithTitle:[CommonStrings retryButton] style:UIAlertActionStyleDefault handler:^(UIAlertAction *action) { [self resendMessage:message fromViewController:fromViewController]; }]; [failureAlert addAction:retryAction]; [fromViewController presentAlert:failureAlert]; } } - (void)confirmIdentityAndResendMessage:(TSOutgoingMessage *)message recipientId:(NSString *)recipientId fromViewController:(UIViewController *)fromViewController { OWSAssertIsOnMainThread(); OWSAssertDebug(message); OWSAssertDebug(recipientId.length > 0); OWSAssertDebug(fromViewController); OWSLogDebug(@"Confirming identity for recipient: %@", recipientId); dispatch_async(dispatch_get_main_queue(), ^(void) { [self resendMessage:message fromViewController:fromViewController]; }); } - (void)resendMessage:(TSOutgoingMessage *)tsMessage fromViewController:(UIViewController *)fromViewController { OWSAssertIsOnMainThread(); OWSAssertDebug(tsMessage); OWSAssertDebug(fromViewController); NSString *progressTitle = NSLocalizedString(@"SHARE_EXTENSION_SENDING_IN_PROGRESS_TITLE", @"Alert title"); UIAlertController *progressAlert = [UIAlertController alertControllerWithTitle:progressTitle message:nil preferredStyle:UIAlertControllerStyleAlert]; UIAlertAction *progressCancelAction = [UIAlertAction actionWithTitle:[CommonStrings cancelButton] style:UIAlertActionStyleCancel handler:^(UIAlertAction *_Nonnull action) { [self.shareViewDelegate shareViewWasCancelled]; }]; [progressAlert addAction:progressCancelAction]; [fromViewController presentAlert:progressAlert completion:^{ SNVisibleMessage *message = [SNVisibleMessage from:tsMessage]; [LKStorage writeWithBlock:^(YapDatabaseReadWriteTransaction *transaction) { NSMutableArray *attachments = @[].mutableCopy; for (NSString *attachmentID in tsMessage.attachmentIds) { TSAttachmentStream *stream = [TSAttachmentStream fetchObjectWithUniqueID:attachmentID transaction:transaction]; if (![stream isKindOfClass:TSAttachmentStream.class]) { continue; } [attachments addObject:stream]; } [SNMessageSender prep:attachments forMessage:message usingTransaction: transaction]; [SNMessageSender sendNonDurably:message withAttachmentIDs:tsMessage.attachmentIds inThread:self.thread usingTransaction:transaction] .thenOn(dispatch_get_main_queue(), ^() { [self.shareViewDelegate shareViewWasCompleted]; }) .catchOn(dispatch_get_main_queue(), ^(NSError *error) { [fromViewController dismissViewControllerAnimated:YES completion:^{ [self showSendFailureAlertWithError:error message:tsMessage fromViewController:fromViewController]; }]; }); }]; }]; } - (void)attachmentUploadProgress:(NSNotification *)notification { OWSLogDebug(@"upload progress."); OWSAssertIsOnMainThread(); if (!self.outgoingMessage) { OWSLogDebug(@"Ignoring upload progress until there is an outgoing message."); return; } // TODO: Support multi-image messages. NSString *_Nullable attachmentRecordId = self.outgoingMessage.attachmentIds.firstObject; if (!attachmentRecordId) { OWSLogDebug(@"Ignoring upload progress until outgoing message has an attachment record id"); return; } NSDictionary *userinfo = [notification userInfo]; float progress = [[userinfo objectForKey:kAttachmentUploadProgressKey] floatValue]; NSString *attachmentID = [userinfo objectForKey:kAttachmentUploadAttachmentIDKey]; if ([attachmentRecordId isEqual:attachmentID]) { if (!isnan(progress)) { // This is where we'd set progress if we could } else { OWSFailDebug(@"Invalid attachment progress."); } } } @end NS_ASSUME_NONNULL_END