diff --git a/Signal/Images.xcassets/introductory_splash_profile.imageset/Contents.json b/Signal/Images.xcassets/introductory_splash_profile.imageset/Contents.json new file mode 100644 index 0000000000..e6a3d1e58d --- /dev/null +++ b/Signal/Images.xcassets/introductory_splash_profile.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "introductory_splash_profile.png", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "introductory_splash_profile@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "filename" : "introductory_splash_profile@3x.png", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Signal/Images.xcassets/introductory_splash_profile.imageset/introductory_splash_profile.png b/Signal/Images.xcassets/introductory_splash_profile.imageset/introductory_splash_profile.png new file mode 100644 index 0000000000..76492d6607 Binary files /dev/null and b/Signal/Images.xcassets/introductory_splash_profile.imageset/introductory_splash_profile.png differ diff --git a/Signal/Images.xcassets/introductory_splash_profile.imageset/introductory_splash_profile@2x.png b/Signal/Images.xcassets/introductory_splash_profile.imageset/introductory_splash_profile@2x.png new file mode 100644 index 0000000000..7bb3e0075d Binary files /dev/null and b/Signal/Images.xcassets/introductory_splash_profile.imageset/introductory_splash_profile@2x.png differ diff --git a/Signal/Images.xcassets/introductory_splash_profile.imageset/introductory_splash_profile@3x.png b/Signal/Images.xcassets/introductory_splash_profile.imageset/introductory_splash_profile@3x.png new file mode 100644 index 0000000000..05cded0958 Binary files /dev/null and b/Signal/Images.xcassets/introductory_splash_profile.imageset/introductory_splash_profile@3x.png differ diff --git a/Signal/src/Signal-Bridging-Header.h b/Signal/src/Signal-Bridging-Header.h index 4eecc63370..61b979666c 100644 --- a/Signal/src/Signal-Bridging-Header.h +++ b/Signal/src/Signal-Bridging-Header.h @@ -24,6 +24,7 @@ #import "OWSViewController.h" #import "OWSWebRTCDataProtos.pb.h" #import "PrivacySettingsTableViewController.h" +#import "ProfileViewController.h" #import "PropertyListPreferences.h" #import "PushManager.h" #import "Release.h" diff --git a/Signal/src/ViewControllers/DebugUI/DebugUIMisc.m b/Signal/src/ViewControllers/DebugUI/DebugUIMisc.m index db6296fc00..ec66c4a93f 100644 --- a/Signal/src/ViewControllers/DebugUI/DebugUIMisc.m +++ b/Signal/src/ViewControllers/DebugUI/DebugUIMisc.m @@ -53,6 +53,10 @@ NS_ASSUME_NONNULL_BEGIN actionBlock:^{ [DebugUIMisc setManualCensorshipCircumventionEnabled:NO]; }]]; + [items addObject:[OWSTableItem itemWithTitle:@"Clear experience upgrades (works once per launch)" + actionBlock:^{ + [ExperienceUpgrade removeAllObjectsInCollection]; + }]]; [items addObject:[OWSTableItem itemWithTitle:@"Clear hasDismissedOffers" actionBlock:^{ [DebugUIMisc clearHasDismissedOffers]; diff --git a/Signal/src/ViewControllers/ExperienceUpgradesPageViewController.swift b/Signal/src/ViewControllers/ExperienceUpgradesPageViewController.swift index 309d38cdeb..f0763ff41c 100644 --- a/Signal/src/ViewControllers/ExperienceUpgradesPageViewController.swift +++ b/Signal/src/ViewControllers/ExperienceUpgradesPageViewController.swift @@ -4,6 +4,94 @@ import Foundation +private class IntroductingProfilesExperienceUpgradeViewController: ExperienceUpgradeViewController { + + override func loadView() { + self.view = UIView() + + /// Create Views + + // Title label + let titleLabel = UILabel() + view.addSubview(titleLabel) + titleLabel.text = header + titleLabel.textAlignment = .center + titleLabel.font = UIFont.ows_regularFont(withSize: ScaleFromIPhone5(24)) + titleLabel.textColor = UIColor.white + titleLabel.minimumScaleFactor = 0.5 + titleLabel.adjustsFontSizeToFitWidth = true + + // Body label + let bodyLabel = UILabel() + self.bodyLabel = bodyLabel + view.addSubview(bodyLabel) + bodyLabel.text = body + bodyLabel.font = UIFont.ows_lightFont(withSize: ScaleFromIPhone5To7Plus(17, 22)) + bodyLabel.textColor = UIColor.black + bodyLabel.numberOfLines = 0 + bodyLabel.lineBreakMode = .byWordWrapping + bodyLabel.textAlignment = .center + + // Image + let imageView = UIImageView(image: image) + view.addSubview(imageView) + imageView.contentMode = .scaleAspectFit + + // Button + let button = UIButton() + view.addSubview(button) + let buttonTitle = NSLocalizedString("UPGRADE_EXPERIENCE_INTRODUCING_PROFILES_BUTTON", comment: "button label shown one time, after user upgrades app") + button.setTitle(buttonTitle, for: .normal) + button.setTitleColor(UIColor.white, for: .normal) + button.backgroundColor = UIColor.ows_materialBlue() + + button.isUserInteractionEnabled = true + button.addTarget(self, action:#selector(didTapButton), for: .touchUpInside) + button.contentEdgeInsets = UIEdgeInsets(top: 10, left: 20, bottom: 10, right: 20) + + button.titleLabel?.font = UIFont.ows_mediumFont(withSize: ScaleFromIPhone5(18)) + + /// Layout Views + + // Image layout + imageView.autoAlignAxis(toSuperviewAxis: .vertical) + imageView.autoPinToSquareAspectRatio() + imageView.autoPinEdge(.top, to: .bottom, of: titleLabel, withOffset: ScaleFromIPhone5To7Plus(36, 40)) + imageView.autoSetDimension(.height, toSize: ScaleFromIPhone5(225)) + + // Title label layout + titleLabel.autoSetDimension(.height, toSize: ScaleFromIPhone5(40)) + titleLabel.autoPinWidthToSuperview(withMargin: ScaleFromIPhone5To7Plus(16, 24)) + titleLabel.autoPinEdge(toSuperviewEdge: .top) + + // Body label layout + bodyLabel.autoPinEdge(.top, to: .bottom, of: imageView, withOffset: ScaleFromIPhone5To7Plus(18, 28)) + bodyLabel.autoPinWidthToSuperview(withMargin: bodyMargin) + bodyLabel.sizeToFit() + + // Button layout + button.autoPinEdge(.top, to: .bottom, of: bodyLabel, withOffset: ScaleFromIPhone5(18)) + button.autoPinWidthToSuperview(withMargin: ScaleFromIPhone5(32)) + button.autoPinEdge(toSuperviewEdge: .bottom, withInset: ScaleFromIPhone5(16)) + button.autoSetDimension(.height, toSize: ScaleFromIPhone5(36)) + } + + // MARK: - Actions + + func didTapButton(sender: UIButton) { + Logger.debug("\(TAG) in \(#function)") + + // dismiss the modally presented view controller, then proceed. + experienceUpgradesPageViewController.dismiss(animated: true) { + guard let fromViewController = UIApplication.shared.frontmostViewController as? SignalsViewController else { + owsFail("unexpected frontmostViewController: \(String(describing: UIApplication.shared.frontmostViewController))") + return + } + ProfileViewController.presentForUpgradeOrNag(from: fromViewController) + } + } +} + private class CallKitExperienceUpgradeViewController: ExperienceUpgradeViewController { override func loadView() { @@ -79,7 +167,7 @@ private class ExperienceUpgradeViewController: OWSViewController { view.addSubview(titleLabel) titleLabel.text = header titleLabel.textAlignment = .center - titleLabel.font = UIFont.ows_regularFont(withSize: ScaleFromIPhone5To7Plus(26, 32)) + titleLabel.font = UIFont.ows_regularFont(withSize: ScaleFromIPhone5(24)) titleLabel.textColor = UIColor.white titleLabel.minimumScaleFactor = 0.5 titleLabel.adjustsFontSizeToFitWidth = true @@ -102,19 +190,22 @@ private class ExperienceUpgradeViewController: OWSViewController { /// Layout Views + // Image layout + imageView.autoAlignAxis(toSuperviewAxis: .vertical) + imageView.autoPinToSquareAspectRatio() + imageView.autoPinEdge(.top, to: .bottom, of: titleLabel, withOffset: ScaleFromIPhone5To7Plus(36, 60)) + imageView.autoSetDimension(.height, toSize: ScaleFromIPhone5(225)) + // Title label layout + titleLabel.autoSetDimension(.height, toSize: ScaleFromIPhone5(40)) titleLabel.autoPinWidthToSuperview(withMargin: ScaleFromIPhone5To7Plus(16, 24)) titleLabel.autoPinEdge(toSuperviewEdge: .top) // Body label layout + bodyLabel.autoPinEdge(.top, to: .bottom, of: imageView, withOffset: ScaleFromIPhone5To7Plus(18, 28)) bodyLabel.autoPinWidthToSuperview(withMargin: bodyMargin) bodyLabel.sizeToFit() - - // Image layout - imageView.autoPinWidthToSuperview() - imageView.autoSetDimension(.height, toSize: ScaleFromIPhone5To7Plus(200, 280)) - imageView.autoPinEdge(.top, to: .bottom, of: titleLabel, withOffset: ScaleFromIPhone5To7Plus(24, 32)) - imageView.autoPinEdge(.bottom, to: .top, of: bodyLabel, withOffset: -ScaleFromIPhone5To7Plus(18, 28)) + bodyLabel.autoPinEdge(toSuperviewEdge: .bottom, withInset: ScaleFromIPhone5(16)) } } @@ -122,7 +213,7 @@ func setPageControlAppearance() { if #available(iOS 9.0, *) { let pageControl = UIPageControl.appearance(whenContainedInInstancesOf: [UIPageViewController.self]) pageControl.pageIndicatorTintColor = UIColor.lightGray - pageControl.currentPageIndicatorTintColor = UIColor.white + pageControl.currentPageIndicatorTintColor = UIColor.ows_materialBlue() } else { // iOS8 won't see the page controls =( } @@ -185,20 +276,14 @@ class ExperienceUpgradesPageViewController: OWSViewController, UIPageViewControl view.addSubview(headerBackgroundView) headerBackgroundView.backgroundColor = UIColor.ows_materialBlue() - // Footer Background - - let footerBackgroundView = UIView() - view.addSubview(footerBackgroundView) - footerBackgroundView.backgroundColor = UIColor.ows_materialBlue() - // Dismiss button let dismissButton = UIButton() view.addSubview(dismissButton) dismissButton.setTitle(CommonStrings.dismissButton, for: .normal) - dismissButton.setTitleColor(UIColor.white, for: .normal) + dismissButton.setTitleColor(UIColor.ows_signalBrandBlue(), for: .normal) dismissButton.isUserInteractionEnabled = true dismissButton.addTarget(self, action:#selector(didTapDismissButton), for: .touchUpInside) - dismissButton.titleLabel?.font = UIFont.ows_mediumFont(withSize: ScaleFromIPhone5(20)) + dismissButton.titleLabel?.font = UIFont.ows_mediumFont(withSize: ScaleFromIPhone5(16)) let dismissInsetValue: CGFloat = ScaleFromIPhone5(10) dismissButton.contentEdgeInsets = UIEdgeInsets(top: dismissInsetValue, left: dismissInsetValue, bottom: dismissInsetValue, right: dismissInsetValue) @@ -216,11 +301,6 @@ class ExperienceUpgradesPageViewController: OWSViewController, UIPageViewControl headerBackgroundView.autoPinEdge(toSuperviewEdge: .top) headerBackgroundView.autoSetDimension(.height, toSize: ScaleFromIPhone5(80)) - // Footer Background layout - footerBackgroundView.autoPinWidthToSuperview() - footerBackgroundView.autoPinEdge(toSuperviewEdge: .bottom) - footerBackgroundView.autoSetDimension(.height, toSize: ScaleFromIPhone5(95)) - // Dismiss button layout dismissButton.autoHCenterInSuperview() dismissButton.autoPinEdge(toSuperviewEdge: .bottom, withInset: ScaleFromIPhone5(10)) @@ -230,7 +310,7 @@ class ExperienceUpgradesPageViewController: OWSViewController, UIPageViewControl // negative inset so as to overlay the header text in the carousel view with the header background which // lives outside of the carousel. We do this so that the user can't bounce past the page view controllers // width limits, exposing the edge of the header. - carouselView.autoPinEdge(.top, to: .bottom, of: headerBackgroundView, withOffset: ScaleFromIPhone5(-35)) + carouselView.autoPin(toTopLayoutGuideOf: self, withInset: ScaleFromIPhone5To7Plus(14, 24)) carouselView.autoPinEdge(.bottom, to: .top, of: dismissButton, withOffset: ScaleFromIPhone5(-10)) } @@ -301,6 +381,8 @@ class ExperienceUpgradesPageViewController: OWSViewController, UIPageViewControl switch identifier { case .callKit: return CallKitExperienceUpgradeViewController(experienceUpgrade: experienceUpgrade, experienceUpgradesPageViewController: self) + case .introducingProfiles: + return IntroductingProfilesExperienceUpgradeViewController(experienceUpgrade: experienceUpgrade, experienceUpgradesPageViewController: self) default: return ExperienceUpgradeViewController(experienceUpgrade: experienceUpgrade, experienceUpgradesPageViewController: self) } diff --git a/Signal/src/ViewControllers/ProfileViewController.h b/Signal/src/ViewControllers/ProfileViewController.h index 88764363af..ca6f3b4620 100644 --- a/Signal/src/ViewControllers/ProfileViewController.h +++ b/Signal/src/ViewControllers/ProfileViewController.h @@ -14,7 +14,8 @@ NS_ASSUME_NONNULL_BEGIN + (void)presentForAppSettings:(UINavigationController *)navigationController; + (void)presentForRegistration:(UINavigationController *)navigationController; -+ (void)presentForUpgradeOrNag:(SignalsViewController *)presentingController; ++ (void)presentForUpgradeOrNag:(SignalsViewController *)presentingController + NS_SWIFT_NAME(presentForUpgradeOrNag(from:)); @end diff --git a/Signal/src/ViewControllers/SignalsViewController.m b/Signal/src/ViewControllers/SignalsViewController.m index 57f6be088a..7433a0b53e 100644 --- a/Signal/src/ViewControllers/SignalsViewController.m +++ b/Signal/src/ViewControllers/SignalsViewController.m @@ -47,6 +47,7 @@ typedef NS_ENUM(NSInteger, CellState) { kArchiveState, kInboxState }; @property (nonatomic) UISegmentedControl *segmentedControl; @property (nonatomic) id previewingContext; @property (nonatomic) NSSet *blockedPhoneNumberSet; +@property (nonatomic) BOOL viewHasEverAppeared; @property (nonatomic) BOOL isViewVisible; @property (nonatomic) BOOL isAppInBackground; @@ -491,9 +492,11 @@ typedef NS_ENUM(NSInteger, CellState) { kArchiveState, kInboxState }; // Start running the disappearing messages job in case the newly registered user // enables this feature [[OWSDisappearingMessagesJob sharedJob] startIfNecessary]; - } else { + } else if (!self.viewHasEverAppeared) { [self displayAnyUnseenUpgradeExperience]; } + + self.viewHasEverAppeared = YES; } #pragma mark - startup diff --git a/Signal/src/environment/ExperienceUpgradeFinder.swift b/Signal/src/environment/ExperienceUpgradeFinder.swift index 0536779617..84fde088a0 100644 --- a/Signal/src/environment/ExperienceUpgradeFinder.swift +++ b/Signal/src/environment/ExperienceUpgradeFinder.swift @@ -6,27 +6,45 @@ import Foundation enum ExperienceUpgradeId: String { case videoCalling = "001", - callKit = "002" + callKit = "002", + introducingProfiles = "003" } class ExperienceUpgradeFinder: NSObject { public let TAG = "[ExperienceUpgradeFinder]" + var videoCalling: ExperienceUpgrade { + return ExperienceUpgrade(uniqueId: ExperienceUpgradeId.videoCalling.rawValue, + title: NSLocalizedString("UPGRADE_EXPERIENCE_VIDEO_TITLE", comment: "Header for upgrade experience"), + body: NSLocalizedString("UPGRADE_EXPERIENCE_VIDEO_DESCRIPTION", comment: "Description of video calling to upgrading (existing) users"), + image: #imageLiteral(resourceName: "introductory_splash_video_calling")) + } + + var callKit: ExperienceUpgrade { + return ExperienceUpgrade(uniqueId: ExperienceUpgradeId.callKit.rawValue, + title: NSLocalizedString("UPGRADE_EXPERIENCE_CALLKIT_TITLE", comment: "Header for upgrade experience"), + body: NSLocalizedString("UPGRADE_EXPERIENCE_CALLKIT_DESCRIPTION", comment: "Description of CallKit to upgrading (existing) users"), + image: #imageLiteral(resourceName: "introductory_splash_callkit")) + } + + var introducingProfiles: ExperienceUpgrade { + return ExperienceUpgrade(uniqueId: ExperienceUpgradeId.introducingProfiles.rawValue, + title: NSLocalizedString("UPGRADE_EXPERIENCE_INTRODUCING_PROFILES_TITLE", comment: "Header for upgrade experience"), + body: NSLocalizedString("UPGRADE_EXPERIENCE_INTRODUCING_PROFILES_DESCRIPTION", comment: "Description of new profile feature for upgrading (existing) users"), + image:#imageLiteral(resourceName: "introductory_splash_profile")) + } + // Keep these ordered by increasing uniqueId. private var allExperienceUpgrades: [ExperienceUpgrade] { - var upgrades = [ExperienceUpgrade(uniqueId: ExperienceUpgradeId.videoCalling.rawValue, - title: NSLocalizedString("UPGRADE_EXPERIENCE_VIDEO_TITLE", comment: "Header for upgrade experience"), - body: NSLocalizedString("UPGRADE_EXPERIENCE_VIDEO_DESCRIPTION", comment: "Description of video calling to upgrading (existing) users"), - image: #imageLiteral(resourceName: "introductory_splash_video_calling"))] - - if UIDevice.current.supportsCallKit { - upgrades.append(ExperienceUpgrade(uniqueId: ExperienceUpgradeId.callKit.rawValue, - title: NSLocalizedString("UPGRADE_EXPERIENCE_CALLKIT_TITLE", comment: "Header for upgrade experience"), - body: NSLocalizedString("UPGRADE_EXPERIENCE_CALLKIT_DESCRIPTION", comment: "Description of CallKit to upgrading (existing) users"), - image: #imageLiteral(resourceName: "introductory_splash_callkit"))) - } - - return upgrades + return [ + // Disable old experience upgrades. Most people have seen them by now, and accomodating multiple makes layout harder. + // Note if we ever want to show multiple experience upgrades again + // we'll have to update the layout in ExperienceUpgradesPageViewController + // + // videoCalling, + // (UIDevice.current.supportsCallKit ? callKit : nil), + introducingProfiles + ].flatMap { $0 } } // MARK: - Instance Methods diff --git a/Signal/src/environment/ExperienceUpgrades/ExperienceUpgrade.swift b/Signal/src/environment/ExperienceUpgrades/ExperienceUpgrade.swift index 1cf694601d..699a4e2250 100644 --- a/Signal/src/environment/ExperienceUpgrades/ExperienceUpgrade.swift +++ b/Signal/src/environment/ExperienceUpgrades/ExperienceUpgrade.swift @@ -8,7 +8,6 @@ class ExperienceUpgrade: TSYapDatabaseObject { let title: String let body: String let image: UIImage? - var seenAt: Date? required init(uniqueId: String, title: String, body: String, image: UIImage) { self.title = title @@ -61,9 +60,4 @@ class ExperienceUpgrade: TSYapDatabaseObject { return super.storageBehaviorForProperty(withKey: propertyKey) } } - - func markAsSeen(transaction: YapDatabaseReadWriteTransaction) { - self.seenAt = Date() - super.save(with: transaction) - } } diff --git a/Signal/translations/en.lproj/Localizable.strings b/Signal/translations/en.lproj/Localizable.strings index 8cdef9d5bf..e6b7bf8316 100644 --- a/Signal/translations/en.lproj/Localizable.strings +++ b/Signal/translations/en.lproj/Localizable.strings @@ -1109,10 +1109,10 @@ "PROFILE_VIEW_PROFILE_AVATAR_FIELD" = "Avatar"; /* Description of the user profile. */ -"PROFILE_VIEW_PROFILE_DESCRIPTION" = "Your profile affects how you will appear to other users if you are not in their contacts."; +"PROFILE_VIEW_PROFILE_DESCRIPTION" = "Your Signal Profile will be visible to your contacts, when you initiate new conversations, and when you share it with other users and groups."; /* Link to more information about the user profile. */ -"PROFILE_VIEW_PROFILE_DESCRIPTION_LINK" = "Click here to learn more."; +"PROFILE_VIEW_PROFILE_DESCRIPTION_LINK" = "Tap here to learn more."; /* Label for the profile name field of the profile view. */ "PROFILE_VIEW_PROFILE_NAME_FIELD" = "Profile Name"; @@ -1516,6 +1516,15 @@ /* Header for upgrade experience */ "UPGRADE_EXPERIENCE_CALLKIT_TITLE" = "Just Swipe to Answer"; +/* button label shown one time, after user upgrades app */ +"UPGRADE_EXPERIENCE_INTRODUCING_PROFILES_BUTTON" = "Set up Your Profile"; + +/* Description of new profile feature for upgrading (existing) users */ +"UPGRADE_EXPERIENCE_INTRODUCING_PROFILES_DESCRIPTION" = "You can now share a profile photo and name with your friends on Signal."; + +/* Header for upgrade experience */ +"UPGRADE_EXPERIENCE_INTRODUCING_PROFILES_TITLE" = "Ready for Your Closeup?"; + /* Description of video calling to upgrading (existing) users */ "UPGRADE_EXPERIENCE_VIDEO_DESCRIPTION" = "Signal now supports secure video calling. Just start a call like normal, tap the camera button, and wave hello.";