From ffb4b3f9d2964838c227617a56d0374d1ef2dadd Mon Sep 17 00:00:00 2001
From: Matthew Chen <charlesmchen@gmail.com>
Date: Tue, 15 Aug 2017 17:55:07 -0400
Subject: [PATCH 1/6] Add profile view to registration workflow.

// FREEBIE
---
 .../AppSettingsViewController.m               |   1 +
 .../CodeVerificationViewController.m          |  19 ++--
 .../ViewControllers/ProfileViewController.h   |   8 ++
 .../ViewControllers/ProfileViewController.m   | 104 +++++++++++++++---
 .../translations/en.lproj/Localizable.strings |   3 +
 5 files changed, 110 insertions(+), 25 deletions(-)

diff --git a/Signal/src/ViewControllers/AppSettingsViewController.m b/Signal/src/ViewControllers/AppSettingsViewController.m
index fd1b6366c..44f107faf 100644
--- a/Signal/src/ViewControllers/AppSettingsViewController.m
+++ b/Signal/src/ViewControllers/AppSettingsViewController.m
@@ -317,6 +317,7 @@
 - (void)showProfile
 {
     ProfileViewController *vc = [[ProfileViewController alloc] init];
+    vc.profileViewMode = ProfileViewMode_AppSettings;
     [self.navigationController pushViewController:vc animated:YES];
 }
 
diff --git a/Signal/src/ViewControllers/CodeVerificationViewController.m b/Signal/src/ViewControllers/CodeVerificationViewController.m
index 505fbd4d8..837a4b3ae 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 <PromiseKit/AnyPromise.h>
@@ -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,12 @@ NS_ASSUME_NONNULL_BEGIN
         });
 }
 
+- (void)vericationWasCompleted
+{
+    ProfileViewController *vc = [[ProfileViewController alloc] init];
+    vc.profileViewMode = ProfileViewMode_Registration;
+    [self.navigationController pushViewController:vc animated:YES];
+}
 
 - (void)presentAlertWithVerificationError:(NSError *)error
 {
diff --git a/Signal/src/ViewControllers/ProfileViewController.h b/Signal/src/ViewControllers/ProfileViewController.h
index 16471c80f..d366ac026 100644
--- a/Signal/src/ViewControllers/ProfileViewController.h
+++ b/Signal/src/ViewControllers/ProfileViewController.h
@@ -6,8 +6,16 @@
 
 NS_ASSUME_NONNULL_BEGIN
 
+typedef NS_ENUM(NSInteger, ProfileViewMode) {
+    ProfileViewMode_AppSettings = 0,
+    ProfileViewMode_Registration,
+    ProfileViewMode_UpgradeOrNag,
+};
+
 @interface ProfileViewController : OWSTableViewController
 
+@property (nonatomic) ProfileViewMode profileViewMode;
+
 @end
 
 NS_ASSUME_NONNULL_END
diff --git a/Signal/src/ViewControllers/ProfileViewController.m b/Signal/src/ViewControllers/ProfileViewController.m
index 350bbfd04..a122a3fe9 100644
--- a/Signal/src/ViewControllers/ProfileViewController.m
+++ b/Signal/src/ViewControllers/ProfileViewController.m
@@ -3,9 +3,12 @@
 //
 
 #import "ProfileViewController.h"
+#import "AppDelegate.h"
 #import "AvatarViewHelper.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"
@@ -42,8 +45,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 +52,7 @@ NS_ASSUME_NONNULL_BEGIN
     _avatar = [OWSProfileManager.sharedManager localProfileAvatarImage];
 
     [self createViews];
+    [self updateNavigationItem];
 }
 
 - (void)createViews
@@ -164,11 +166,20 @@ NS_ASSUME_NONNULL_BEGIN
 
 - (void)backButtonPressed:(id)sender
 {
+    [self leaveViewCheckingForUnsavedChanges:^{
+        [self.navigationController popViewControllerAnimated:YES];
+    }];
+}
+
+- (void)leaveViewCheckingForUnsavedChanges:(void (^_Nonnull)())leaveViewBlock
+{
+    OWSAssert(leaveViewBlock);
+
     [self.nameTextField resignFirstResponder];
 
     if (!self.hasUnsavedChanges) {
         // If user made no changes, return to conversation settings view.
-        [self.navigationController popViewControllerAnimated:YES];
+        leaveViewBlock();
         return;
     }
 
@@ -185,7 +196,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];
+                                             leaveViewBlock();
                                          }]];
     [controller addAction:[UIAlertAction actionWithTitle:NSLocalizedString(@"TXT_CANCEL_TITLE", nil)
                                                    style:UIAlertActionStyleCancel
@@ -204,15 +215,43 @@ 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.
+    [self.navigationController setNavigationBarHidden:NO animated:YES];
+
+    UIBarButtonItem *rightItem = nil;
+    switch (self.profileViewMode) {
+        case ProfileViewMode_AppSettings:
+            self.navigationItem.leftBarButtonItem =
+                [self createOWSBackButtonWithTarget:self selector:@selector(backButtonPressed:)];
+            break;
+        case ProfileViewMode_Registration:
+        case ProfileViewMode_UpgradeOrNag:
+            // Registration and "upgrade or nag" mode don't need a back button.
+            self.navigationItem.hidesBackButton = YES;
+
+            // Registration and "upgrade or nag" mode have "skip" or "update".
+            //
+            // TODO: Should this be some other verb instead of "update"?
+            rightItem = [[UIBarButtonItem alloc]
+                initWithTitle:NSLocalizedString(@"NAVIGATION_ITEM_SKIP_BUTTON", @"A button to skip a view.")
+                        style:UIBarButtonItemStylePlain
+                       target:self
+                       action:@selector(skipPressed)];
+            break;
     }
+    if (self.hasUnsavedChanges) {
+        rightItem = [[UIBarButtonItem alloc]
+            initWithTitle:NSLocalizedString(@"EDIT_GROUP_UPDATE_BUTTON", @"The title for the 'update group' button.")
+                    style:UIBarButtonItemStylePlain
+                   target:self
+                   action:@selector(updatePressed)];
+    }
+    self.navigationItem.rightBarButtonItem = rightItem;
 }
 
 - (void)updatePressed
@@ -239,8 +278,7 @@ NS_ASSUME_NONNULL_BEGIN
                              success:^{
                                  [alertController dismissViewControllerAnimated:NO
                                                                      completion:^{
-                                                                         [weakSelf.navigationController
-                                                                             popViewControllerAnimated:YES];
+                                                                         [weakSelf updateProfileCompleted];
                                                                      }];
                              }
                              failure:^{
@@ -265,6 +303,44 @@ NS_ASSUME_NONNULL_BEGIN
     return [self.nameTextField.text stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceCharacterSet]];
 }
 
