diff --git a/Signal.xcodeproj/project.pbxproj b/Signal.xcodeproj/project.pbxproj index e10cb3b117..85a903bd38 100644 --- a/Signal.xcodeproj/project.pbxproj +++ b/Signal.xcodeproj/project.pbxproj @@ -74,6 +74,7 @@ 34B3F89C1E8DF3270035BE1A /* BlockListViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 34B3F89B1E8DF3270035BE1A /* BlockListViewController.m */; }; 34B3F89F1E8DF5490035BE1A /* OWSTableViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 34B3F89E1E8DF5490035BE1A /* OWSTableViewController.m */; }; 34B3F8A21E8EA6040035BE1A /* ViewControllerUtils.m in Sources */ = {isa = PBXBuildFile; fileRef = 34B3F8A11E8EA6040035BE1A /* ViewControllerUtils.m */; }; + 34C42D5B1F45F7A80072EC04 /* OWSNavigationController.m in Sources */ = {isa = PBXBuildFile; fileRef = 34C42D5A1F45F7A80072EC04 /* OWSNavigationController.m */; }; 34CCAF381F0C0599004084F4 /* AppUpdateNag.m in Sources */ = {isa = PBXBuildFile; fileRef = 34CCAF371F0C0599004084F4 /* AppUpdateNag.m */; }; 34CCAF3B1F0C2748004084F4 /* OWSAddToContactViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 34CCAF3A1F0C2748004084F4 /* OWSAddToContactViewController.m */; }; 34CE88E71F2FB9A10098030F /* ProfileViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 34CE88E61F2FB9A10098030F /* ProfileViewController.m */; }; @@ -505,6 +506,8 @@ 34B3F89E1E8DF5490035BE1A /* OWSTableViewController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = OWSTableViewController.m; sourceTree = ""; }; 34B3F8A01E8EA6040035BE1A /* ViewControllerUtils.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ViewControllerUtils.h; sourceTree = ""; }; 34B3F8A11E8EA6040035BE1A /* ViewControllerUtils.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ViewControllerUtils.m; sourceTree = ""; }; + 34C42D591F45F7A80072EC04 /* OWSNavigationController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = OWSNavigationController.h; sourceTree = ""; }; + 34C42D5A1F45F7A80072EC04 /* OWSNavigationController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = OWSNavigationController.m; sourceTree = ""; }; 34CCAF361F0C0599004084F4 /* AppUpdateNag.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = AppUpdateNag.h; sourceTree = ""; }; 34CCAF371F0C0599004084F4 /* AppUpdateNag.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = AppUpdateNag.m; sourceTree = ""; }; 34CCAF391F0C2748004084F4 /* OWSAddToContactViewController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = OWSAddToContactViewController.h; sourceTree = ""; }; @@ -1026,6 +1029,8 @@ 34B3F85F1E8DF1700035BE1A /* OWSLinkedDevicesTableViewController.h */, 34B3F8601E8DF1700035BE1A /* OWSLinkedDevicesTableViewController.m */, 34B3F8611E8DF1700035BE1A /* OWSMessagesToolbarContentView.xib */, + 34C42D591F45F7A80072EC04 /* OWSNavigationController.h */, + 34C42D5A1F45F7A80072EC04 /* OWSNavigationController.m */, 34B3F8621E8DF1700035BE1A /* OWSQRCodeScanningViewController.h */, 34B3F8631E8DF1700035BE1A /* OWSQRCodeScanningViewController.m */, 34B3F89D1E8DF5490035BE1A /* OWSTableViewController.h */, @@ -2307,6 +2312,7 @@ 341BB7491DB727EE001E2975 /* JSQMediaItem+OWS.m in Sources */, 34B3F89C1E8DF3270035BE1A /* BlockListViewController.m in Sources */, 45F2B1941D9C9F48000D2C69 /* OWSOutgoingMessageCollectionViewCell.m in Sources */, + 34C42D5B1F45F7A80072EC04 /* OWSNavigationController.m in Sources */, BFB074C919A5611000F2947C /* ObservableValue.m in Sources */, B68EF9BA1C0B1EBD009C3DCD /* FLAnimatedImage.m in Sources */, B68112EA1A4D9EC400BA82FF /* UIImage+normalizeImage.m in Sources */, diff --git a/Signal/src/AppDelegate.m b/Signal/src/AppDelegate.m index b97adf95bd..c38553cdf4 100644 --- a/Signal/src/AppDelegate.m +++ b/Signal/src/AppDelegate.m @@ -11,6 +11,7 @@ #import "NotificationsManager.h" #import "OWSContactsManager.h" #import "OWSContactsSyncing.h" +#import "OWSNavigationController.h" #import "OWSProfileManager.h" #import "OWSStaleNotificationObserver.h" #import "Pastelog.h" @@ -802,8 +803,8 @@ static NSString *const kURLHostVerifyPrefix = @"verify"; self.window.rootViewController = navigationController; } else { RegistrationViewController *viewController = [RegistrationViewController new]; - UINavigationController *navigationController = - [[UINavigationController alloc] initWithRootViewController:viewController]; + OWSNavigationController *navigationController = + [[OWSNavigationController alloc] initWithRootViewController:viewController]; navigationController.navigationBarHidden = YES; self.window.rootViewController = navigationController; } diff --git a/Signal/src/ViewControllers/AppSettingsViewController.m b/Signal/src/ViewControllers/AppSettingsViewController.m index fd1b6366ce..9a91317fb8 100644 --- a/Signal/src/ViewControllers/AppSettingsViewController.m +++ b/Signal/src/ViewControllers/AppSettingsViewController.m @@ -316,8 +316,7 @@ - (void)showProfile { - ProfileViewController *vc = [[ProfileViewController alloc] init]; - [self.navigationController pushViewController:vc animated:YES]; + [ProfileViewController presentForAppSettings:self.navigationController]; } - (void)showAdvanced diff --git a/Signal/src/ViewControllers/CodeVerificationViewController.m b/Signal/src/ViewControllers/CodeVerificationViewController.m index 505fbd4d84..3040e52243 100644 --- a/Signal/src/ViewControllers/CodeVerificationViewController.m +++ b/Signal/src/ViewControllers/CodeVerificationViewController.m @@ -3,10 +3,8 @@ // #import "CodeVerificationViewController.h" -#import "AppDelegate.h" +#import "ProfileViewController.h" #import "Signal-Swift.h" -#import "SignalsNavigationController.h" -#import "SignalsViewController.h" #import "StringUtil.h" #import "UIViewController+OWS.h" #import @@ -269,14 +267,7 @@ NS_ASSUME_NONNULL_BEGIN DDLogInfo(@"%@ Successfully registered Signal account.", weakSelf.tag); dispatch_async(dispatch_get_main_queue(), ^{ [weakSelf stopActivityIndicator]; - - SignalsViewController *homeView = [SignalsViewController new]; - homeView.newlyRegisteredUser = YES; - SignalsNavigationController *navigationController = - [[SignalsNavigationController alloc] initWithRootViewController:homeView]; - AppDelegate *appDelegate = (AppDelegate *)[UIApplication sharedApplication].delegate; - appDelegate.window.rootViewController = navigationController; - OWSAssert([navigationController.topViewController isKindOfClass:[SignalsViewController class]]); + [weakSelf vericationWasCompleted]; }); }) .catch(^(NSError *_Nonnull error) { @@ -290,6 +281,10 @@ NS_ASSUME_NONNULL_BEGIN }); } +- (void)vericationWasCompleted +{ + [ProfileViewController presentForRegistration:self.navigationController]; +} - (void)presentAlertWithVerificationError:(NSError *)error { diff --git a/Signal/src/ViewControllers/ConversationView/MessagesViewController.m b/Signal/src/ViewControllers/ConversationView/MessagesViewController.m index 41322b8d9b..e12348046e 100644 --- a/Signal/src/ViewControllers/ConversationView/MessagesViewController.m +++ b/Signal/src/ViewControllers/ConversationView/MessagesViewController.m @@ -158,7 +158,6 @@ typedef enum : NSUInteger { OWSVoiceMemoGestureDelegate, UIDocumentMenuDelegate, UIDocumentPickerDelegate, - UIGestureRecognizerDelegate, UIImagePickerControllerDelegate, UINavigationControllerDelegate, UITextViewDelegate> @@ -549,10 +548,6 @@ typedef enum : NSUInteger { // In case we're dismissing a CNContactViewController which requires default system appearance [UIUtil applySignalAppearence]; - // Since we're using a custom back button, we have to do some extra work to manage the - // interactivePopGestureRecognizer - self.navigationController.interactivePopGestureRecognizer.delegate = self; - // We need to recheck on every appearance, since the user may have left the group in the settings VC, // or on another device. [self hideInputIfNeeded]; @@ -992,10 +987,6 @@ typedef enum : NSUInteger { self.isViewVisible = NO; - // Since we're using a custom back button, we have to do some extra work to manage the - // interactivePopGestureRecognizer - self.navigationController.interactivePopGestureRecognizer.delegate = nil; - [self.audioAttachmentPlayer stop]; self.audioAttachmentPlayer = nil; diff --git a/Signal/src/ViewControllers/NewGroupViewController.m b/Signal/src/ViewControllers/NewGroupViewController.m index 11f350826d..7af2fbaab8 100644 --- a/Signal/src/ViewControllers/NewGroupViewController.m +++ b/Signal/src/ViewControllers/NewGroupViewController.m @@ -10,6 +10,7 @@ #import "ContactsViewHelper.h" #import "Environment.h" #import "OWSContactsManager.h" +#import "OWSNavigationController.h" #import "OWSTableViewController.h" #import "Signal-Swift.h" #import "SignalKeyingStorage.h" @@ -34,7 +35,8 @@ const NSUInteger kNewGroupViewControllerAvatarWidth = 68; AvatarViewHelperDelegate, AddToGroupViewControllerDelegate, OWSTableViewControllerDelegate, - UINavigationControllerDelegate> + UINavigationControllerDelegate, + OWSNavigationView> @property (nonatomic, readonly) OWSMessageSender *messageSender; @property (nonatomic, readonly) ContactsViewHelper *contactsViewHelper; @@ -97,8 +99,6 @@ const NSUInteger kNewGroupViewControllerAvatarWidth = 68; [super loadView]; self.title = NSLocalizedString(@"NEW_GROUP_DEFAULT_TITLE", @"The navbar title for the 'new group' view."); - self.navigationItem.leftBarButtonItem = - [self createOWSBackButtonWithTarget:self selector:@selector(backButtonPressed:)]; self.navigationItem.rightBarButtonItem = [[UIBarButtonItem alloc] initWithTitle:NSLocalizedString(@"NEW_GROUP_CREATE_BUTTON", @"The title for the 'create group' button.") @@ -547,7 +547,7 @@ const NSUInteger kNewGroupViewControllerAvatarWidth = 68; #pragma mark - Event Handling -- (void)backButtonPressed:(id)sender +- (void)backButtonPressed { [self.groupNameTextField resignFirstResponder]; @@ -649,6 +649,17 @@ const NSUInteger kNewGroupViewControllerAvatarWidth = 68; return [self.memberRecipientIds containsObject:recipientId]; } +#pragma mark - OWSNavigationView + +- (BOOL)shouldCancelNavigationBack +{ + BOOL result = self.hasUnsavedChanges; + if (self.hasUnsavedChanges) { + [self backButtonPressed]; + } + return result; +} + @end NS_ASSUME_NONNULL_END diff --git a/Signal/src/ViewControllers/OWSConversationSettingsViewController.m b/Signal/src/ViewControllers/OWSConversationSettingsViewController.m index de045a625a..a72897566c 100644 --- a/Signal/src/ViewControllers/OWSConversationSettingsViewController.m +++ b/Signal/src/ViewControllers/OWSConversationSettingsViewController.m @@ -827,10 +827,7 @@ NS_ASSUME_NONNULL_BEGIN updateGroupViewController.conversationSettingsViewDelegate = self.conversationSettingsViewDelegate; updateGroupViewController.thread = (TSGroupThread *)self.thread; updateGroupViewController.mode = mode; - - UINavigationController *navigationController = - [[UINavigationController alloc] initWithRootViewController:updateGroupViewController]; - [self presentViewController:navigationController animated:YES completion:nil]; + [self.navigationController pushViewController:updateGroupViewController animated:YES]; } - (void)presentContactViewController diff --git a/Signal/src/ViewControllers/OWSNavigationController.h b/Signal/src/ViewControllers/OWSNavigationController.h new file mode 100644 index 0000000000..26900eddea --- /dev/null +++ b/Signal/src/ViewControllers/OWSNavigationController.h @@ -0,0 +1,24 @@ +// +// Copyright (c) 2017 Open Whisper Systems. All rights reserved. +// + +#import + +// Any view controller which wants to be able cancel back button +// presses and back gestures should implement this protocol. +@protocol OWSNavigationView + +// shouldCancelNavigationBack will be called if the back button was pressed or +// if a back gesture was performed but not if the view is popped programmatically. +- (BOOL)shouldCancelNavigationBack; + +@end + +#pragma mark - + +// This navigation controller subclass should be used anywhere we might +// want to cancel back button presses or back gestures due to, for example, +// unsaved changes. +@interface OWSNavigationController : UINavigationController + +@end diff --git a/Signal/src/ViewControllers/OWSNavigationController.m b/Signal/src/ViewControllers/OWSNavigationController.m new file mode 100644 index 0000000000..0f21f71d0c --- /dev/null +++ b/Signal/src/ViewControllers/OWSNavigationController.m @@ -0,0 +1,76 @@ +// +// Copyright (c) 2017 Open Whisper Systems. All rights reserved. +// + +#import "OWSNavigationController.h" + +// We use a category to expose UINavigationController's private +// UINavigationBarDelegate methods. +@interface UINavigationController (OWSNavigationController) + +@end + +#pragma mark - + +@interface OWSNavigationController () + +@end + +#pragma mark - + +@implementation OWSNavigationController + +- (void)viewDidLoad +{ + [super viewDidLoad]; + + self.interactivePopGestureRecognizer.delegate = self; +} + +#pragma mark - UINavigationBarDelegate + +// All UINavigationController serve as the UINavigationBarDelegate for their navbar. +// We override shouldPopItem: in order to cancel some back button presses - for example, +// if a view has unsaved changes. +- (BOOL)navigationBar:(UINavigationBar *)navigationBar shouldPopItem:(UINavigationItem *)item +{ + OWSAssert(self.interactivePopGestureRecognizer.delegate == self); + UIViewController *topViewController = self.topViewController; + + // wasBackButtonClicked is YES if the back button was pressed but not + // if a back gesture was performed or if the view is popped programmatically. + BOOL wasBackButtonClicked = topViewController.navigationItem == item; + BOOL result = YES; + if (wasBackButtonClicked) { + if ([topViewController conformsToProtocol:@protocol(OWSNavigationView)]) { + id navigationView = (id)topViewController; + result = ![navigationView shouldCancelNavigationBack]; + } + } + + // If we're not going to cancel the pop/back, we need to call the super + // implementation since it has important side effects. + if (result) { + result = [super navigationBar:navigationBar shouldPopItem:item]; + OWSAssert(result); + } + return result; +} + +#pragma mark - UIGestureRecognizerDelegate + +// We serve as the UIGestureRecognizerDelegate of the interactivePopGestureRecognizer +// in order to cancel some "back" gestures - for example, +// if a view has unsaved changes. +- (BOOL)gestureRecognizerShouldBegin:(UIGestureRecognizer *)gestureRecognizer +{ + UIViewController *topViewController = self.topViewController; + if ([topViewController conformsToProtocol:@protocol(OWSNavigationView)]) { + id navigationView = (id)topViewController; + return ![navigationView shouldCancelNavigationBack]; + } else { + return YES; + } +} + +@end diff --git a/Signal/src/ViewControllers/ProfileViewController.h b/Signal/src/ViewControllers/ProfileViewController.h index 16471c80fb..fdad9ce4d9 100644 --- a/Signal/src/ViewControllers/ProfileViewController.h +++ b/Signal/src/ViewControllers/ProfileViewController.h @@ -6,8 +6,18 @@ NS_ASSUME_NONNULL_BEGIN +@class SignalsViewController; + @interface ProfileViewController : OWSTableViewController +- (instancetype)init NS_UNAVAILABLE; + ++ (BOOL)shouldDisplayProfileViewOnLaunch; + ++ (void)presentForAppSettings:(UINavigationController *)navigationController; ++ (void)presentForRegistration:(UINavigationController *)navigationController; ++ (void)presentForUpgradeOrNag:(SignalsViewController *)presentingController; + @end NS_ASSUME_NONNULL_END diff --git a/Signal/src/ViewControllers/ProfileViewController.m b/Signal/src/ViewControllers/ProfileViewController.m index 350bbfd04d..eb14108f86 100644 --- a/Signal/src/ViewControllers/ProfileViewController.m +++ b/Signal/src/ViewControllers/ProfileViewController.m @@ -3,17 +3,32 @@ // #import "ProfileViewController.h" +#import "AppDelegate.h" #import "AvatarViewHelper.h" +#import "OWSNavigationController.h" #import "OWSProfileManager.h" #import "Signal-Swift.h" +#import "SignalsNavigationController.h" +#import "SignalsViewController.h" #import "UIColor+OWS.h" #import "UIFont+OWS.h" #import "UIView+OWS.h" #import "UIViewController+OWS.h" +#import +#import NS_ASSUME_NONNULL_BEGIN -@interface ProfileViewController () +typedef NS_ENUM(NSInteger, ProfileViewMode) { + ProfileViewMode_AppSettings = 0, + ProfileViewMode_Registration, + ProfileViewMode_UpgradeOrNag, +}; + +NSString *const kProfileView_Collection = @"kProfileView_Collection"; +NSString *const kProfileView_LastPresentedDate = @"kProfileView_LastPresentedDate"; + +@interface ProfileViewController () @property (nonatomic, readonly) AvatarViewHelper *avatarViewHelper; @@ -29,12 +44,32 @@ NS_ASSUME_NONNULL_BEGIN @property (nonatomic) BOOL hasUnsavedChanges; +@property (nonatomic) ProfileViewMode profileViewMode; + @end #pragma mark - @implementation ProfileViewController +- (instancetype)initWithMode:(ProfileViewMode)profileViewMode +{ + self = [super init]; + + if (!self) { + return self; + } + + self.profileViewMode = profileViewMode; + + // Use the TSStorageManager.dbReadWriteConnection for consistency with the reads below. + [[[TSStorageManager sharedManager] dbReadWriteConnection] setDate:[NSDate new] + forKey:kProfileView_LastPresentedDate + inCollection:kProfileView_Collection]; + + return self; +} + - (void)loadView { [super loadView]; @@ -42,8 +77,6 @@ NS_ASSUME_NONNULL_BEGIN self.view.backgroundColor = [UIColor whiteColor]; [self.navigationController.navigationBar setTranslucent:NO]; self.title = NSLocalizedString(@"PROFILE_VIEW_TITLE", @"Title for the profile view."); - self.navigationItem.leftBarButtonItem = - [self createOWSBackButtonWithTarget:self selector:@selector(backButtonPressed:)]; _avatarViewHelper = [AvatarViewHelper new]; _avatarViewHelper.delegate = self; @@ -51,6 +84,7 @@ NS_ASSUME_NONNULL_BEGIN _avatar = [OWSProfileManager.sharedManager localProfileAvatarImage]; [self createViews]; + [self updateNavigationItem]; } - (void)createViews @@ -162,13 +196,18 @@ NS_ASSUME_NONNULL_BEGIN #pragma mark - Event Handling -- (void)backButtonPressed:(id)sender +- (void)backOrSkipButtonPressed +{ + [self leaveViewCheckingForUnsavedChanges]; +} + +- (void)leaveViewCheckingForUnsavedChanges { [self.nameTextField resignFirstResponder]; if (!self.hasUnsavedChanges) { // If user made no changes, return to conversation settings view. - [self.navigationController popViewControllerAnimated:YES]; + [self profileCompletedOrSkipped]; return; } @@ -185,7 +224,7 @@ NS_ASSUME_NONNULL_BEGIN @"The label for the 'discard' button in alerts and action sheets.") style:UIAlertActionStyleDestructive handler:^(UIAlertAction *action) { - [self.navigationController popViewControllerAnimated:YES]; + [self profileCompletedOrSkipped]; }]]; [controller addAction:[UIAlertAction actionWithTitle:NSLocalizedString(@"TXT_CANCEL_TITLE", nil) style:UIAlertActionStyleCancel @@ -204,14 +243,42 @@ NS_ASSUME_NONNULL_BEGIN { _hasUnsavedChanges = hasUnsavedChanges; - if (hasUnsavedChanges) { - self.navigationItem.rightBarButtonItem = (self.hasUnsavedChanges - ? [[UIBarButtonItem alloc] initWithTitle:NSLocalizedString(@"EDIT_GROUP_UPDATE_BUTTON", - @"The title for the 'update group' button.") - style:UIBarButtonItemStylePlain - target:self - action:@selector(updatePressed)] - : nil); + [self updateNavigationItem]; +} + +- (void)updateNavigationItem +{ + // The navigation bar is hidden in the registration workflow. + if (self.navigationController.navigationBarHidden) { + [self.navigationController setNavigationBarHidden:NO animated:YES]; + } + + // Always display a left item to leave the view without making changes. + // This might be a "back", "skip" or "cancel" button depending on the + // context. + switch (self.profileViewMode) { + case ProfileViewMode_AppSettings: + break; + case ProfileViewMode_UpgradeOrNag: + self.navigationItem.leftBarButtonItem = + [[UIBarButtonItem alloc] initWithBarButtonSystemItem:UIBarButtonSystemItemCancel + target:self + action:@selector(backOrSkipButtonPressed)]; + break; + case ProfileViewMode_Registration: + self.navigationItem.leftBarButtonItem = [[UIBarButtonItem alloc] + initWithTitle:NSLocalizedString(@"NAVIGATION_ITEM_SKIP_BUTTON", @"A button to skip a view.") + style:UIBarButtonItemStylePlain + target:self + action:@selector(backOrSkipButtonPressed)]; + break; + } + if (self.hasUnsavedChanges) { + // If we have a unsaved changes, right item should be a "save" button. + self.navigationItem.rightBarButtonItem = + [[UIBarButtonItem alloc] initWithBarButtonSystemItem:UIBarButtonSystemItemSave + target:self + action:@selector(updatePressed)]; } } @@ -239,8 +306,7 @@ NS_ASSUME_NONNULL_BEGIN success:^{ [alertController dismissViewControllerAnimated:NO completion:^{ - [weakSelf.navigationController - popViewControllerAnimated:YES]; + [weakSelf updateProfileCompleted]; }]; } failure:^{ @@ -265,6 +331,38 @@ NS_ASSUME_NONNULL_BEGIN return [self.nameTextField.text stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceCharacterSet]]; } +- (void)updateProfileCompleted +{ + [self profileCompletedOrSkipped]; +} + +- (void)profileCompletedOrSkipped +{ + // Dismiss this view. + switch (self.profileViewMode) { + case ProfileViewMode_AppSettings: + [self.navigationController popViewControllerAnimated:YES]; + break; + case ProfileViewMode_Registration: + [self showHomeView]; + break; + case ProfileViewMode_UpgradeOrNag: + [self dismissViewControllerAnimated:YES completion:nil]; + break; + } +} + +- (void)showHomeView +{ + SignalsViewController *homeView = [SignalsViewController new]; + homeView.newlyRegisteredUser = YES; + SignalsNavigationController *navigationController = + [[SignalsNavigationController alloc] initWithRootViewController:homeView]; + AppDelegate *appDelegate = (AppDelegate *)[UIApplication sharedApplication].delegate; + appDelegate.window.rootViewController = navigationController; + OWSAssert([navigationController.topViewController isKindOfClass:[SignalsViewController class]]); +} + #pragma mark - UITextFieldDelegate - (BOOL)textField:(UITextField *)textField @@ -313,6 +411,53 @@ NS_ASSUME_NONNULL_BEGIN #pragma mark - AvatarViewHelperDelegate ++ (BOOL)shouldDisplayProfileViewOnLaunch +{ + // Only nag until the user sets a profile _name_. Profile names are + // recommended; profile avatars are optional. + if ([OWSProfileManager sharedManager].localProfileName.length > 0) { + return NO; + } + + // Use the TSStorageManager.dbReadWriteConnection for consistency with the writes above. + NSTimeInterval kProfileNagFrequency = kDayInterval * 30; + NSDate *_Nullable lastPresentedDate = + [[[TSStorageManager sharedManager] dbReadWriteConnection] dateForKey:kProfileView_LastPresentedDate + inCollection:kProfileView_Collection]; + return (!lastPresentedDate || fabs([lastPresentedDate timeIntervalSinceNow]) > kProfileNagFrequency); +} + ++ (void)presentForAppSettings:(UINavigationController *)navigationController +{ + OWSAssert(navigationController); + OWSAssert([navigationController isKindOfClass:[OWSNavigationController class]]); + + ProfileViewController *vc = [[ProfileViewController alloc] initWithMode:ProfileViewMode_AppSettings]; + [navigationController pushViewController:vc animated:YES]; +} + ++ (void)presentForRegistration:(UINavigationController *)navigationController +{ + OWSAssert(navigationController); + OWSAssert([navigationController isKindOfClass:[OWSNavigationController class]]); + + ProfileViewController *vc = [[ProfileViewController alloc] initWithMode:ProfileViewMode_Registration]; + [navigationController pushViewController:vc animated:YES]; +} + ++ (void)presentForUpgradeOrNag:(SignalsViewController *)presentingController +{ + OWSAssert(presentingController); + + ProfileViewController *vc = [[ProfileViewController alloc] initWithMode:ProfileViewMode_UpgradeOrNag]; + OWSNavigationController *navigationController = [[OWSNavigationController alloc] initWithRootViewController:vc]; + [presentingController presentTopLevelModalViewController:navigationController + animateDismissal:YES + animatePresentation:YES]; +} + +#pragma mark - AvatarViewHelperDelegate + - (NSString *)avatarActionSheetTitle { return NSLocalizedString( @@ -347,6 +492,17 @@ NS_ASSUME_NONNULL_BEGIN self.avatar = nil; } +#pragma mark - OWSNavigationView + +- (BOOL)shouldCancelNavigationBack +{ + BOOL result = self.hasUnsavedChanges; + if (result) { + [self backOrSkipButtonPressed]; + } + return result; +} + #pragma mark - Logging + (NSString *)tag diff --git a/Signal/src/ViewControllers/SignalsNavigationController.h b/Signal/src/ViewControllers/SignalsNavigationController.h index 88d3f97ffb..7d8b6a0417 100644 --- a/Signal/src/ViewControllers/SignalsNavigationController.h +++ b/Signal/src/ViewControllers/SignalsNavigationController.h @@ -2,8 +2,8 @@ // Copyright (c) 2017 Open Whisper Systems. All rights reserved. // -#import +#import "OWSNavigationController.h" -@interface SignalsNavigationController : UINavigationController +@interface SignalsNavigationController : OWSNavigationController @end diff --git a/Signal/src/ViewControllers/SignalsViewController.m b/Signal/src/ViewControllers/SignalsViewController.m index 84290579d9..86d63a7be6 100644 --- a/Signal/src/ViewControllers/SignalsViewController.m +++ b/Signal/src/ViewControllers/SignalsViewController.m @@ -10,6 +10,8 @@ #import "MessagesViewController.h" #import "NSDate+millisecondTimeStamp.h" #import "OWSContactsManager.h" +#import "OWSNavigationController.h" +#import "ProfileViewController.h" #import "PropertyListPreferences.h" #import "PushManager.h" #import "Signal-Swift.h" @@ -49,6 +51,7 @@ typedef NS_ENUM(NSInteger, CellState) { kArchiveState, kInboxState }; @property (nonatomic) BOOL isViewVisible; @property (nonatomic) BOOL isAppInBackground; @property (nonatomic) BOOL shouldObserveDBModifications; +@property (nonatomic) BOOL hasBeenPresented; // Dependencies @@ -307,7 +310,7 @@ typedef NS_ENUM(NSInteger, CellState) { kArchiveState, kInboxState }; - (void)settingsButtonPressed:(id)sender { AppSettingsViewController *vc = [AppSettingsViewController new]; - UINavigationController *navigationController = [[UINavigationController alloc] initWithRootViewController:vc]; + OWSNavigationController *navigationController = [[OWSNavigationController alloc] initWithRootViewController:vc]; [self presentViewController:navigationController animated:YES completion:nil]; } @@ -351,8 +354,8 @@ typedef NS_ENUM(NSInteger, CellState) { kArchiveState, kInboxState }; // // We just want to make sure contact access is *complete* before showing the compose // screen to avoid flicker. - UINavigationController *navigationController = - [[UINavigationController alloc] initWithRootViewController:viewController]; + OWSNavigationController *navigationController = + [[OWSNavigationController alloc] initWithRootViewController:viewController]; [self presentTopLevelModalViewController:navigationController animateDismissal:YES animatePresentation:YES]; }]; } @@ -528,7 +531,11 @@ typedef NS_ENUM(NSInteger, CellState) { kArchiveState, kInboxState }; completion:^{ [self markAllUpgradeExperiencesAsSeen]; }]; + } else if (!self.hasBeenPresented && [ProfileViewController shouldDisplayProfileViewOnLaunch]) { + [ProfileViewController presentForUpgradeOrNag:self]; } + + self.hasBeenPresented = YES; } - (void)tableViewSetUp { diff --git a/Signal/src/ViewControllers/UpdateGroupViewController.m b/Signal/src/ViewControllers/UpdateGroupViewController.m index 0bf85eafc7..384367ebc1 100644 --- a/Signal/src/ViewControllers/UpdateGroupViewController.m +++ b/Signal/src/ViewControllers/UpdateGroupViewController.m @@ -10,6 +10,7 @@ #import "ContactsViewHelper.h" #import "Environment.h" #import "OWSContactsManager.h" +#import "OWSNavigationController.h" #import "OWSTableViewController.h" #import "Signal-Swift.h" #import "SignalKeyingStorage.h" @@ -33,7 +34,8 @@ NS_ASSUME_NONNULL_BEGIN AvatarViewHelperDelegate, AddToGroupViewControllerDelegate, OWSTableViewControllerDelegate, - UINavigationControllerDelegate> + UINavigationControllerDelegate, + OWSNavigationView> @property (nonatomic, readonly) OWSMessageSender *messageSender; @property (nonatomic, readonly) ContactsViewHelper *contactsViewHelper; @@ -103,8 +105,6 @@ NS_ASSUME_NONNULL_BEGIN self.previousMemberRecipientIds = [NSSet setWithArray:self.thread.groupModel.groupMemberIds]; self.title = NSLocalizedString(@"EDIT_GROUP_DEFAULT_TITLE", @"The navbar title for the 'update group' view."); - self.navigationItem.leftBarButtonItem = - [self createOWSBackButtonWithTarget:self selector:@selector(backButtonPressed:)]; // First section. @@ -409,13 +409,13 @@ NS_ASSUME_NONNULL_BEGIN #pragma mark - Event Handling -- (void)backButtonPressed:(id)sender +- (void)backButtonPressed { [self.groupNameTextField resignFirstResponder]; if (!self.hasUnsavedChanges) { // If user made no changes, return to conversation settings view. - [self dismissViewControllerAnimated:YES completion:nil]; + [self.navigationController popViewControllerAnimated:YES]; return; } @@ -441,7 +441,7 @@ NS_ASSUME_NONNULL_BEGIN @"The label for the 'don't save' button in action sheets.") style:UIAlertActionStyleDestructive handler:^(UIAlertAction *action) { - [self dismissViewControllerAnimated:YES completion:nil]; + [self.navigationController popViewControllerAnimated:YES]; }]]; [self presentViewController:controller animated:YES completion:nil]; } @@ -528,6 +528,17 @@ NS_ASSUME_NONNULL_BEGIN return [self.memberRecipientIds containsObject:recipientId]; } +#pragma mark - OWSNavigationView + +- (BOOL)shouldCancelNavigationBack +{ + BOOL result = self.hasUnsavedChanges; + if (result) { + [self backButtonPressed]; + } + return result; +} + @end NS_ASSUME_NONNULL_END diff --git a/Signal/src/util/UIViewController+OWS.h b/Signal/src/util/UIViewController+OWS.h index 5bbd8a0778..ac00b42e91 100644 --- a/Signal/src/util/UIViewController+OWS.h +++ b/Signal/src/util/UIViewController+OWS.h @@ -17,8 +17,6 @@ NS_ASSUME_NONNULL_BEGIN */ - (UIBarButtonItem *)createOWSBackButton; -- (UIBarButtonItem *)createOWSBackButtonWithTarget:(id)target selector:(SEL)selector; - @end NS_ASSUME_NONNULL_END diff --git a/Signal/translations/en.lproj/Localizable.strings b/Signal/translations/en.lproj/Localizable.strings index 4b924e6eaf..4da0fd5b3c 100644 --- a/Signal/translations/en.lproj/Localizable.strings +++ b/Signal/translations/en.lproj/Localizable.strings @@ -861,6 +861,9 @@ /* An explanation of the consequences of muting a thread. */ "MUTE_BEHAVIOR_EXPLANATION" = "You will not receive notifications for muted conversations."; +/* A button to skip a view. */ +"NAVIGATION_ITEM_SKIP_BUTTON" = "Skip"; + /* No comment provided by engineer. */ "NETWORK_ERROR_RECOVERY" = "Please check you're online and try again.";