diff --git a/Signal.xcodeproj/project.pbxproj b/Signal.xcodeproj/project.pbxproj index fe83aa68a..36c2470a4 100644 --- a/Signal.xcodeproj/project.pbxproj +++ b/Signal.xcodeproj/project.pbxproj @@ -90,6 +90,7 @@ 34D8C0281ED3673300188D7C /* DebugUITableViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 34D8C0261ED3673300188D7C /* DebugUITableViewController.m */; }; 34D8C02B1ED3685800188D7C /* DebugUIContacts.m in Sources */ = {isa = PBXBuildFile; fileRef = 34D8C02A1ED3685800188D7C /* DebugUIContacts.m */; }; 34D9134B1F62D4A500722898 /* SignalAttachment.swift in Sources */ = {isa = PBXBuildFile; fileRef = 34D913491F62D4A500722898 /* SignalAttachment.swift */; }; + 34D9134D1F66DB7C00722898 /* ModalActivityIndicatorViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 34D9134C1F66DB7C00722898 /* ModalActivityIndicatorViewController.swift */; }; 34D99C8C1F27B13B00D284D6 /* OWSViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 34D99C8B1F27B13B00D284D6 /* OWSViewController.m */; }; 34D99C931F2937CC00D284D6 /* OWSAnalytics.swift in Sources */ = {isa = PBXBuildFile; fileRef = 34D99C911F2937CC00D284D6 /* OWSAnalytics.swift */; }; 34D99C941F2937CC00D284D6 /* OWSSwiftUtils.swift in Sources */ = {isa = PBXBuildFile; fileRef = 34D99C921F2937CC00D284D6 /* OWSSwiftUtils.swift */; }; @@ -551,6 +552,7 @@ 34D8C0291ED3685800188D7C /* DebugUIContacts.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = DebugUIContacts.h; sourceTree = ""; }; 34D8C02A1ED3685800188D7C /* DebugUIContacts.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = DebugUIContacts.m; sourceTree = ""; }; 34D913491F62D4A500722898 /* SignalAttachment.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SignalAttachment.swift; sourceTree = ""; }; + 34D9134C1F66DB7C00722898 /* ModalActivityIndicatorViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ModalActivityIndicatorViewController.swift; sourceTree = ""; }; 34D99C8A1F27B13B00D284D6 /* OWSViewController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = OWSViewController.h; sourceTree = ""; }; 34D99C8B1F27B13B00D284D6 /* OWSViewController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = OWSViewController.m; sourceTree = ""; }; 34D99C911F2937CC00D284D6 /* OWSAnalytics.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OWSAnalytics.swift; sourceTree = ""; }; @@ -1029,6 +1031,7 @@ 34B3F84C1E8DF1700035BE1A /* InviteFlow.swift */, 34B3F84D1E8DF1700035BE1A /* LockInteractionController.h */, 34B3F84E1E8DF1700035BE1A /* LockInteractionController.m */, + 34D9134C1F66DB7C00722898 /* ModalActivityIndicatorViewController.swift */, 34B3F84F1E8DF1700035BE1A /* NewContactThreadViewController.h */, 34B3F8501E8DF1700035BE1A /* NewContactThreadViewController.m */, 34B3F8541E8DF1700035BE1A /* NewGroupViewController.h */, @@ -2263,6 +2266,7 @@ 451686AB1F520CDA00AC3D4B /* MultiDeviceProfileKeyUpdateJob.swift in Sources */, 76EB058A18170B33006006FC /* Release.m in Sources */, 45D231771DC7E8F10034FA89 /* SessionResetJob.swift in Sources */, + 34D9134D1F66DB7C00722898 /* ModalActivityIndicatorViewController.swift in Sources */, 4563ADF11F22BD7100DEB8C7 /* OWS106EnsureProfileComplete.swift in Sources */, 450873C71D9D867B006B54F2 /* OWSIncomingMessageCollectionViewCell.m in Sources */, 76EB057A18170B33006006FC /* OWSContactsManager.m in Sources */, diff --git a/Signal/src/AppDelegate.m b/Signal/src/AppDelegate.m index 93b835a62..e4f1ed3d3 100644 --- a/Signal/src/AppDelegate.m +++ b/Signal/src/AppDelegate.m @@ -538,7 +538,7 @@ static NSString *const kURLHostVerifyPrefix = @"verify"; performActionForShortcutItem:(UIApplicationShortcutItem *)shortcutItem completionHandler:(void (^)(BOOL succeeded))completionHandler { if ([TSAccountManager isRegistered]) { - [[Environment getCurrent].signalsViewController composeNew]; + [[Environment getCurrent].signalsViewController showNewConversationView]; completionHandler(YES); } else { UIAlertController *controller = diff --git a/Signal/src/ViewControllers/AppSettingsViewController.m b/Signal/src/ViewControllers/AppSettingsViewController.m index eb320837c..0f6262e8a 100644 --- a/Signal/src/ViewControllers/AppSettingsViewController.m +++ b/Signal/src/ViewControllers/AppSettingsViewController.m @@ -374,12 +374,24 @@ - (void)proceedToUnregistration { - [TSAccountManager unregisterTextSecureWithSuccess:^{ - [Environment resetAppData]; - } - failure:^(NSError *error) { - [OWSAlerts showAlertWithTitle:NSLocalizedString(@"UNREGISTER_SIGNAL_FAIL", @"")]; - }]; + [ModalActivityIndicatorViewController + presentFromViewController:self + canCancel:NO + presentCompletion:^(ModalActivityIndicatorViewController *modalActivityIndicator) { + dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ + [TSAccountManager unregisterTextSecureWithSuccess:^{ + [Environment resetAppData]; + } + failure:^(NSError *error) { + dispatch_async(dispatch_get_main_queue(), ^{ + [modalActivityIndicator dismissWithCompletion:^{ + [OWSAlerts + showAlertWithTitle:NSLocalizedString(@"UNREGISTER_SIGNAL_FAIL", @"")]; + }]; + }); + }]; + }); + }]; } #pragma mark - Socket Status Notifications diff --git a/Signal/src/ViewControllers/ConversationView/ConversationViewController.m b/Signal/src/ViewControllers/ConversationView/ConversationViewController.m index d53bdcf85..680abbaef 100644 --- a/Signal/src/ViewControllers/ConversationView/ConversationViewController.m +++ b/Signal/src/ViewControllers/ConversationView/ConversationViewController.m @@ -3445,31 +3445,51 @@ typedef NS_ENUM(NSInteger, MessagesRangeSizeMode) { { OWSAssert([NSThread isMainThread]); - AVAsset *video = [AVAsset assetWithURL:movieURL]; - AVAssetExportSession *exportSession = - [AVAssetExportSession exportSessionWithAsset:video presetName:AVAssetExportPresetMediumQuality]; - exportSession.shouldOptimizeForNetworkUse = YES; - exportSession.outputFileType = AVFileTypeMPEG4; - NSURL *compressedVideoUrl = [[self videoTempFolder] - URLByAppendingPathComponent:[[[NSUUID UUID] UUIDString] stringByAppendingPathExtension:@"mp4"]]; - exportSession.outputURL = compressedVideoUrl; - [exportSession exportAsynchronouslyWithCompletionHandler:^{ - dispatch_async(dispatch_get_main_queue(), ^{ - id _Nullable dataSource = [DataSourcePath dataSourceWithURL:compressedVideoUrl]; - [dataSource setSourceFilename:filename]; - SignalAttachment *attachment = - [SignalAttachment attachmentWithDataSource:dataSource dataUTI:(NSString *)kUTTypeMPEG4]; - if (!attachment || [attachment hasError]) { - DDLogError(@"%@ %s Invalid attachment: %@.", - self.tag, - __PRETTY_FUNCTION__, - attachment ? [attachment errorName] : @"Missing data"); - [self showErrorAlertForAttachment:attachment]; - } else { - [self tryToSendAttachmentIfApproved:attachment skipApprovalDialog:skipApprovalDialog]; - } - }); - }]; + [ModalActivityIndicatorViewController + presentFromViewController:self + canCancel:YES + presentCompletion:^(ModalActivityIndicatorViewController *modalActivityIndicator) { + dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ + AVAsset *video = [AVAsset assetWithURL:movieURL]; + AVAssetExportSession *exportSession = + [AVAssetExportSession exportSessionWithAsset:video + presetName:AVAssetExportPresetMediumQuality]; + exportSession.shouldOptimizeForNetworkUse = YES; + exportSession.outputFileType = AVFileTypeMPEG4; + NSURL *compressedVideoUrl = [[self videoTempFolder] + URLByAppendingPathComponent:[[[NSUUID UUID] UUIDString] + stringByAppendingPathExtension:@"mp4"]]; + exportSession.outputURL = compressedVideoUrl; + [exportSession exportAsynchronouslyWithCompletionHandler:^{ + dispatch_async(dispatch_get_main_queue(), ^{ + OWSAssert([NSThread isMainThread]); + + if (modalActivityIndicator.wasCancelled) { + return; + } + + [modalActivityIndicator dismissWithCompletion:^{ + id _Nullable dataSource = + [DataSourcePath dataSourceWithURL:compressedVideoUrl]; + [dataSource setSourceFilename:filename]; + SignalAttachment *attachment = + [SignalAttachment attachmentWithDataSource:dataSource + dataUTI:(NSString *)kUTTypeMPEG4]; + if (!attachment || [attachment hasError]) { + DDLogError(@"%@ %s Invalid attachment: %@.", + self.tag, + __PRETTY_FUNCTION__, + attachment ? [attachment errorName] : @"Missing data"); + [self showErrorAlertForAttachment:attachment]; + } else { + [self tryToSendAttachmentIfApproved:attachment + skipApprovalDialog:skipApprovalDialog]; + } + }]; + }); + }]; + }); + }]; } diff --git a/Signal/src/ViewControllers/HomeViewController.h b/Signal/src/ViewControllers/HomeViewController.h index 78e2b8fa4..223006547 100644 --- a/Signal/src/ViewControllers/HomeViewController.h +++ b/Signal/src/ViewControllers/HomeViewController.h @@ -18,7 +18,7 @@ - (void)updateInboxCountLabel; -- (void)composeNew; +- (void)showNewConversationView; - (void)presentTopLevelModalViewController:(UIViewController *)viewController animateDismissal:(BOOL)animateDismissal diff --git a/Signal/src/ViewControllers/HomeViewController.m b/Signal/src/ViewControllers/HomeViewController.m index 65e580697..6b244e04e 100644 --- a/Signal/src/ViewControllers/HomeViewController.m +++ b/Signal/src/ViewControllers/HomeViewController.m @@ -175,7 +175,7 @@ typedef NS_ENUM(NSInteger, CellState) { kArchiveState, kInboxState }; self.navigationItem.rightBarButtonItem = [[UIBarButtonItem alloc] initWithBarButtonSystemItem:UIBarButtonSystemItemCompose target:self - action:@selector(composeNew)]; + action:@selector(showNewConversationView)]; ReminderView *archiveReminderView = [ReminderView new]; archiveReminderView.text = NSLocalizedString( @@ -346,7 +346,7 @@ typedef NS_ENUM(NSInteger, CellState) { kArchiveState, kInboxState }; [self.navigationController pushViewController:vc animated:NO]; } -- (void)composeNew +- (void)showNewConversationView { NewContactThreadViewController *viewController = [NewContactThreadViewController new]; diff --git a/Signal/src/ViewControllers/ModalActivityIndicatorViewController.swift b/Signal/src/ViewControllers/ModalActivityIndicatorViewController.swift new file mode 100644 index 000000000..7a7c5375b --- /dev/null +++ b/Signal/src/ViewControllers/ModalActivityIndicatorViewController.swift @@ -0,0 +1,152 @@ +// +// Copyright (c) 2017 Open Whisper Systems. All rights reserved. +// + +import Foundation +import MediaPlayer + +// A modal view that be used during blocking interactions (e.g. waiting on response from +// service or on the completion of a long-running local operation). +class ModalActivityIndicatorViewController: OWSViewController { + + let TAG = "[ModalActivityIndicatorViewController]" + + let canCancel: Bool + + public var wasCancelled: Bool = false + + var activityIndicator: UIActivityIndicatorView? + + var presentTimer: Timer? + + var wasDimissed: Bool = false + + // MARK: Initializers + + @available(*, unavailable, message:"use canCancel:completion: constructor instead.") + required init?(coder aDecoder: NSCoder) { + self.canCancel = false + super.init(coder: aDecoder) + owsFail("\(self.TAG) invalid constructor") + } + + required init(canCancel: Bool) { + self.canCancel = canCancel + super.init(nibName: nil, bundle: nil) + } + + public class func present(fromViewController: UIViewController, + canCancel: Bool, presentCompletion : @escaping (ModalActivityIndicatorViewController) -> Void) { + AssertIsOnMainThread() + + let view = ModalActivityIndicatorViewController(canCancel:canCancel) + // Present this modal _over_ the current view contents. + view.modalPresentationStyle = .overFullScreen + fromViewController.present(view, + animated: false) { + presentCompletion(view) + } + } + + public func dismiss(completion : @escaping () -> Void) { + AssertIsOnMainThread() + + if !wasDimissed { + // Only dismiss once. + self.dismiss(animated:false, completion: completion) + wasDimissed = true + } else { + // If already dismissed, wait a beat then call completion. + DispatchQueue.main.async { + completion() + } + } + } + + override func loadView() { + super.loadView() + + self.view.backgroundColor = UIColor(colorLiteralRed: 0, green: 0, blue: 0, alpha: 0.25) + self.view.isOpaque = false + + let activityIndicator = UIActivityIndicatorView(activityIndicatorStyle:.whiteLarge) + self.activityIndicator = activityIndicator + self.view.addSubview(activityIndicator) + activityIndicator.autoCenterInSuperview() + + if canCancel { + let cancelButton = UIButton(type:.custom) + cancelButton.setTitle(CommonStrings.cancelButton, for: .normal) + cancelButton.setTitleColor(UIColor.white, for: .normal) + cancelButton.backgroundColor = UIColor.ows_darkGray() + cancelButton.titleLabel?.font = UIFont.ows_mediumFont(withSize:ScaleFromIPhone5To7Plus(18, 22)) + cancelButton.layer.cornerRadius = ScaleFromIPhone5To7Plus(4, 5) + cancelButton.clipsToBounds = true + cancelButton.addTarget(self, action:#selector(cancelPressed), for:.touchUpInside) + let buttonWidth = ScaleFromIPhone5To7Plus(140, 160) + let buttonHeight = ScaleFromIPhone5To7Plus(40, 50) + self.view.addSubview(cancelButton) + cancelButton.autoHCenterInSuperview() + cancelButton.autoPinEdge(toSuperviewEdge: .bottom, withInset:50) + cancelButton.autoSetDimension(.width, toSize:buttonWidth) + cancelButton.autoSetDimension(.height, toSize:buttonHeight) + } + + // Hide the modal until the presentation animation completes. + self.view.layer.opacity = 0.0 + } + + override func viewWillAppear(_ animated: Bool) { + super.viewWillAppear(animated) + + self.activityIndicator?.startAnimating() + + // Hide the the modal and wait for a second before revealing it, + // to avoid "blipping" in the modal during short blocking operations. + // + // NOTE: It will still intercept user interactions while hidden, as it + // should. + let kPresentationDelaySeconds = TimeInterval(1) + self.presentTimer?.invalidate() + self.presentTimer = Timer.weakScheduledTimer(withTimeInterval: kPresentationDelaySeconds, target: self, selector: #selector(presentTimerFired), userInfo: nil, repeats: false) + } + + override func viewWillDisappear(_ animated: Bool) { + super.viewWillDisappear(animated) + + clearTimer() + } + + override func viewDidDisappear(_ animated: Bool) { + super.viewDidDisappear(animated) + + self.activityIndicator?.stopAnimating() + + clearTimer() + } + + private func clearTimer() { + self.presentTimer?.invalidate() + self.presentTimer = nil + } + + func presentTimerFired() { + AssertIsOnMainThread() + + clearTimer() + + // Fade in the modal. + UIView.animate(withDuration: 0.35) { + self.view.layer.opacity = 1.0 + } + } + + func cancelPressed() { + AssertIsOnMainThread() + + wasCancelled = true + + dismiss { + } + } +} diff --git a/Signal/src/ViewControllers/NewGroupViewController.m b/Signal/src/ViewControllers/NewGroupViewController.m index f1f101f75..0ad0ee595 100644 --- a/Signal/src/ViewControllers/NewGroupViewController.m +++ b/Signal/src/ViewControllers/NewGroupViewController.m @@ -483,36 +483,32 @@ const NSUInteger kNewGroupViewControllerAvatarWidth = 68; }); }; - UIAlertController *alertController = - [UIAlertController alertControllerWithTitle:NSLocalizedString(@"GROUP_CREATING", nil) - message:nil - preferredStyle:UIAlertControllerStyleAlert]; + [ModalActivityIndicatorViewController + presentFromViewController:self + canCancel:NO + presentCompletion:^(ModalActivityIndicatorViewController *modalActivityIndicator) { + TSOutgoingMessage *message = + [[TSOutgoingMessage alloc] initWithTimestamp:[NSDate ows_millisecondTimeStamp] + inThread:thread + groupMetaMessage:TSGroupMessageNew]; - [self presentViewController:alertController - animated:YES - completion:^{ - TSOutgoingMessage *message = - [[TSOutgoingMessage alloc] initWithTimestamp:[NSDate ows_millisecondTimeStamp] - inThread:thread - groupMetaMessage:TSGroupMessageNew]; + // This will save the message. + [message updateWithCustomMessage:NSLocalizedString(@"GROUP_CREATED", nil)]; - // This will save the message. - [message updateWithCustomMessage:NSLocalizedString(@"GROUP_CREATED", nil)]; - - if (model.groupImage) { - NSData *data = UIImagePNGRepresentation(model.groupImage); - id _Nullable dataSource = - [DataSourceValue dataSourceWithData:data fileExtension:@"png"]; - [self.messageSender sendAttachmentData:dataSource - contentType:OWSMimeTypeImagePng - sourceFilename:nil - inMessage:message - success:successHandler - failure:failureHandler]; - } else { - [self.messageSender sendMessage:message success:successHandler failure:failureHandler]; - } - }]; + if (model.groupImage) { + NSData *data = UIImagePNGRepresentation(model.groupImage); + id _Nullable dataSource = + [DataSourceValue dataSourceWithData:data fileExtension:@"png"]; + [self.messageSender sendAttachmentData:dataSource + contentType:OWSMimeTypeImagePng + sourceFilename:nil + inMessage:message + success:successHandler + failure:failureHandler]; + } else { + [self.messageSender sendMessage:message success:successHandler failure:failureHandler]; + } + }]; } - (TSGroupModel *)makeGroup diff --git a/Signal/src/ViewControllers/SelectRecipientViewController.m b/Signal/src/ViewControllers/SelectRecipientViewController.m index 070cbebab..379d1110a 100644 --- a/Signal/src/ViewControllers/SelectRecipientViewController.m +++ b/Signal/src/ViewControllers/SelectRecipientViewController.m @@ -310,51 +310,46 @@ NSString *const kSelectRecipientViewControllerCellIdentifier = @"kSelectRecipien if ([self.delegate shouldValidatePhoneNumbers]) { // Show an alert while validating the recipient. - __block BOOL wasCancelled = NO; - UIAlertController *activityAlert = [UIAlertController - alertControllerWithTitle:NSLocalizedString(@"ALERT_VALIDATE_RECIPIENT_TITLE", - @"A title for the alert shown while validating a signal account") - message:NSLocalizedString(@"ALERT_VALIDATE_RECIPIENT_MESSAGE", - @"A message for the alert shown while validating a signal account") - preferredStyle:UIAlertControllerStyleAlert]; - [activityAlert addAction:[UIAlertAction actionWithTitle:CommonStrings.cancelButton - style:UIAlertActionStyleCancel - handler:^(UIAlertAction *_Nonnull action) { - wasCancelled = YES; - }]]; - [[UIApplication sharedApplication].frontmostViewController presentViewController:activityAlert - animated:YES - completion:nil]; __weak SelectRecipientViewController *weakSelf = self; - [[ContactsUpdater sharedUpdater] lookupIdentifiers:possiblePhoneNumbers - success:^(NSArray *recipients) { - OWSAssert([NSThread isMainThread]); - OWSAssert(recipients.count > 0); + [ModalActivityIndicatorViewController + presentFromViewController:self + canCancel:YES + presentCompletion:^(ModalActivityIndicatorViewController *modalActivityIndicator) { + dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ + [[ContactsUpdater sharedUpdater] lookupIdentifiers:possiblePhoneNumbers + success:^(NSArray *recipients) { + OWSAssert([NSThread isMainThread]); + OWSAssert(recipients.count > 0); - if (wasCancelled) { - return; - } + if (modalActivityIndicator.wasCancelled) { + return; + } - NSString *recipientId = recipients[0].uniqueId; - [activityAlert dismissViewControllerAnimated:NO - completion:^{ - [weakSelf.delegate phoneNumberWasSelected:recipientId]; - }]; - } - failure:^(NSError *error) { - OWSAssert([NSThread isMainThread]); - if (wasCancelled) { - return; - } - [activityAlert dismissViewControllerAnimated:NO - completion:^{ - [OWSAlerts - showAlertWithTitle:NSLocalizedString(@"ALERT_ERROR_TITLE", - @"Title for a generic error alert.") - message:error.localizedDescription]; - }]; - }]; + NSString *recipientId = recipients[0].uniqueId; + [modalActivityIndicator + dismissViewControllerAnimated:NO + completion:^{ + [weakSelf.delegate phoneNumberWasSelected:recipientId]; + }]; + } + failure:^(NSError *error) { + OWSAssert([NSThread isMainThread]); + if (modalActivityIndicator.wasCancelled) { + return; + } + [modalActivityIndicator + dismissViewControllerAnimated:NO + completion:^{ + [OWSAlerts + showAlertWithTitle: + NSLocalizedString(@"ALERT_ERROR_TITLE", + @"Title for a generic error alert.") + message:error.localizedDescription]; + }]; + }]; + }); + }]; } else { NSString *recipientId = possiblePhoneNumbers[0]; [self.delegate phoneNumberWasSelected:recipientId];