// // Copyright (c) 2017 Open Whisper Systems. All rights reserved. // #import "SharingThreadPickerViewController.h" #import "Environment.h" #import "NSString+OWS.h" #import "SignalApp.h" #import "ThreadUtil.h" #import "UIColor+OWS.h" #import "UIFont+OWS.h" #import "UIView+OWS.h" #import #import #import NS_ASSUME_NONNULL_BEGIN @interface SharingThreadPickerViewController () @property (nonatomic, readonly) OWSContactsManager *contactsManager; @property (nonatomic, readonly) OWSMessageSender *messageSender; @property (nonatomic) TSThread *thread; @property (nonatomic, readonly, weak) id shareViewDelegate; @property (nonatomic, readonly) UIProgressView *progressView; @property (nullable, atomic) 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; } - (void)loadView { [super loadView]; _contactsManager = [Environment current].contactsManager; _messageSender = [Environment current].messageSender; _progressView = [[UIProgressView alloc] initWithProgressViewStyle:UIProgressViewStyleDefault]; self.title = NSLocalizedString(@"SEND_EXTERNAL_FILE_VIEW_TITLE", @"Title for the 'send external file' view."); } - (void)viewDidLoad { [super viewDidLoad]; [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(attachmentUploadProgress:) name:kAttachmentUploadProgressNotification object:nil]; } - (BOOL)canSelectBlockedContact { return NO; } - (nullable UIView *)createHeaderWithSearchBar:(UISearchBar *)searchBar { OWSAssert(searchBar) const CGFloat contentVMargin = 0; UIView *header = [UIView new]; header.backgroundColor = [UIColor whiteColor]; 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 - (void)threadWasSelected:(TSThread *)thread { OWSAssert(self.attachment); OWSAssert(thread); self.thread = thread; __weak typeof(self) weakSelf = self; // FIXME SHARINGEXTENSION // Handling safety number changes brings in a lot of machinery. // How do we want to handle this? // e.g. fingerprint scanning, etc. in the SAE or just redirect the user to the main app? // BOOL didShowSNAlert = // [SafetyNumberConfirmationAlert presentAlertIfNecessaryWithRecipientIds:thread.recipientIdentifiers // confirmationText:[SafetyNumberStrings // confirmSendButton] // contactsManager:self.contactsManager // completion:^(BOOL didConfirm) { // if (didConfirm) { // [weakSelf threadWasSelected:thread]; // } // }]; // if (didShowSNAlert) { // return; // } AttachmentApprovalViewController *approvalVC = [[AttachmentApprovalViewController alloc] initWithAttachment:self.attachment delegate:self]; [self.navigationController pushViewController:approvalVC animated:YES]; } - (void)didTapCancelShareButton { DDLogDebug(@"%@ tapped cancel share button", self.logTag); [self cancelShareExperience]; } - (void)cancelShareExperience { [self.shareViewDelegate shareViewWasCancelled]; } #pragma mark - AttachmentApprovalViewControllerDelegate - (void)attachmentApproval:(AttachmentApprovalViewController *)attachmentApproval didApproveAttachment:(SignalAttachment *)attachment { [ThreadUtil addThreadToProfileWhitelistIfEmptyContactThread:self.thread]; [self tryToSendAttachment:attachment fromViewController:attachmentApproval]; } - (void)attachmentApproval:(AttachmentApprovalViewController *)attachmentApproval didCancelAttachment:(SignalAttachment *)attachment { [self cancelShareExperience]; } #pragma mark - Helpers - (void)tryToSendAttachment:(SignalAttachment *)attachment fromViewController:(UIViewController *)fromViewController { // Reset progress in case we're retrying self.progressView.progress = 0; self.attachment = attachment; 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]; // We add a progress subview to an AlertController, which is a total hack. // ...but it looks good, and given how short a progress view is and how // little the alert controller changes, I'm not super worried about it. [progressAlert.view addSubview:self.progressView]; [self.progressView autoPinWidthToSuperviewWithMargin:24]; [self.progressView autoAlignAxis:ALAxisHorizontal toSameAxisOfView:progressAlert.view withOffset:4]; #ifdef DEBUG if (@available(iOS 12, *)) { // TODO: Congratulations! You survived to see another iOS release. OWSFail(@"Make sure the progress view still looks good, and increment the version canary."); } #endif void (^sendCompletion)(NSError *_Nullable) = ^(NSError *_Nullable error) { AssertIsOnMainThread(); if (error) { [fromViewController dismissViewControllerAnimated:YES completion:^(void) { DDLogInfo(@"%@ Sending attachment failed with error: %@", self.logTag, error); [self showSendFailureAlertWithError:error attachment:attachment fromViewController:fromViewController]; }]; return; } DDLogInfo(@"%@ Sending attachment succeeded.", self.logTag); [self.shareViewDelegate shareViewWasCompleted]; }; [fromViewController presentViewController:progressAlert animated:YES completion:^(void) { TSOutgoingMessage *outgoingMessage = [ThreadUtil sendMessageWithAttachment:self.attachment inThread:self.thread messageSender:self.messageSender completion:sendCompletion]; self.outgoingMessage = outgoingMessage; }]; } - (void)showSendFailureAlertWithError:(NSError *)error attachment:(SignalAttachment *)attachment fromViewController:(UIViewController *)fromViewController { AssertIsOnMainThread(); NSString *failureTitle = NSLocalizedString(@"SHARE_EXTENSION_SENDING_FAILURE_TITLE", @"Alert title"); 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 tryToSendAttachment:attachment fromViewController:fromViewController]; }]; [failureAlert addAction:retryAction]; [fromViewController presentViewController:failureAlert animated:YES completion:nil]; } - (void)attachmentUploadProgress:(NSNotification *)notification { DDLogDebug(@"%@ upload progress.", self.logTag); AssertIsOnMainThread(); OWSAssert(self.progressView); if (!self.outgoingMessage) { DDLogDebug(@"%@ Ignoring upload progress until there is an outgoing message.", self.logTag); return; } NSString *_Nullable attachmentRecordId = self.outgoingMessage.attachmentIds.firstObject; if (!attachmentRecordId) { DDLogDebug(@"%@ Ignoring upload progress until outgoing message has an attachment record id", self.logTag); return; } NSDictionary *userinfo = [notification userInfo]; float progress = [[userinfo objectForKey:kAttachmentUploadProgressKey] floatValue]; NSString *attachmentID = [userinfo objectForKey:kAttachmentUploadAttachmentIDKey]; if ([attachmentRecordId isEqual:attachmentID]) { if (!isnan(progress)) { [self.progressView setProgress:progress animated:YES]; } else { OWSFail(@"%@ Invalid attachment progress.", self.logTag); } } } @end NS_ASSUME_NONNULL_END