+- (void)updateProfileCompleted
+{
+    [self profileCompletedOrSkipped];
+}
+
+- (void)skipPressed
+{
+    [self leaveViewCheckingForUnsavedChanges:^{
+        [self profileCompletedOrSkipped];
+    }];
+}
+
+- (void)profileCompletedOrSkipped
+{
+    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
diff --git a/Signal/translations/en.lproj/Localizable.strings b/Signal/translations/en.lproj/Localizable.strings
index 4b924e6ea..4da0fd5b3 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.";
 

From 9d8c39684834a45dbce4a0d4a6037c4ba78e1dcf Mon Sep 17 00:00:00 2001
From: Matthew Chen <charlesmchen@gmail.com>
Date: Wed, 16 Aug 2017 10:25:36 -0400
Subject: [PATCH 2/6] Add profile view to upgrade/nag workflow.

// FREEBIE
---
 .../AppSettingsViewController.m               |   4 +-
 .../CodeVerificationViewController.m          |   4 +-
 .../ViewControllers/ProfileViewController.h   |  14 +-
 .../ViewControllers/ProfileViewController.m   | 137 +++++++++++++-----
 .../ViewControllers/SignalsViewController.m   |   6 +
 5 files changed, 119 insertions(+), 46 deletions(-)

diff --git a/Signal/src/ViewControllers/AppSettingsViewController.m b/Signal/src/ViewControllers/AppSettingsViewController.m
index 44f107faf..9a91317fb 100644
--- a/Signal/src/ViewControllers/AppSettingsViewController.m
+++ b/Signal/src/ViewControllers/AppSettingsViewController.m
@@ -316,9 +316,7 @@
 
 - (void)showProfile
 {
-    ProfileViewController *vc = [[ProfileViewController alloc] init];
-    vc.profileViewMode = ProfileViewMode_AppSettings;
-    [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 837a4b3ae..3040e5224 100644
--- a/Signal/src/ViewControllers/CodeVerificationViewController.m
+++ b/Signal/src/ViewControllers/CodeVerificationViewController.m
@@ -283,9 +283,7 @@ NS_ASSUME_NONNULL_BEGIN
 
 - (void)vericationWasCompleted
 {
-    ProfileViewController *vc = [[ProfileViewController alloc] init];
-    vc.profileViewMode = ProfileViewMode_Registration;
-    [self.navigationController pushViewController:vc animated:YES];
+    [ProfileViewController presentForRegistration:self.navigationController];
 }
 
 - (void)presentAlertWithVerificationError:(NSError *)error
diff --git a/Signal/src/ViewControllers/ProfileViewController.h b/Signal/src/ViewControllers/ProfileViewController.h
index d366ac026..fdad9ce4d 100644
--- a/Signal/src/ViewControllers/ProfileViewController.h
+++ b/Signal/src/ViewControllers/ProfileViewController.h
@@ -6,15 +6,17 @@
 
 NS_ASSUME_NONNULL_BEGIN
 
-typedef NS_ENUM(NSInteger, ProfileViewMode) {
-    ProfileViewMode_AppSettings = 0,
-    ProfileViewMode_Registration,
-    ProfileViewMode_UpgradeOrNag,
-};
+@class SignalsViewController;
 
 @interface ProfileViewController : OWSTableViewController
 
-@property (nonatomic) ProfileViewMode profileViewMode;
+- (instancetype)init NS_UNAVAILABLE;
+
++ (BOOL)shouldDisplayProfileViewOnLaunch;
+
++ (void)presentForAppSettings:(UINavigationController *)navigationController;
++ (void)presentForRegistration:(UINavigationController *)navigationController;
++ (void)presentForUpgradeOrNag:(SignalsViewController *)presentingController;
 
 @end
 
diff --git a/Signal/src/ViewControllers/ProfileViewController.m b/Signal/src/ViewControllers/ProfileViewController.m
index a122a3fe9..0230df82d 100644
--- a/Signal/src/ViewControllers/ProfileViewController.m
+++ b/Signal/src/ViewControllers/ProfileViewController.m
@@ -13,9 +13,20 @@
 #import "UIFont+OWS.h"
 #import "UIView+OWS.h"
 #import "UIViewController+OWS.h"
+#import <SignalServiceKit/NSDate+OWS.h>
+#import <SignalServiceKit/TSStorageManager.h>
 
 NS_ASSUME_NONNULL_BEGIN
 
+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 () <UITextFieldDelegate, AvatarViewHelperDelegate>
 
 @property (nonatomic, readonly) AvatarViewHelper *avatarViewHelper;
@@ -32,12 +43,34 @@ NS_ASSUME_NONNULL_BEGIN
 
 @property (nonatomic) BOOL hasUnsavedChanges;
 
+@property (nonatomic) ProfileViewMode profileViewMode;
+
+@property (nonatomic) YapDatabaseConnection *databaseConnection;
+
 @end
 
 #pragma mark -
 
 @implementation ProfileViewController
 
+- (instancetype)initWithMode:(ProfileViewMode)profileViewMode
+{
+    self = [super init];
+
+    if (!self) {
+        return self;
+    }
+
+    self.profileViewMode = profileViewMode;
+    self.databaseConnection = [[TSStorageManager sharedManager] newDatabaseConnection];
+
+    [self.databaseConnection setDate:[NSDate new]
+                              forKey:kProfileView_LastPresentedDate
+                        inCollection:kProfileView_Collection];
+
+    return self;
+}
+
 - (void)loadView
 {
     [super loadView];
@@ -164,22 +197,18 @@ NS_ASSUME_NONNULL_BEGIN
 
 #pragma mark - Event Handling
 
-- (void)backButtonPressed:(id)sender
+- (void)backOrSkipButtonPressed
 {
-    [self leaveViewCheckingForUnsavedChanges:^{
-        [self.navigationController popViewControllerAnimated:YES];
-    }];
+    [self leaveViewCheckingForUnsavedChanges];
 }
 
-- (void)leaveViewCheckingForUnsavedChanges:(void (^_Nonnull)())leaveViewBlock
+- (void)leaveViewCheckingForUnsavedChanges
 {
-    OWSAssert(leaveViewBlock);
-
     [self.nameTextField resignFirstResponder];
 
     if (!self.hasUnsavedChanges) {
         // If user made no changes, return to conversation settings view.
-        leaveViewBlock();
+        [self profileCompletedOrSkipped];
         return;
     }
 
@@ -196,7 +225,7 @@ NS_ASSUME_NONNULL_BEGIN
                                                      @"The label for the 'discard' button in alerts and action sheets.")
                                            style:UIAlertActionStyleDestructive
                                          handler:^(UIAlertAction *action) {
-                                             leaveViewBlock();
+                                             [self profileCompletedOrSkipped];
                                          }]];
     [controller addAction:[UIAlertAction actionWithTitle:NSLocalizedString(@"TXT_CANCEL_TITLE", nil)
                                                    style:UIAlertActionStyleCancel
@@ -221,37 +250,39 @@ NS_ASSUME_NONNULL_BEGIN
 - (void)updateNavigationItem
 {
     // The navigation bar is hidden in the registration workflow.
-    [self.navigationController setNavigationBarHidden:NO animated:YES];
+    if (self.navigationController.navigationBarHidden) {
+        [self.navigationController setNavigationBarHidden:NO animated:YES];
+    }
 
-    UIBarButtonItem *rightItem = nil;
+    // 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:
             self.navigationItem.leftBarButtonItem =
-                [self createOWSBackButtonWithTarget:self selector:@selector(backButtonPressed:)];
+                [self createOWSBackButtonWithTarget:self selector:@selector(backOrSkipButtonPressed)];
             break;
-        case ProfileViewMode_Registration:
         case ProfileViewMode_UpgradeOrNag:
-            // Registration and "upgrade or nag" mode don't need a back button.
-            self.navigationItem.hidesBackButton = YES;
-
-            // Registration and "upgrade or nag" mode have "skip" or "update".
-            //
-            // TODO: Should this be some other verb instead of "update"?
-            rightItem = [[UIBarButtonItem alloc]
+            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(skipPressed)];
+                       action:@selector(backOrSkipButtonPressed)];
             break;
     }
     if (self.hasUnsavedChanges) {
-        rightItem = [[UIBarButtonItem alloc]
-            initWithTitle:NSLocalizedString(@"EDIT_GROUP_UPDATE_BUTTON", @"The title for the 'update group' button.")
-                    style:UIBarButtonItemStylePlain
-                   target:self
-                   action:@selector(updatePressed)];
+        // 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)];
     }
-    self.navigationItem.rightBarButtonItem = rightItem;
 }
 
 - (void)updatePressed
@@ -308,15 +339,9 @@ NS_ASSUME_NONNULL_BEGIN
     [self profileCompletedOrSkipped];
 }
 
-- (void)skipPressed
-{
-    [self leaveViewCheckingForUnsavedChanges:^{
-        [self profileCompletedOrSkipped];
-    }];
-}
-
 - (void)profileCompletedOrSkipped
 {
+    // Dismiss this view.
     switch (self.profileViewMode) {
         case ProfileViewMode_AppSettings:
             [self.navigationController popViewControllerAnimated:YES];
@@ -389,6 +414,50 @@ 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;
+    }
+
+    NSTimeInterval kProfileNagFrequency = kDayInterval * 30;
+    NSDate *_Nullable lastPresentedDate =
+        [[[TSStorageManager sharedManager] dbReadConnection] dateForKey:kProfileView_LastPresentedDate
+                                                           inCollection:kProfileView_Collection];
+    return (!lastPresentedDate || fabs([lastPresentedDate timeIntervalSinceNow]) > kProfileNagFrequency);
+}
+
++ (void)presentForAppSettings:(UINavigationController *)navigationController
+{
+    OWSAssert(navigationController);
+
+    ProfileViewController *vc = [[ProfileViewController alloc] initWithMode:ProfileViewMode_AppSettings];
+    [navigationController pushViewController:vc animated:YES];
+}
+
++ (void)presentForRegistration:(UINavigationController *)navigationController
+{
+    OWSAssert(navigationController);
+
+    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];
+    UINavigationController *navigationController = [[UINavigationController alloc] initWithRootViewController:vc];
+    [presentingController presentTopLevelModalViewController:navigationController
+                                            animateDismissal:YES
+                                         animatePresentation:YES];
+}
+
+#pragma mark - AvatarViewHelperDelegate
+
 - (NSString *)avatarActionSheetTitle
 {
     return NSLocalizedString(
diff --git a/Signal/src/ViewControllers/SignalsViewController.m b/Signal/src/ViewControllers/SignalsViewController.m
index 84290579d..9db269e52 100644
--- a/Signal/src/ViewControllers/SignalsViewController.m
+++ b/Signal/src/ViewControllers/SignalsViewController.m
@@ -10,6 +10,7 @@
 #import "MessagesViewController.h"
 #import "NSDate+millisecondTimeStamp.h"
 #import "OWSContactsManager.h"
+#import "ProfileViewController.h"
 #import "PropertyListPreferences.h"
 #import "PushManager.h"
 #import "Signal-Swift.h"
@@ -49,6 +50,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
 
@@ -528,7 +530,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 {

From 08347478a2b2f0a62513b76f1a1e29b84169d049 Mon Sep 17 00:00:00 2001
From: Matthew Chen <charlesmchen@gmail.com>
Date: Thu, 17 Aug 2017 12:37:21 -0400
Subject: [PATCH 3/6] Implement alternative approach to veto-able back buttons.

// FREEBIE
---
 Signal.xcodeproj/project.pbxproj              |  6 +++
 Signal/src/AppDelegate.m                      |  5 ++-
 .../ViewControllers/NewGroupViewController.m  | 15 ++++++--
 .../OWSConversationSettingsViewController.m   |  5 +--
 .../ViewControllers/OWSNavigationController.h | 20 ++++++++++
 .../ViewControllers/OWSNavigationController.m | 37 +++++++++++++++++++
 .../ViewControllers/ProfileViewController.m   | 16 ++++++--
 .../SignalsNavigationController.h             |  4 +-
 .../ViewControllers/SignalsViewController.m   |  7 ++--
 .../UpdateGroupViewController.m               | 19 +++++++---
 Signal/src/util/UIViewController+OWS.h        |  2 -
 11 files changed, 109 insertions(+), 27 deletions(-)
 create mode 100644 Signal/src/ViewControllers/OWSNavigationController.h
 create mode 100644 Signal/src/ViewControllers/OWSNavigationController.m

diff --git a/Signal.xcodeproj/project.pbxproj b/Signal.xcodeproj/project.pbxproj
index e10cb3b11..85a903bd3 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 = "<group>"; };
 		34B3F8A01E8EA6040035BE1A /* ViewControllerUtils.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ViewControllerUtils.h; sourceTree = "<group>"; };
 		34B3F8A11E8EA6040035BE1A /* ViewControllerUtils.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ViewControllerUtils.m; sourceTree = "<group>"; };
+		34C42D591F45F7A80072EC04 /* OWSNavigationController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = OWSNavigationController.h; sourceTree = "<group>"; };
+		34C42D5A1F45F7A80072EC04 /* OWSNavigationController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = OWSNavigationController.m; sourceTree = "<group>"; };
 		34CCAF361F0C0599004084F4 /* AppUpdateNag.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = AppUpdateNag.h; sourceTree = "<group>"; };
 		34CCAF371F0C0599004084F4 /* AppUpdateNag.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = AppUpdateNag.m; sourceTree = "<group>"; };
 		34CCAF391F0C2748004084F4 /* OWSAddToContactViewController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = OWSAddToContactViewController.h; sourceTree = "<group>"; };
@@ -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 b97adf95b..c38553cdf 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/NewGroupViewController.m b/Signal/src/ViewControllers/NewGroupViewController.m
index 11f350826..03ef92704 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,13 @@ const NSUInteger kNewGroupViewControllerAvatarWidth = 68;
     return [self.memberRecipientIds containsObject:recipientId];
 }
 
+#pragma mark - OWSNavigationView
+
+- (void)navBackButtonPressed
+{
+    [self backButtonPressed];
+}
+
 @end
 
 NS_ASSUME_NONNULL_END
diff --git a/Signal/src/ViewControllers/OWSConversationSettingsViewController.m b/Signal/src/ViewControllers/OWSConversationSettingsViewController.m
index de045a625..a72897566 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 000000000..d8279dacb
--- /dev/null
+++ b/Signal/src/ViewControllers/OWSNavigationController.h
@@ -0,0 +1,20 @@
+//
+//  Copyright (c) 2017 Open Whisper Systems. All rights reserved.
+//
+
+#import <UIKit/UIKit.h>
+
+@protocol OWSNavigationView <NSObject>
+
+- (void)navBackButtonPressed;
+
+@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 000000000..e475f48a4
--- /dev/null
+++ b/Signal/src/ViewControllers/OWSNavigationController.m
@@ -0,0 +1,37 @@
+//
+//  Copyright (c) 2017 Open Whisper Systems. All rights reserved.
+//
+
+#import "OWSNavigationController.h"
+
+@interface OWSNavigationController ()
+
+@end
+
+#pragma mark -
+
+@implementation OWSNavigationController
+
+- (BOOL)navigationBar:(UINavigationBar *)navigationBar shouldPopItem:(UINavigationItem *)item
+{
+
+    UIViewController *topViewController = self.topViewController;
+    BOOL wasBackButtonClicked = topViewController.navigationItem == item;
+
+    if (wasBackButtonClicked) {
+        if ([topViewController respondsToSelector:@selector(navBackButtonPressed)]) {
+            // if user did press back on the view controller where you handle the navBackButtonPressed
+            [topViewController performSelector:@selector(navBackButtonPressed)];
+            return NO;
+        } else {
+            // if user did press back but you are not on the view controller that can handle the navBackButtonPressed
+            [self popViewControllerAnimated:YES];
+            return YES;
+        }
+    } else {
+        // when you call popViewController programmatically you do not want to pop it twice
+        return YES;
+    }
+}
+
+@end
diff --git a/Signal/src/ViewControllers/ProfileViewController.m b/Signal/src/ViewControllers/ProfileViewController.m
index 0230df82d..1c2c20f16 100644
--- a/Signal/src/ViewControllers/ProfileViewController.m
+++ b/Signal/src/ViewControllers/ProfileViewController.m
@@ -5,6 +5,7 @@
 #import "ProfileViewController.h"
 #import "AppDelegate.h"
 #import "AvatarViewHelper.h"
+#import "OWSNavigationController.h"
 #import "OWSProfileManager.h"
 #import "Signal-Swift.h"
 #import "SignalsNavigationController.h"
@@ -27,7 +28,7 @@ typedef NS_ENUM(NSInteger, ProfileViewMode) {
 NSString *const kProfileView_Collection = @"kProfileView_Collection";
 NSString *const kProfileView_LastPresentedDate = @"kProfileView_LastPresentedDate";
 
-@interface ProfileViewController () <UITextFieldDelegate, AvatarViewHelperDelegate>
+@interface ProfileViewController () <UITextFieldDelegate, AvatarViewHelperDelegate, OWSNavigationView>
 
 @property (nonatomic, readonly) AvatarViewHelper *avatarViewHelper;
 
@@ -259,8 +260,6 @@ NSString *const kProfileView_LastPresentedDate = @"kProfileView_LastPresentedDat
     // context.
     switch (self.profileViewMode) {
         case ProfileViewMode_AppSettings:
-            self.navigationItem.leftBarButtonItem =
-                [self createOWSBackButtonWithTarget:self selector:@selector(backOrSkipButtonPressed)];
             break;
         case ProfileViewMode_UpgradeOrNag:
             self.navigationItem.leftBarButtonItem =
@@ -432,6 +431,7 @@ NSString *const kProfileView_LastPresentedDate = @"kProfileView_LastPresentedDat
 + (void)presentForAppSettings:(UINavigationController *)navigationController
 {
     OWSAssert(navigationController);
+    OWSAssert([navigationController isKindOfClass:[OWSNavigationController class]]);
 
     ProfileViewController *vc = [[ProfileViewController alloc] initWithMode:ProfileViewMode_AppSettings];
     [navigationController pushViewController:vc animated:YES];
@@ -440,6 +440,7 @@ NSString *const kProfileView_LastPresentedDate = @"kProfileView_LastPresentedDat
 + (void)presentForRegistration:(UINavigationController *)navigationController
 {
     OWSAssert(navigationController);
+    OWSAssert([navigationController isKindOfClass:[OWSNavigationController class]]);
 
     ProfileViewController *vc = [[ProfileViewController alloc] initWithMode:ProfileViewMode_Registration];
     [navigationController pushViewController:vc animated:YES];
@@ -450,7 +451,7 @@ NSString *const kProfileView_LastPresentedDate = @"kProfileView_LastPresentedDat
     OWSAssert(presentingController);
 
     ProfileViewController *vc = [[ProfileViewController alloc] initWithMode:ProfileViewMode_UpgradeOrNag];
-    UINavigationController *navigationController = [[UINavigationController alloc] initWithRootViewController:vc];
+    OWSNavigationController *navigationController = [[OWSNavigationController alloc] initWithRootViewController:vc];
     [presentingController presentTopLevelModalViewController:navigationController
                                             animateDismissal:YES
                                          animatePresentation:YES];
@@ -492,6 +493,13 @@ NSString *const kProfileView_LastPresentedDate = @"kProfileView_LastPresentedDat
     self.avatar = nil;
 }
 
+#pragma mark - OWSNavigationView
+
+- (void)navBackButtonPressed
+{
+    [self backOrSkipButtonPressed];
+}
+
 #pragma mark - Logging
 
 + (NSString *)tag
diff --git a/Signal/src/ViewControllers/SignalsNavigationController.h b/Signal/src/ViewControllers/SignalsNavigationController.h
index 88d3f97ff..7d8b6a041 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 <UIKit/UIKit.h>
+#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 9db269e52..86d63a7be 100644
--- a/Signal/src/ViewControllers/SignalsViewController.m
+++ b/Signal/src/ViewControllers/SignalsViewController.m
@@ -10,6 +10,7 @@
 #import "MessagesViewController.h"
 #import "NSDate+millisecondTimeStamp.h"
 #import "OWSContactsManager.h"
+#import "OWSNavigationController.h"
 #import "ProfileViewController.h"
 #import "PropertyListPreferences.h"
 #import "PushManager.h"
@@ -309,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];
 }
 
@@ -353,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];
     }];
 }
diff --git a/Signal/src/ViewControllers/UpdateGroupViewController.m b/Signal/src/ViewControllers/UpdateGroupViewController.m
index 0bf85eafc..3ebb1edf2 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,13 @@ NS_ASSUME_NONNULL_BEGIN
     return [self.memberRecipientIds containsObject:recipientId];
 }
 
+#pragma mark - OWSNavigationView
+
+- (void)navBackButtonPressed
+{
+    [self backButtonPressed];
+}
+
 @end
 
 NS_ASSUME_NONNULL_END
diff --git a/Signal/src/util/UIViewController+OWS.h b/Signal/src/util/UIViewController+OWS.h
index 5bbd8a077..ac00b42e9 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

From 25b0f79615e541879d8dc3f6ef6a431ebf97eb1c Mon Sep 17 00:00:00 2001
From: Matthew Chen <charlesmchen@gmail.com>
Date: Fri, 18 Aug 2017 09:58:16 -0400
Subject: [PATCH 4/6] Rework "cancel navigate back" logic.

// FREEBIE
---
 .../ConversationView/MessagesViewController.m |  9 ---
 .../ViewControllers/NewGroupViewController.m  | 12 +++-
 .../ViewControllers/OWSNavigationController.h |  4 +-
 .../ViewControllers/OWSNavigationController.m | 61 +++++++++++++++----
 .../ViewControllers/ProfileViewController.m   | 13 +++-
 .../UpdateGroupViewController.m               | 12 +++-
 6 files changed, 80 insertions(+), 31 deletions(-)

diff --git a/Signal/src/ViewControllers/ConversationView/MessagesViewController.m b/Signal/src/ViewControllers/ConversationView/MessagesViewController.m
index 41322b8d9..e12348046 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 03ef92704..1dd609b1b 100644
--- a/Signal/src/ViewControllers/NewGroupViewController.m
+++ b/Signal/src/ViewControllers/NewGroupViewController.m
@@ -50,6 +50,7 @@ const NSUInteger kNewGroupViewControllerAvatarWidth = 68;
 @property (nonatomic) NSMutableSet<NSString *> *memberRecipientIds;
 
 @property (nonatomic) BOOL hasUnsavedChanges;
+@property (nonatomic) BOOL shouldIgnoreSavedChanges;
 @property (nonatomic) BOOL hasAppeared;
 
 @end
@@ -551,7 +552,7 @@ const NSUInteger kNewGroupViewControllerAvatarWidth = 68;
 {
     [self.groupNameTextField resignFirstResponder];
 
-    if (!self.hasUnsavedChanges) {
+    if (!self.hasUnsavedChanges || self.shouldIgnoreSavedChanges) {
         // If user made no changes, return to conversation settings view.
         [self.navigationController popViewControllerAnimated:YES];
         return;
@@ -570,6 +571,7 @@ const NSUInteger kNewGroupViewControllerAvatarWidth = 68;
                                                      @"The label for the 'discard' button in alerts and action sheets.")
                                            style:UIAlertActionStyleDestructive
                                          handler:^(UIAlertAction *action) {
+                                             self.shouldIgnoreSavedChanges = YES;
                                              [self.navigationController popViewControllerAnimated:YES];
                                          }]];
     [controller addAction:[UIAlertAction actionWithTitle:NSLocalizedString(@"TXT_CANCEL_TITLE", nil)
@@ -651,9 +653,13 @@ const NSUInteger kNewGroupViewControllerAvatarWidth = 68;
 
 #pragma mark - OWSNavigationView
 
-- (void)navBackButtonPressed
+- (BOOL)shouldCancelNavigationBack
 {
-    [self backButtonPressed];
+    BOOL result = self.hasUnsavedChanges && !self.shouldIgnoreSavedChanges;
+    if (result) {
+        [self backButtonPressed];
+    }
+    return result;
 }
 
 @end
diff --git a/Signal/src/ViewControllers/OWSNavigationController.h b/Signal/src/ViewControllers/OWSNavigationController.h
index d8279dacb..a47b602df 100644
--- a/Signal/src/ViewControllers/OWSNavigationController.h
+++ b/Signal/src/ViewControllers/OWSNavigationController.h
@@ -4,9 +4,11 @@
 
 #import <UIKit/UIKit.h>
 
+// Any view controller which wants to be able cancel back button
+// presses and back gestures should implement this protocol.
 @protocol OWSNavigationView <NSObject>
 
-- (void)navBackButtonPressed;
+- (BOOL)shouldCancelNavigationBack;
 
 @end
 
diff --git a/Signal/src/ViewControllers/OWSNavigationController.m b/Signal/src/ViewControllers/OWSNavigationController.m
index e475f48a4..b1fe079ab 100644
--- a/Signal/src/ViewControllers/OWSNavigationController.m
+++ b/Signal/src/ViewControllers/OWSNavigationController.m
@@ -4,7 +4,15 @@
 
 #import "OWSNavigationController.h"
 
-@interface OWSNavigationController ()
+// We use a category to expose UINavigationController's private
+// UINavigationBarDelegate methods.
+@interface UINavigationController (OWSNavigationController) <UINavigationBarDelegate>
+
+@end
+
+#pragma mark -
+
+@interface OWSNavigationController () <UIGestureRecognizerDelegate>
 
 @end
 
@@ -12,24 +20,53 @@
 
 @implementation OWSNavigationController
 
-- (BOOL)navigationBar:(UINavigationBar *)navigationBar shouldPopItem:(UINavigationItem *)item
+- (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;
-    BOOL wasBackButtonClicked = topViewController.navigationItem == item;
 
+    BOOL wasBackButtonClicked = topViewController.navigationItem == item;
+    BOOL result = YES;
     if (wasBackButtonClicked) {
-        if ([topViewController respondsToSelector:@selector(navBackButtonPressed)]) {
-            // if user did press back on the view controller where you handle the navBackButtonPressed
-            [topViewController performSelector:@selector(navBackButtonPressed)];
-            return NO;
-        } else {
-            // if user did press back but you are not on the view controller that can handle the navBackButtonPressed
-            [self popViewControllerAnimated:YES];
-            return YES;
+        if ([topViewController conformsToProtocol:@protocol(OWSNavigationView)]) {
+            id<OWSNavigationView> navigationView = (id<OWSNavigationView>)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<OWSNavigationView> navigationView = (id<OWSNavigationView>)topViewController;
+        return ![navigationView shouldCancelNavigationBack];
     } else {
-        // when you call popViewController programmatically you do not want to pop it twice
         return YES;
     }
 }
diff --git a/Signal/src/ViewControllers/ProfileViewController.m b/Signal/src/ViewControllers/ProfileViewController.m
index 1c2c20f16..40f0a7ce5 100644
--- a/Signal/src/ViewControllers/ProfileViewController.m
+++ b/Signal/src/ViewControllers/ProfileViewController.m
@@ -44,6 +44,8 @@ NSString *const kProfileView_LastPresentedDate = @"kProfileView_LastPresentedDat
 
 @property (nonatomic) BOOL hasUnsavedChanges;
 
+@property (nonatomic) BOOL shouldIgnoreSavedChanges;
+
 @property (nonatomic) ProfileViewMode profileViewMode;
 
 @property (nonatomic) YapDatabaseConnection *databaseConnection;
@@ -207,7 +209,7 @@ NSString *const kProfileView_LastPresentedDate = @"kProfileView_LastPresentedDat
 {
     [self.nameTextField resignFirstResponder];
 
-    if (!self.hasUnsavedChanges) {
+    if (!self.hasUnsavedChanges || self.shouldIgnoreSavedChanges) {
         // If user made no changes, return to conversation settings view.
         [self profileCompletedOrSkipped];
         return;
@@ -226,6 +228,7 @@ NSString *const kProfileView_LastPresentedDate = @"kProfileView_LastPresentedDat
                                                      @"The label for the 'discard' button in alerts and action sheets.")
                                            style:UIAlertActionStyleDestructive
                                          handler:^(UIAlertAction *action) {
+                                             self.shouldIgnoreSavedChanges = YES;
                                              [self profileCompletedOrSkipped];
                                          }]];
     [controller addAction:[UIAlertAction actionWithTitle:NSLocalizedString(@"TXT_CANCEL_TITLE", nil)
@@ -495,9 +498,13 @@ NSString *const kProfileView_LastPresentedDate = @"kProfileView_LastPresentedDat
 
 #pragma mark - OWSNavigationView
 
-- (void)navBackButtonPressed
+- (BOOL)shouldCancelNavigationBack
 {
-    [self backOrSkipButtonPressed];
+    BOOL result = self.hasUnsavedChanges && !self.shouldIgnoreSavedChanges;
+    if (result) {
+        [self backOrSkipButtonPressed];
+    }
+    return result;
 }
 
 #pragma mark - Logging
diff --git a/Signal/src/ViewControllers/UpdateGroupViewController.m b/Signal/src/ViewControllers/UpdateGroupViewController.m
index 3ebb1edf2..27ad12c2c 100644
--- a/Signal/src/ViewControllers/UpdateGroupViewController.m
+++ b/Signal/src/ViewControllers/UpdateGroupViewController.m
@@ -50,6 +50,7 @@ NS_ASSUME_NONNULL_BEGIN
 @property (nonatomic) NSMutableSet<NSString *> *memberRecipientIds;
 
 @property (nonatomic) BOOL hasUnsavedChanges;
+@property (nonatomic) BOOL shouldIgnoreSavedChanges;
 
 @end
 
@@ -413,7 +414,7 @@ NS_ASSUME_NONNULL_BEGIN
 {
     [self.groupNameTextField resignFirstResponder];
 
-    if (!self.hasUnsavedChanges) {
+    if (!self.hasUnsavedChanges || self.shouldIgnoreSavedChanges) {
         // If user made no changes, return to conversation settings view.
         [self.navigationController popViewControllerAnimated:YES];
         return;
@@ -441,6 +442,7 @@ NS_ASSUME_NONNULL_BEGIN
                                                              @"The label for the 'don't save' button in action sheets.")
                                                    style:UIAlertActionStyleDestructive
                                                  handler:^(UIAlertAction *action) {
+                                                     self.shouldIgnoreSavedChanges = YES;
                                                      [self.navigationController popViewControllerAnimated:YES];
                                                  }]];
     [self presentViewController:controller animated:YES completion:nil];
@@ -530,9 +532,13 @@ NS_ASSUME_NONNULL_BEGIN
 
 #pragma mark - OWSNavigationView
 
-- (void)navBackButtonPressed
+- (BOOL)shouldCancelNavigationBack
 {
-    [self backButtonPressed];
+    BOOL result = self.hasUnsavedChanges && !self.shouldIgnoreSavedChanges;
+    if (result) {
+        [self backButtonPressed];
+    }
+    return result;
 }
 
 @end

From 1b055c485d81e5f587774b2f014c47beabd9f4d2 Mon Sep 17 00:00:00 2001
From: Matthew Chen <charlesmchen@gmail.com>
Date: Fri, 18 Aug 2017 10:02:45 -0400
Subject: [PATCH 5/6] Rework "cancel navigate back" logic.

// FREEBIE
---
 Signal/src/ViewControllers/NewGroupViewController.m    | 8 +++-----
 Signal/src/ViewControllers/OWSNavigationController.h   | 2 ++
 Signal/src/ViewControllers/OWSNavigationController.m   | 2 ++
 Signal/src/ViewControllers/ProfileViewController.m     | 7 ++-----
 Signal/src/ViewControllers/UpdateGroupViewController.m | 6 ++----
 5 files changed, 11 insertions(+), 14 deletions(-)

diff --git a/Signal/src/ViewControllers/NewGroupViewController.m b/Signal/src/ViewControllers/NewGroupViewController.m
index 1dd609b1b..7af2fbaab 100644
--- a/Signal/src/ViewControllers/NewGroupViewController.m
+++ b/Signal/src/ViewControllers/NewGroupViewController.m
@@ -50,7 +50,6 @@ const NSUInteger kNewGroupViewControllerAvatarWidth = 68;
 @property (nonatomic) NSMutableSet<NSString *> *memberRecipientIds;
 
 @property (nonatomic) BOOL hasUnsavedChanges;
-@property (nonatomic) BOOL shouldIgnoreSavedChanges;
 @property (nonatomic) BOOL hasAppeared;
 
 @end
@@ -552,7 +551,7 @@ const NSUInteger kNewGroupViewControllerAvatarWidth = 68;
 {
     [self.groupNameTextField resignFirstResponder];
 
-    if (!self.hasUnsavedChanges || self.shouldIgnoreSavedChanges) {
+    if (!self.hasUnsavedChanges) {
         // If user made no changes, return to conversation settings view.
         [self.navigationController popViewControllerAnimated:YES];
         return;
@@ -571,7 +570,6 @@ const NSUInteger kNewGroupViewControllerAvatarWidth = 68;
                                                      @"The label for the 'discard' button in alerts and action sheets.")
                                            style:UIAlertActionStyleDestructive
                                          handler:^(UIAlertAction *action) {
-                                             self.shouldIgnoreSavedChanges = YES;
                                              [self.navigationController popViewControllerAnimated:YES];
                                          }]];
     [controller addAction:[UIAlertAction actionWithTitle:NSLocalizedString(@"TXT_CANCEL_TITLE", nil)
@@ -655,8 +653,8 @@ const NSUInteger kNewGroupViewControllerAvatarWidth = 68;
 
 - (BOOL)shouldCancelNavigationBack
 {
-    BOOL result = self.hasUnsavedChanges && !self.shouldIgnoreSavedChanges;
-    if (result) {
+    BOOL result = self.hasUnsavedChanges;
+    if (self.hasUnsavedChanges) {
         [self backButtonPressed];
     }
     return result;
diff --git a/Signal/src/ViewControllers/OWSNavigationController.h b/Signal/src/ViewControllers/OWSNavigationController.h
index a47b602df..26900edde 100644
--- a/Signal/src/ViewControllers/OWSNavigationController.h
+++ b/Signal/src/ViewControllers/OWSNavigationController.h
@@ -8,6 +8,8 @@
 // presses and back gestures should implement this protocol.
 @protocol OWSNavigationView <NSObject>
 
+// 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
diff --git a/Signal/src/ViewControllers/OWSNavigationController.m b/Signal/src/ViewControllers/OWSNavigationController.m
index b1fe079ab..0f21f71d0 100644
--- a/Signal/src/ViewControllers/OWSNavigationController.m
+++ b/Signal/src/ViewControllers/OWSNavigationController.m
@@ -37,6 +37,8 @@
     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) {
diff --git a/Signal/src/ViewControllers/ProfileViewController.m b/Signal/src/ViewControllers/ProfileViewController.m
index 40f0a7ce5..b48343bc9 100644
--- a/Signal/src/ViewControllers/ProfileViewController.m
+++ b/Signal/src/ViewControllers/ProfileViewController.m
@@ -44,8 +44,6 @@ NSString *const kProfileView_LastPresentedDate = @"kProfileView_LastPresentedDat
 
 @property (nonatomic) BOOL hasUnsavedChanges;
 
-@property (nonatomic) BOOL shouldIgnoreSavedChanges;
-
 @property (nonatomic) ProfileViewMode profileViewMode;
 
 @property (nonatomic) YapDatabaseConnection *databaseConnection;
@@ -209,7 +207,7 @@ NSString *const kProfileView_LastPresentedDate = @"kProfileView_LastPresentedDat
 {
     [self.nameTextField resignFirstResponder];
 
-    if (!self.hasUnsavedChanges || self.shouldIgnoreSavedChanges) {
+    if (!self.hasUnsavedChanges) {
         // If user made no changes, return to conversation settings view.
         [self profileCompletedOrSkipped];
         return;
@@ -228,7 +226,6 @@ NSString *const kProfileView_LastPresentedDate = @"kProfileView_LastPresentedDat
                                                      @"The label for the 'discard' button in alerts and action sheets.")
                                            style:UIAlertActionStyleDestructive
                                          handler:^(UIAlertAction *action) {
-                                             self.shouldIgnoreSavedChanges = YES;
                                              [self profileCompletedOrSkipped];
                                          }]];
     [controller addAction:[UIAlertAction actionWithTitle:NSLocalizedString(@"TXT_CANCEL_TITLE", nil)
@@ -500,7 +497,7 @@ NSString *const kProfileView_LastPresentedDate = @"kProfileView_LastPresentedDat
 
 - (BOOL)shouldCancelNavigationBack
 {
-    BOOL result = self.hasUnsavedChanges && !self.shouldIgnoreSavedChanges;
+    BOOL result = self.hasUnsavedChanges;
     if (result) {
         [self backOrSkipButtonPressed];
     }
diff --git a/Signal/src/ViewControllers/UpdateGroupViewController.m b/Signal/src/ViewControllers/UpdateGroupViewController.m
index 27ad12c2c..384367ebc 100644
--- a/Signal/src/ViewControllers/UpdateGroupViewController.m
+++ b/Signal/src/ViewControllers/UpdateGroupViewController.m
@@ -50,7 +50,6 @@ NS_ASSUME_NONNULL_BEGIN
 @property (nonatomic) NSMutableSet<NSString *> *memberRecipientIds;
 
 @property (nonatomic) BOOL hasUnsavedChanges;
-@property (nonatomic) BOOL shouldIgnoreSavedChanges;
 
 @end
 
@@ -414,7 +413,7 @@ NS_ASSUME_NONNULL_BEGIN
 {
     [self.groupNameTextField resignFirstResponder];
 
-    if (!self.hasUnsavedChanges || self.shouldIgnoreSavedChanges) {
+    if (!self.hasUnsavedChanges) {
         // If user made no changes, return to conversation settings view.
         [self.navigationController popViewControllerAnimated:YES];
         return;
@@ -442,7 +441,6 @@ NS_ASSUME_NONNULL_BEGIN
                                                              @"The label for the 'don't save' button in action sheets.")
                                                    style:UIAlertActionStyleDestructive
                                                  handler:^(UIAlertAction *action) {
-                                                     self.shouldIgnoreSavedChanges = YES;
                                                      [self.navigationController popViewControllerAnimated:YES];
                                                  }]];
     [self presentViewController:controller animated:YES completion:nil];
@@ -534,7 +532,7 @@ NS_ASSUME_NONNULL_BEGIN
 
 - (BOOL)shouldCancelNavigationBack
 {
-    BOOL result = self.hasUnsavedChanges && !self.shouldIgnoreSavedChanges;
+    BOOL result = self.hasUnsavedChanges;
     if (result) {
         [self backButtonPressed];
     }

From 27e496ad06593cdd17f12af60945286c7dbabbee Mon Sep 17 00:00:00 2001
From: Matthew Chen <charlesmchen@gmail.com>
Date: Fri, 18 Aug 2017 10:11:37 -0400
Subject: [PATCH 6/6] Respond to CR.

// FREEBIE
---
 .../src/ViewControllers/ProfileViewController.m   | 15 +++++++--------
 1 file changed, 7 insertions(+), 8 deletions(-)

diff --git a/Signal/src/ViewControllers/ProfileViewController.m b/Signal/src/ViewControllers/ProfileViewController.m
index b48343bc9..eb14108f8 100644
--- a/Signal/src/ViewControllers/ProfileViewController.m
+++ b/Signal/src/ViewControllers/ProfileViewController.m
@@ -46,8 +46,6 @@ NSString *const kProfileView_LastPresentedDate = @"kProfileView_LastPresentedDat
 
 @property (nonatomic) ProfileViewMode profileViewMode;
 
-@property (nonatomic) YapDatabaseConnection *databaseConnection;
-
 @end
 
 #pragma mark -
@@ -63,11 +61,11 @@ NSString *const kProfileView_LastPresentedDate = @"kProfileView_LastPresentedDat
     }
 
     self.profileViewMode = profileViewMode;
-    self.databaseConnection = [[TSStorageManager sharedManager] newDatabaseConnection];
 
-    [self.databaseConnection setDate:[NSDate new]
-                              forKey:kProfileView_LastPresentedDate
-                        inCollection:kProfileView_Collection];
+    // Use the TSStorageManager.dbReadWriteConnection for consistency with the reads below.
+    [[[TSStorageManager sharedManager] dbReadWriteConnection] setDate:[NSDate new]
+                                                               forKey:kProfileView_LastPresentedDate
+                                                         inCollection:kProfileView_Collection];
 
     return self;
 }
@@ -421,10 +419,11 @@ NSString *const kProfileView_LastPresentedDate = @"kProfileView_LastPresentedDat
         return NO;
     }
 
+    // Use the TSStorageManager.dbReadWriteConnection for consistency with the writes above.
     NSTimeInterval kProfileNagFrequency = kDayInterval * 30;
     NSDate *_Nullable lastPresentedDate =
-        [[[TSStorageManager sharedManager] dbReadConnection] dateForKey:kProfileView_LastPresentedDate
-                                                           inCollection:kProfileView_Collection];
+        [[[TSStorageManager sharedManager] dbReadWriteConnection] dateForKey:kProfileView_LastPresentedDate
+                                                                inCollection:kProfileView_Collection];
     return (!lastPresentedDate || fabs([lastPresentedDate timeIntervalSinceNow]) > kProfileNagFrequency);
 }