From afcacbb55c958cdff16a52c91408004c6e06a7a6 Mon Sep 17 00:00:00 2001 From: Matthew Chen Date: Fri, 15 Feb 2019 16:46:06 -0500 Subject: [PATCH 1/3] Sketch out the 'onboarding profile' view. --- Signal.xcodeproj/project.pbxproj | 4 + Signal/src/Signal-Bridging-Header.h | 1 + Signal/src/ViewControllers/AvatarViewHelper.h | 4 +- .../HomeView/HomeViewController.m | 13 + .../ViewControllers/NewGroupViewController.m | 2 +- .../ViewControllers/ProfileViewController.m | 2 +- .../Registration/OnboardingController.swift | 18 + .../OnboardingPhoneNumberViewController.swift | 22 +- .../OnboardingProfileViewController.swift | 412 ++++++++++++++++++ ...OnboardingVerificationViewController.swift | 10 +- .../UpdateGroupViewController.m | 2 +- .../translations/en.lproj/Localizable.strings | 9 + 12 files changed, 468 insertions(+), 31 deletions(-) create mode 100644 Signal/src/ViewControllers/Registration/OnboardingProfileViewController.swift diff --git a/Signal.xcodeproj/project.pbxproj b/Signal.xcodeproj/project.pbxproj index 0b34e517f..6939b7a95 100644 --- a/Signal.xcodeproj/project.pbxproj +++ b/Signal.xcodeproj/project.pbxproj @@ -166,6 +166,7 @@ 3496957421A301A100DCFE74 /* OWSBackupAPI.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3496956B21A301A100DCFE74 /* OWSBackupAPI.swift */; }; 349EA07C2162AEA800F7B17F /* OWS111UDAttributesMigration.swift in Sources */ = {isa = PBXBuildFile; fileRef = 349EA07B2162AEA700F7B17F /* OWS111UDAttributesMigration.swift */; }; 34A4C61E221613D00042EF2E /* OnboardingVerificationViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 34A4C61D221613D00042EF2E /* OnboardingVerificationViewController.swift */; }; + 34A4C62022175C5C0042EF2E /* OnboardingProfileViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 34A4C61F22175C5C0042EF2E /* OnboardingProfileViewController.swift */; }; 34A55F3720485465002CC6DE /* OWS2FARegistrationViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 34A55F3520485464002CC6DE /* OWS2FARegistrationViewController.m */; }; 34A6C28021E503E700B5B12E /* OWSImagePickerController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 34A6C27F21E503E600B5B12E /* OWSImagePickerController.swift */; }; 34A8B3512190A40E00218A25 /* MediaAlbumCellView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 34A8B3502190A40E00218A25 /* MediaAlbumCellView.swift */; }; @@ -848,6 +849,7 @@ 3496956D21A301A100DCFE74 /* OWSBackupIO.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = OWSBackupIO.h; sourceTree = ""; }; 349EA07B2162AEA700F7B17F /* OWS111UDAttributesMigration.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OWS111UDAttributesMigration.swift; sourceTree = ""; }; 34A4C61D221613D00042EF2E /* OnboardingVerificationViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OnboardingVerificationViewController.swift; sourceTree = ""; }; + 34A4C61F22175C5C0042EF2E /* OnboardingProfileViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OnboardingProfileViewController.swift; sourceTree = ""; }; 34A55F3520485464002CC6DE /* OWS2FARegistrationViewController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = OWS2FARegistrationViewController.m; sourceTree = ""; }; 34A55F3620485464002CC6DE /* OWS2FARegistrationViewController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = OWS2FARegistrationViewController.h; sourceTree = ""; }; 34A6C27F21E503E600B5B12E /* OWSImagePickerController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OWSImagePickerController.swift; sourceTree = ""; }; @@ -1474,6 +1476,7 @@ 3448E15D221333F5004B052E /* OnboardingController.swift */, 3448E15B22133274004B052E /* OnboardingPermissionsViewController.swift */, 3448E16322135FFA004B052E /* OnboardingPhoneNumberViewController.swift */, + 34A4C61F22175C5C0042EF2E /* OnboardingProfileViewController.swift */, 3448E15F22134C88004B052E /* OnboardingSplashViewController.swift */, 34A4C61D221613D00042EF2E /* OnboardingVerificationViewController.swift */, 346E9D5321B040B600562252 /* RegistrationController.swift */, @@ -3491,6 +3494,7 @@ 343A65981FC4CFE7000477A1 /* ConversationScrollButton.m in Sources */, 34386A51207D0C01009F5D9C /* HomeViewController.m in Sources */, 34D1F0A91F867BFC0066283D /* ConversationViewCell.m in Sources */, + 34A4C62022175C5C0042EF2E /* OnboardingProfileViewController.swift in Sources */, 4505C2BF1E648EA300CEBF41 /* ExperienceUpgrade.swift in Sources */, EF764C351DB67CC5000D9A87 /* UIViewController+Permissions.m in Sources */, 45CD81EF1DC030E7004C9430 /* SyncPushTokensJob.swift in Sources */, diff --git a/Signal/src/Signal-Bridging-Header.h b/Signal/src/Signal-Bridging-Header.h index a8f7d2804..12e45a336 100644 --- a/Signal/src/Signal-Bridging-Header.h +++ b/Signal/src/Signal-Bridging-Header.h @@ -8,6 +8,7 @@ // Separate iOS Frameworks from other imports. #import "AppSettingsViewController.h" #import "AttachmentUploadView.h" +#import "AvatarViewHelper.h" #import "ContactCellView.h" #import "ContactTableViewCell.h" #import "ConversationViewCell.h" diff --git a/Signal/src/ViewControllers/AvatarViewHelper.h b/Signal/src/ViewControllers/AvatarViewHelper.h index 01d6b71f8..82ecc282d 100644 --- a/Signal/src/ViewControllers/AvatarViewHelper.h +++ b/Signal/src/ViewControllers/AvatarViewHelper.h @@ -1,5 +1,5 @@ // -// Copyright (c) 2017 Open Whisper Systems. All rights reserved. +// Copyright (c) 2019 Open Whisper Systems. All rights reserved. // #import @@ -13,7 +13,7 @@ NS_ASSUME_NONNULL_BEGIN @protocol AvatarViewHelperDelegate -- (NSString *)avatarActionSheetTitle; +- (nullable NSString *)avatarActionSheetTitle; - (void)avatarDidChange:(UIImage *)image; diff --git a/Signal/src/ViewControllers/HomeView/HomeViewController.m b/Signal/src/ViewControllers/HomeView/HomeViewController.m index 2e8ab8ba1..a0b62b970 100644 --- a/Signal/src/ViewControllers/HomeView/HomeViewController.m +++ b/Signal/src/ViewControllers/HomeView/HomeViewController.m @@ -482,6 +482,19 @@ NSString *const kArchivedConversationsReuseIdentifier = @"kArchivedConversations [self.searchResultsController viewDidAppear:animated]; self.hasEverAppeared = YES; + + dispatch_async(dispatch_get_main_queue(), ^{ + OnboardingController *onboardingController = [OnboardingController new]; + [onboardingController + updateWithPhoneNumber:[[OnboardingPhoneNumber alloc] initWithE164:@"+13213214321" userInput:@"3213214321"]]; + + // UIViewController *view = [onboardingController initialViewController]; + UIViewController *view = + [[OnboardingProfileViewController alloc] initWithOnboardingController:onboardingController]; + OWSNavigationController *navigationController = + [[OWSNavigationController alloc] initWithRootViewController:view]; + [self presentViewController:navigationController animated:YES completion:nil]; + }); } - (void)viewDidDisappear:(BOOL)animated diff --git a/Signal/src/ViewControllers/NewGroupViewController.m b/Signal/src/ViewControllers/NewGroupViewController.m index 258fd631a..562d97c37 100644 --- a/Signal/src/ViewControllers/NewGroupViewController.m +++ b/Signal/src/ViewControllers/NewGroupViewController.m @@ -612,7 +612,7 @@ NS_ASSUME_NONNULL_BEGIN #pragma mark - AvatarViewHelperDelegate -- (NSString *)avatarActionSheetTitle +- (nullable NSString *)avatarActionSheetTitle { return NSLocalizedString( @"NEW_GROUP_ADD_PHOTO_ACTION", @"Action Sheet title prompting the user for a group avatar"); diff --git a/Signal/src/ViewControllers/ProfileViewController.m b/Signal/src/ViewControllers/ProfileViewController.m index 90d65feb1..2ce609506 100644 --- a/Signal/src/ViewControllers/ProfileViewController.m +++ b/Signal/src/ViewControllers/ProfileViewController.m @@ -573,7 +573,7 @@ NSString *const kProfileView_LastPresentedDate = @"kProfileView_LastPresentedDat #pragma mark - AvatarViewHelperDelegate -- (NSString *)avatarActionSheetTitle +- (nullable NSString *)avatarActionSheetTitle { return NSLocalizedString( @"PROFILE_VIEW_AVATAR_ACTIONSHEET_TITLE", @"Action Sheet title prompting the user for a profile avatar"); diff --git a/Signal/src/ViewControllers/Registration/OnboardingController.swift b/Signal/src/ViewControllers/Registration/OnboardingController.swift index a457b56fb..c8b91a6df 100644 --- a/Signal/src/ViewControllers/Registration/OnboardingController.swift +++ b/Signal/src/ViewControllers/Registration/OnboardingController.swift @@ -450,3 +450,21 @@ public class OnboardingController: NSObject { } } } + +// MARK: - + +public extension UIView { + public func addBottomStroke() -> UIView { + return addBottomStroke(color: Theme.middleGrayColor, strokeWidth: CGHairlineWidth()) + } + + public func addBottomStroke(color: UIColor, strokeWidth: CGFloat) -> UIView { + let strokeView = UIView() + strokeView.backgroundColor = color + addSubview(strokeView) + strokeView.autoSetDimension(.height, toSize: strokeWidth) + strokeView.autoPinWidthToSuperview() + strokeView.autoPinEdge(toSuperviewEdge: .bottom) + return strokeView + } +} diff --git a/Signal/src/ViewControllers/Registration/OnboardingPhoneNumberViewController.swift b/Signal/src/ViewControllers/Registration/OnboardingPhoneNumberViewController.swift index 388843b2b..ef0e7c791 100644 --- a/Signal/src/ViewControllers/Registration/OnboardingPhoneNumberViewController.swift +++ b/Signal/src/ViewControllers/Registration/OnboardingPhoneNumberViewController.swift @@ -62,7 +62,7 @@ public class OnboardingPhoneNumberViewController: OnboardingBaseViewController { countryRow.isUserInteractionEnabled = true countryRow.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(countryRowTapped))) countryRow.autoSetDimension(.height, toSize: rowHeight) - _ = addBottomStroke(countryRow) + _ = countryRow.addBottomStroke() callingCodeLabel.textColor = Theme.primaryColor callingCodeLabel.font = UIFont.ows_dynamicTypeBodyClamped @@ -70,7 +70,7 @@ public class OnboardingPhoneNumberViewController: OnboardingBaseViewController { callingCodeLabel.setCompressionResistanceHorizontalHigh() callingCodeLabel.isUserInteractionEnabled = true callingCodeLabel.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(countryCodeTapped))) - _ = addBottomStroke(callingCodeLabel) + _ = callingCodeLabel.addBottomStroke() callingCodeLabel.autoSetDimension(.width, toSize: rowHeight, relation: .greaterThanOrEqual) phoneNumberTextField.textAlignment = .left @@ -81,8 +81,8 @@ public class OnboardingPhoneNumberViewController: OnboardingBaseViewController { phoneNumberTextField.setContentHuggingHorizontalLow() phoneNumberTextField.setCompressionResistanceHorizontalLow() - phoneStrokeNormal = addBottomStroke(phoneNumberTextField) - phoneStrokeError = addBottomStroke(phoneNumberTextField, color: .ows_destructiveRed, strokeWidth: 2) + phoneStrokeNormal = phoneNumberTextField.addBottomStroke() + phoneStrokeError = phoneNumberTextField.addBottomStroke(color: .ows_destructiveRed, strokeWidth: 2) let phoneNumberRow = UIStackView(arrangedSubviews: [ callingCodeLabel, @@ -140,20 +140,6 @@ public class OnboardingPhoneNumberViewController: OnboardingBaseViewController { validationWarningLabel.autoPinEdge(.leading, to: .leading, of: phoneNumberTextField) } - private func addBottomStroke(_ view: UIView) -> UIView { - return addBottomStroke(view, color: Theme.middleGrayColor, strokeWidth: CGHairlineWidth()) - } - - private func addBottomStroke(_ view: UIView, color: UIColor, strokeWidth: CGFloat) -> UIView { - let strokeView = UIView() - strokeView.backgroundColor = color - view.addSubview(strokeView) - strokeView.autoSetDimension(.height, toSize: strokeWidth) - strokeView.autoPinWidthToSuperview() - strokeView.autoPinEdge(toSuperviewEdge: .bottom) - return strokeView - } - // MARK: - View Lifecycle public override func viewWillAppear(_ animated: Bool) { diff --git a/Signal/src/ViewControllers/Registration/OnboardingProfileViewController.swift b/Signal/src/ViewControllers/Registration/OnboardingProfileViewController.swift new file mode 100644 index 000000000..7d00e4d48 --- /dev/null +++ b/Signal/src/ViewControllers/Registration/OnboardingProfileViewController.swift @@ -0,0 +1,412 @@ +// +// Copyright (c) 2019 Open Whisper Systems. All rights reserved. +// + +import UIKit + +@objc +public class OnboardingProfileViewController: OnboardingBaseViewController { + +// private var titleLabel: UILabel? +// private let phoneNumberTextField = UITextField() +// private let onboardingCodeView = OnboardingCodeView() +// private var codeStateLink: OWSFlatButton? +// private let errorLabel = UILabel() + private let avatarView = AvatarImageView() + private let nameTextfield = UITextField() + private var avatar: UIImage? + private let cameraCircle = UIView.container() + + private let avatarViewHelper = AvatarViewHelper() + + override public func loadView() { + super.loadView() + + avatarViewHelper.delegate = self + + view.backgroundColor = Theme.backgroundColor + view.layoutMargins = .zero + + let titleLabel = self.titleLabel(text: NSLocalizedString("ONBOARDING_PROFILE_TITLE", comment: "Title of the 'onboarding profile' view.")) + + let explanationLabel = self.explanationLabel(explanationText: NSLocalizedString("ONBOARDING_PROFILE_EXPLANATION", + comment: "Explanation in the 'onboarding profile' view.")) + + let nextButton = self.button(title: NSLocalizedString("BUTTON_NEXT", + comment: "Label for the 'next' button."), + selector: #selector(nextPressed)) + + avatarView.autoSetDimensions(to: CGSize(width: CGFloat(avatarSize), height: CGFloat(avatarSize))) + + let cameraImageView = UIImageView() + cameraImageView.image = UIImage(named: "settings-avatar-camera") + cameraCircle.backgroundColor = Theme.backgroundColor + cameraCircle.addSubview(cameraImageView) + let cameraCircleDiameter: CGFloat = 40 + cameraCircle.autoSetDimensions(to: CGSize(width: cameraCircleDiameter, height: cameraCircleDiameter)) + cameraCircle.layer.shadowColor = UIColor(white: 0, alpha: 0.15).cgColor + cameraCircle.layer.shadowRadius = 5 + cameraCircle.layer.shadowOffset = CGSize(width: 1, height: 1) + cameraCircle.layer.shadowOpacity = 1 + cameraCircle.layer.cornerRadius = cameraCircleDiameter * 0.5 + cameraCircle.clipsToBounds = false + cameraImageView.autoCenterInSuperview() + + let avatarWrapper = UIView.container() + avatarWrapper.isUserInteractionEnabled = true + avatarWrapper.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(avatarTapped))) + avatarWrapper.addSubview(avatarView) + avatarView.autoPinEdgesToSuperviewEdges(with: UIEdgeInsets(top: 4, leading: 4, bottom: 4, trailing: 4)) + avatarWrapper.addSubview(cameraCircle) + cameraCircle.autoPinEdge(toSuperviewEdge: .trailing) + cameraCircle.autoPinEdge(toSuperviewEdge: .bottom) + + nameTextfield.textAlignment = .left + nameTextfield.delegate = self + nameTextfield.returnKeyType = .done + nameTextfield.textColor = Theme.primaryColor +// nameTextfield.tintColor = UIColor.ows_materialBlue + nameTextfield.font = UIFont.ows_dynamicTypeBodyClamped + nameTextfield.placeholder = NSLocalizedString("ONBOARDING_PROFILE_NAME_PLACEHOLDER", + comment: "Placeholder text for the profile name in the 'onboarding profile' view.") + nameTextfield.setContentHuggingHorizontalLow() + nameTextfield.setCompressionResistanceHorizontalLow() + + let nameWrapper = UIView.container() + nameWrapper.setCompressionResistanceHorizontalLow() + nameWrapper.setContentHuggingHorizontalLow() + nameWrapper.addSubview(nameTextfield) + nameTextfield.autoPinWidthToSuperview() + nameTextfield.autoPinEdge(toSuperviewEdge: .top, withInset: 8) + nameTextfield.autoPinEdge(toSuperviewEdge: .bottom, withInset: 8) + _ = nameWrapper.addBottomStroke() + + let profileRow = UIStackView(arrangedSubviews: [ + avatarWrapper, + nameWrapper + ]) + profileRow.axis = .horizontal + profileRow.alignment = .center + profileRow.spacing = 8 + + let topSpacer = UIView.vStretchingSpacer() + let bottomSpacer = UIView.vStretchingSpacer() + + let stackView = UIStackView(arrangedSubviews: [ + titleLabel, + topSpacer, + profileRow, + UIView.spacer(withHeight: 25), + explanationLabel, + UIView.spacer(withHeight: 20), + nextButton, + bottomSpacer + ]) + stackView.axis = .vertical + stackView.alignment = .fill + stackView.layoutMargins = UIEdgeInsets(top: 32, left: 32, bottom: 32, right: 32) + stackView.isLayoutMarginsRelativeArrangement = true + view.addSubview(stackView) + stackView.autoPinWidthToSuperview() + stackView.autoPin(toTopLayoutGuideOf: self, withInset: 0) + autoPinView(toBottomOfViewControllerOrKeyboard: stackView, avoidNotch: true) + + // Ensure whitespace is balanced, so inputs are vertically centered. + topSpacer.autoMatch(.height, to: .height, of: bottomSpacer) + + updateAvatarView() + } + + private let avatarSize: UInt = 80 + + private func updateAvatarView() { + if let avatar = avatar { + avatarView.image = avatar + cameraCircle.isHidden = true + return + } + + let defaultAvatar = OWSContactAvatarBuilder(forLocalUserWithDiameter: avatarSize).buildDefaultImage() + avatarView.image = defaultAvatar + cameraCircle.isHidden = false + } + +// // MARK: - Code State +// +// private let countdownDuration: TimeInterval = 60 +// private var codeCountdownTimer: Timer? +// private var codeCountdownStart: NSDate? +// +// deinit { +// if let codeCountdownTimer = codeCountdownTimer { +// codeCountdownTimer.invalidate() +// } +// } +// +// private func startCodeCountdown() { +// codeCountdownStart = NSDate() +// codeCountdownTimer = Timer.weakScheduledTimer(withTimeInterval: 1, target: self, selector: #selector(codeCountdownTimerFired), userInfo: nil, repeats: true) +// } +// +// @objc +// public func codeCountdownTimerFired() { +// guard let codeCountdownStart = codeCountdownStart else { +// owsFailDebug("Missing codeCountdownStart.") +// return +// } +// guard let codeCountdownTimer = codeCountdownTimer else { +// owsFailDebug("Missing codeCountdownTimer.") +// return +// } +// +// let countdownInterval = abs(codeCountdownStart.timeIntervalSinceNow) +// +// guard countdownInterval < countdownDuration else { +// // Countdown complete. +// codeCountdownTimer.invalidate() +// self.codeCountdownTimer = nil +// +// if codeState != .pending { +// owsFailDebug("Unexpected codeState: \(codeState)") +// } +// codeState = .possiblyNotDelivered +// updateCodeState() +// return +// } +// +// // Update the "code state" UI to reflect the countdown. +// updateCodeState() +// } +// +// private func updateCodeState() { +// AssertIsOnMainThread() +// +// guard let codeCountdownStart = codeCountdownStart else { +// owsFailDebug("Missing codeCountdownStart.") +// return +// } +// guard let titleLabel = titleLabel else { +// owsFailDebug("Missing titleLabel.") +// return +// } +// guard let codeStateLink = codeStateLink else { +// owsFailDebug("Missing codeStateLink.") +// return +// } +// +// var e164PhoneNumber = "" +// if let phoneNumber = onboardingController.phoneNumber { +// e164PhoneNumber = phoneNumber.e164 +// } +// let formattedPhoneNumber = PhoneNumber.bestEffortLocalizedPhoneNumber(withE164: e164PhoneNumber) +// +// // Update titleLabel +// switch codeState { +// case .pending, .possiblyNotDelivered: +// titleLabel.text = String(format: NSLocalizedString("ONBOARDING_VERIFICATION_TITLE_DEFAULT_FORMAT", +// comment: "Format for the title of the 'onboarding verification' view. Embeds {{the user's phone number}}."), +// formattedPhoneNumber) +// case .resent: +// titleLabel.text = String(format: NSLocalizedString("ONBOARDING_VERIFICATION_TITLE_RESENT_FORMAT", +// comment: "Format for the title of the 'onboarding verification' view after the verification code has been resent. Embeds {{the user's phone number}}."), +// formattedPhoneNumber) +// } +// +// // Update codeStateLink +// switch codeState { +// case .pending: +// let countdownInterval = abs(codeCountdownStart.timeIntervalSinceNow) +// let countdownRemaining = max(0, countdownDuration - countdownInterval) +// let formattedCountdown = OWSFormat.formatDurationSeconds(Int(round(countdownRemaining))) +// let text = String(format: NSLocalizedString("ONBOARDING_VERIFICATION_CODE_COUNTDOWN_FORMAT", +// comment: "Format for the label of the 'pending code' label of the 'onboarding verification' view. Embeds {{the time until the code can be resent}}."), +// formattedCountdown) +// codeStateLink.setTitle(title: text, font: .ows_dynamicTypeBodyClamped, titleColor: Theme.secondaryColor) +//// codeStateLink.setBackgroundColors(upColor: Theme.backgroundColor) +// case .possiblyNotDelivered: +// codeStateLink.setTitle(title: NSLocalizedString("ONBOARDING_VERIFICATION_ORIGINAL_CODE_MISSING_LINK", +// comment: "Label for link that can be used when the original code did not arrive."), +// font: .ows_dynamicTypeBodyClamped, +// titleColor: .ows_materialBlue) +// case .resent: +// codeStateLink.setTitle(title: NSLocalizedString("ONBOARDING_VERIFICATION_RESENT_CODE_MISSING_LINK", +// comment: "Label for link that can be used when the resent code did not arrive."), +// font: .ows_dynamicTypeBodyClamped, +// titleColor: .ows_materialBlue) +// } +// } + + public override func viewDidAppear(_ animated: Bool) { + super.viewDidAppear(animated) + + _ = nameTextfield.becomeFirstResponder() + } + + // MARK: - Events + + @objc func avatarTapped(sender: UIGestureRecognizer) { + guard sender.state == .recognized else { + return + } + showAvatarActionSheet() + } + + @objc func nextPressed() { + Logger.info("") + + // TODO: +// parseAndTryToRegister() + } + + private func showAvatarActionSheet() { + AssertIsOnMainThread() + + Logger.info("") + + avatarViewHelper.showChangeAvatarUI() + +// let alert = UIAlertController(title: NSLocalizedString("CHECK_FOR_BACKUP_FAILED_TITLE", +// comment: "Title for alert shown when the app failed to check for an existing backup."), +// message: NSLocalizedString("CHECK_FOR_BACKUP_FAILED_MESSAGE", +// comment: "Message for alert shown when the app failed to check for an existing backup."), +// preferredStyle: .alert) +// alert.addAction(UIAlertAction(title: NSLocalizedString("REGISTER_FAILED_TRY_AGAIN", comment: ""), +// style: .default) { (_) in +// self.checkCanImportBackup(fromView: view) +// }) +// alert.addAction(UIAlertAction(title: NSLocalizedString("CHECK_FOR_BACKUP_DO_NOT_RESTORE", comment: "The label for the 'do not restore backup' button."), +// style: .destructive) { (_) in +// self.showProfileView(fromView: view) +// }) +// view.present(alert, animated: true) + } + + // @objc func backLinkTapped() { +// Logger.info("") +// +// self.navigationController?.popViewController(animated: true) +// } +// +// @objc func resendCodeLinkTapped() { +// Logger.info("") +// +// switch codeState { +// case .pending: +// // Ignore taps until the countdown expires. +// break +// case .possiblyNotDelivered, .resent: +// showResendActionSheet() +// } +// } +// +// private func showResendActionSheet() { +// Logger.info("") +// +// let actionSheet = UIAlertController(title: NSLocalizedString("ONBOARDING_VERIFICATION_RESEND_CODE_ALERT_TITLE", +// comment: "Title for the 'resend code' alert in the 'onboarding verification' view."), +// message: NSLocalizedString("ONBOARDING_VERIFICATION_RESEND_CODE_ALERT_MESSAGE", +// comment: "Message for the 'resend code' alert in the 'onboarding verification' view."), +// preferredStyle: .actionSheet) +// +// actionSheet.addAction(UIAlertAction(title: NSLocalizedString("ONBOARDING_VERIFICATION_RESEND_CODE_BY_SMS_BUTTON", +// comment: "Label for the 'resend code by SMS' button in the 'onboarding verification' view."), +// style: .default) { _ in +// self.onboardingController.tryToRegister(fromViewController: self, smsVerification: true) +// }) +// actionSheet.addAction(UIAlertAction(title: NSLocalizedString("ONBOARDING_VERIFICATION_RESEND_CODE_BY_VOICE_BUTTON", +// comment: "Label for the 'resend code by voice' button in the 'onboarding verification' view."), +// style: .default) { _ in +// self.onboardingController.tryToRegister(fromViewController: self, smsVerification: false) +// }) +// actionSheet.addAction(OWSAlerts.cancelAction) +// +// self.present(actionSheet, animated: true) +// } +// +// private func tryToVerify() { +// Logger.info("") +// +// guard onboardingCodeView.isComplete else { +// return +// } +// +// setHasInvalidCode(false) +// +// onboardingController.tryToVerify(fromViewController: self, verificationCode: onboardingCodeView.verificationCode, pin: nil, isInvalidCodeCallback: { +// self.setHasInvalidCode(true) +// }) +// } +// +// private func setHasInvalidCode(_ value: Bool) { +// onboardingCodeView.setHasError(value) +// errorLabel.isHidden = !value +// } +//} +// +//// MARK: - +// +//extension OnboardingProfileViewController: OnboardingCodeViewDelegate { +// public func codeViewDidChange() { +// AssertIsOnMainThread() +// +// setHasInvalidCode(false) +// +// tryToVerify() +// } +} + +// MARK: - + +extension OnboardingProfileViewController: UITextFieldDelegate { + public func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool { + // // TODO: Fix auto-format of phone numbers. + // ViewControllerUtils.phoneNumber(textField, shouldChangeCharactersIn: range, replacementString: string, countryCode: countryCode) + // + // isPhoneNumberInvalid = false + // updateValidationWarnings() + + // Inform our caller that we took care of performing the change. + return true + } + + public func textFieldShouldReturn(_ textField: UITextField) -> Bool { + // parseAndTryToRegister() + return false + } +} + +// MARK: - + +extension OnboardingProfileViewController: AvatarViewHelperDelegate { + public func avatarActionSheetTitle() -> String? { + return nil + } + + public func avatarDidChange(_ image: UIImage) { + AssertIsOnMainThread() + + let maxDiameter = CGFloat(kOWSProfileManager_MaxAvatarDiameter) + avatar = image.resizedImage(toFillPixelSize: CGSize(width: maxDiameter, + height: maxDiameter)) + + updateAvatarView() + } + + public func fromViewController() -> UIViewController { + return self + } + + public func hasClearAvatarAction() -> Bool { + return avatar != nil + } + + public func clearAvatar() { + avatar = nil + + updateAvatarView() + } + + public func clearAvatarActionLabel() -> String { + return NSLocalizedString("PROFILE_VIEW_CLEAR_AVATAR", comment: "Label for action that clear's the user's profile avatar") + } +} diff --git a/Signal/src/ViewControllers/Registration/OnboardingVerificationViewController.swift b/Signal/src/ViewControllers/Registration/OnboardingVerificationViewController.swift index a19f2227e..48cb07bba 100644 --- a/Signal/src/ViewControllers/Registration/OnboardingVerificationViewController.swift +++ b/Signal/src/ViewControllers/Registration/OnboardingVerificationViewController.swift @@ -133,14 +133,8 @@ private class OnboardingCodeView: UIView { digitView.addSubview(digitLabel) digitLabel.autoCenterInSuperview() - let strokeView = UIView.container() - if hasStroke { - strokeView.backgroundColor = Theme.primaryColor - digitView.addSubview(strokeView) - strokeView.autoPinWidthToSuperview() - strokeView.autoPinEdge(toSuperviewEdge: .bottom) - strokeView.autoSetDimension(.height, toSize: 1) - } + let strokeColor = (hasStroke ? Theme.primaryColor : UIColor.clear) + let strokeView = digitView.addBottomStroke(color: strokeColor, strokeWidth: 1) let vMargin: CGFloat = 4 let cellHeight: CGFloat = digitLabel.font.lineHeight + vMargin * 2 diff --git a/Signal/src/ViewControllers/ThreadSettings/UpdateGroupViewController.m b/Signal/src/ViewControllers/ThreadSettings/UpdateGroupViewController.m index 1820ad17c..1baa19bec 100644 --- a/Signal/src/ViewControllers/ThreadSettings/UpdateGroupViewController.m +++ b/Signal/src/ViewControllers/ThreadSettings/UpdateGroupViewController.m @@ -485,7 +485,7 @@ NS_ASSUME_NONNULL_BEGIN #pragma mark - AvatarViewHelperDelegate -- (NSString *)avatarActionSheetTitle +- (nullable NSString *)avatarActionSheetTitle { return NSLocalizedString( @"NEW_GROUP_ADD_PHOTO_ACTION", @"Action Sheet title prompting the user for a group avatar"); diff --git a/Signal/translations/en.lproj/Localizable.strings b/Signal/translations/en.lproj/Localizable.strings index 5bd79ce6d..f61ad1a2c 100644 --- a/Signal/translations/en.lproj/Localizable.strings +++ b/Signal/translations/en.lproj/Localizable.strings @@ -1529,6 +1529,15 @@ /* Label indicating that the phone number is invalid in the 'onboarding phone number' view. */ "ONBOARDING_PHONE_NUMBER_VALIDATION_WARNING" = "Invalid number"; +/* Explanation in the 'onboarding profile' view. */ +"ONBOARDING_PROFILE_EXPLANATION" = "Signal profiles are end-to-end encrypted and the Signal service never has access to this information."; + +/* Placeholder text for the profile name in the 'onboarding profile' view. */ +"ONBOARDING_PROFILE_NAME_PLACEHOLDER" = "Your Name"; + +/* Title of the 'onboarding profile' view. */ +"ONBOARDING_PROFILE_TITLE" = "Set up your profile"; + /* Link to the 'terms and privacy policy' in the 'onboarding splash' view. */ "ONBOARDING_SPLASH_TERM_AND_PRIVACY_POLICY" = "Terms & Privacy Policy"; From ab3b79cfe537eda35302f9a68abdbbe18d41cea3 Mon Sep 17 00:00:00 2001 From: Matthew Chen Date: Fri, 15 Feb 2019 17:20:16 -0500 Subject: [PATCH 2/3] Sketch out the 'onboarding profile' view. --- .../Registration/OnboardingController.swift | 18 ++ .../OnboardingProfileViewController.swift | 264 ++++-------------- 2 files changed, 70 insertions(+), 212 deletions(-) diff --git a/Signal/src/ViewControllers/Registration/OnboardingController.swift b/Signal/src/ViewControllers/Registration/OnboardingController.swift index c8b91a6df..4ec897d27 100644 --- a/Signal/src/ViewControllers/Registration/OnboardingController.swift +++ b/Signal/src/ViewControllers/Registration/OnboardingController.swift @@ -247,6 +247,24 @@ public class OnboardingController: NSObject { // navigationController.pushViewController(view, animated: true) } + @objc + public func profileWasSkipped(fromView view: UIViewController) { + AssertIsOnMainThread() + + Logger.info("") + + // TODO: + } + + @objc + public func profileDidComplete(fromView view: UIViewController) { + AssertIsOnMainThread() + + Logger.info("") + + // TODO: + } + // MARK: - State public private(set) var countryState: OnboardingCountryState = .defaultValue diff --git a/Signal/src/ViewControllers/Registration/OnboardingProfileViewController.swift b/Signal/src/ViewControllers/Registration/OnboardingProfileViewController.swift index 7d00e4d48..a8c3f9023 100644 --- a/Signal/src/ViewControllers/Registration/OnboardingProfileViewController.swift +++ b/Signal/src/ViewControllers/Registration/OnboardingProfileViewController.swift @@ -7,11 +7,14 @@ import UIKit @objc public class OnboardingProfileViewController: OnboardingBaseViewController { -// private var titleLabel: UILabel? -// private let phoneNumberTextField = UITextField() -// private let onboardingCodeView = OnboardingCodeView() -// private var codeStateLink: OWSFlatButton? -// private let errorLabel = UILabel() + // MARK: - Dependencies + + var profileManager: OWSProfileManager { + return OWSProfileManager.shared() + } + + // MARK: - + private let avatarView = AvatarImageView() private let nameTextfield = UITextField() private var avatar: UIImage? @@ -65,7 +68,6 @@ public class OnboardingProfileViewController: OnboardingBaseViewController { nameTextfield.delegate = self nameTextfield.returnKeyType = .done nameTextfield.textColor = Theme.primaryColor -// nameTextfield.tintColor = UIColor.ows_materialBlue nameTextfield.font = UIFont.ows_dynamicTypeBodyClamped nameTextfield.placeholder = NSLocalizedString("ONBOARDING_PROFILE_NAME_PLACEHOLDER", comment: "Placeholder text for the profile name in the 'onboarding profile' view.") @@ -131,110 +133,48 @@ public class OnboardingProfileViewController: OnboardingBaseViewController { cameraCircle.isHidden = false } -// // MARK: - Code State -// -// private let countdownDuration: TimeInterval = 60 -// private var codeCountdownTimer: Timer? -// private var codeCountdownStart: NSDate? -// -// deinit { -// if let codeCountdownTimer = codeCountdownTimer { -// codeCountdownTimer.invalidate() -// } -// } -// -// private func startCodeCountdown() { -// codeCountdownStart = NSDate() -// codeCountdownTimer = Timer.weakScheduledTimer(withTimeInterval: 1, target: self, selector: #selector(codeCountdownTimerFired), userInfo: nil, repeats: true) -// } -// -// @objc -// public func codeCountdownTimerFired() { -// guard let codeCountdownStart = codeCountdownStart else { -// owsFailDebug("Missing codeCountdownStart.") -// return -// } -// guard let codeCountdownTimer = codeCountdownTimer else { -// owsFailDebug("Missing codeCountdownTimer.") -// return -// } -// -// let countdownInterval = abs(codeCountdownStart.timeIntervalSinceNow) -// -// guard countdownInterval < countdownDuration else { -// // Countdown complete. -// codeCountdownTimer.invalidate() -// self.codeCountdownTimer = nil -// -// if codeState != .pending { -// owsFailDebug("Unexpected codeState: \(codeState)") -// } -// codeState = .possiblyNotDelivered -// updateCodeState() -// return -// } -// -// // Update the "code state" UI to reflect the countdown. -// updateCodeState() -// } -// -// private func updateCodeState() { -// AssertIsOnMainThread() -// -// guard let codeCountdownStart = codeCountdownStart else { -// owsFailDebug("Missing codeCountdownStart.") -// return -// } -// guard let titleLabel = titleLabel else { -// owsFailDebug("Missing titleLabel.") -// return -// } -// guard let codeStateLink = codeStateLink else { -// owsFailDebug("Missing codeStateLink.") -// return -// } -// -// var e164PhoneNumber = "" -// if let phoneNumber = onboardingController.phoneNumber { -// e164PhoneNumber = phoneNumber.e164 -// } -// let formattedPhoneNumber = PhoneNumber.bestEffortLocalizedPhoneNumber(withE164: e164PhoneNumber) -// -// // Update titleLabel -// switch codeState { -// case .pending, .possiblyNotDelivered: -// titleLabel.text = String(format: NSLocalizedString("ONBOARDING_VERIFICATION_TITLE_DEFAULT_FORMAT", -// comment: "Format for the title of the 'onboarding verification' view. Embeds {{the user's phone number}}."), -// formattedPhoneNumber) -// case .resent: -// titleLabel.text = String(format: NSLocalizedString("ONBOARDING_VERIFICATION_TITLE_RESENT_FORMAT", -// comment: "Format for the title of the 'onboarding verification' view after the verification code has been resent. Embeds {{the user's phone number}}."), -// formattedPhoneNumber) -// } -// -// // Update codeStateLink -// switch codeState { -// case .pending: -// let countdownInterval = abs(codeCountdownStart.timeIntervalSinceNow) -// let countdownRemaining = max(0, countdownDuration - countdownInterval) -// let formattedCountdown = OWSFormat.formatDurationSeconds(Int(round(countdownRemaining))) -// let text = String(format: NSLocalizedString("ONBOARDING_VERIFICATION_CODE_COUNTDOWN_FORMAT", -// comment: "Format for the label of the 'pending code' label of the 'onboarding verification' view. Embeds {{the time until the code can be resent}}."), -// formattedCountdown) -// codeStateLink.setTitle(title: text, font: .ows_dynamicTypeBodyClamped, titleColor: Theme.secondaryColor) -//// codeStateLink.setBackgroundColors(upColor: Theme.backgroundColor) -// case .possiblyNotDelivered: -// codeStateLink.setTitle(title: NSLocalizedString("ONBOARDING_VERIFICATION_ORIGINAL_CODE_MISSING_LINK", -// comment: "Label for link that can be used when the original code did not arrive."), -// font: .ows_dynamicTypeBodyClamped, -// titleColor: .ows_materialBlue) -// case .resent: -// codeStateLink.setTitle(title: NSLocalizedString("ONBOARDING_VERIFICATION_RESENT_CODE_MISSING_LINK", -// comment: "Label for link that can be used when the resent code did not arrive."), -// font: .ows_dynamicTypeBodyClamped, -// titleColor: .ows_materialBlue) -// } -// } + // MARK: - + + private func normalizedProfileName() -> String? { + return nameTextfield.text?.ows_stripped() + } + + private func tryToComplete() { + + let profileName = self.normalizedProfileName() + let profileAvatar = self.avatar + + if profileName == nil, profileAvatar == nil { + onboardingController.profileWasSkipped(fromView: self) + return + } + + if let name = profileName, + profileManager.isProfileNameTooLong(name) { + OWSAlerts.showErrorAlert(message: NSLocalizedString("PROFILE_VIEW_ERROR_PROFILE_NAME_TOO_LONG", + comment: "Error message shown when user tries to update profile with a profile name that is too long.")) + return + } + + ModalActivityIndicatorViewController.present(fromViewController: self, + canCancel: true) { (modal) in + + self.profileManager.updateLocalProfileName(profileName, avatarImage: profileAvatar, success: { + DispatchQueue.main.async { + modal.dismiss(completion: { + self.onboardingController.profileDidComplete(fromView: self) + }) + } + }, failure: { + DispatchQueue.main.async { + modal.dismiss(completion: { + OWSAlerts.showErrorAlert(message: NSLocalizedString("PROFILE_VIEW_ERROR_UPDATE_FAILED", + comment: "Error message shown when a profile update fails.")) + }) + } + }) + } + } public override func viewDidAppear(_ animated: Bool) { super.viewDidAppear(animated) @@ -254,8 +194,7 @@ public class OnboardingProfileViewController: OnboardingBaseViewController { @objc func nextPressed() { Logger.info("") - // TODO: -// parseAndTryToRegister() + tryToComplete() } private func showAvatarActionSheet() { @@ -264,113 +203,14 @@ public class OnboardingProfileViewController: OnboardingBaseViewController { Logger.info("") avatarViewHelper.showChangeAvatarUI() - -// let alert = UIAlertController(title: NSLocalizedString("CHECK_FOR_BACKUP_FAILED_TITLE", -// comment: "Title for alert shown when the app failed to check for an existing backup."), -// message: NSLocalizedString("CHECK_FOR_BACKUP_FAILED_MESSAGE", -// comment: "Message for alert shown when the app failed to check for an existing backup."), -// preferredStyle: .alert) -// alert.addAction(UIAlertAction(title: NSLocalizedString("REGISTER_FAILED_TRY_AGAIN", comment: ""), -// style: .default) { (_) in -// self.checkCanImportBackup(fromView: view) -// }) -// alert.addAction(UIAlertAction(title: NSLocalizedString("CHECK_FOR_BACKUP_DO_NOT_RESTORE", comment: "The label for the 'do not restore backup' button."), -// style: .destructive) { (_) in -// self.showProfileView(fromView: view) -// }) -// view.present(alert, animated: true) } - - // @objc func backLinkTapped() { -// Logger.info("") -// -// self.navigationController?.popViewController(animated: true) -// } -// -// @objc func resendCodeLinkTapped() { -// Logger.info("") -// -// switch codeState { -// case .pending: -// // Ignore taps until the countdown expires. -// break -// case .possiblyNotDelivered, .resent: -// showResendActionSheet() -// } -// } -// -// private func showResendActionSheet() { -// Logger.info("") -// -// let actionSheet = UIAlertController(title: NSLocalizedString("ONBOARDING_VERIFICATION_RESEND_CODE_ALERT_TITLE", -// comment: "Title for the 'resend code' alert in the 'onboarding verification' view."), -// message: NSLocalizedString("ONBOARDING_VERIFICATION_RESEND_CODE_ALERT_MESSAGE", -// comment: "Message for the 'resend code' alert in the 'onboarding verification' view."), -// preferredStyle: .actionSheet) -// -// actionSheet.addAction(UIAlertAction(title: NSLocalizedString("ONBOARDING_VERIFICATION_RESEND_CODE_BY_SMS_BUTTON", -// comment: "Label for the 'resend code by SMS' button in the 'onboarding verification' view."), -// style: .default) { _ in -// self.onboardingController.tryToRegister(fromViewController: self, smsVerification: true) -// }) -// actionSheet.addAction(UIAlertAction(title: NSLocalizedString("ONBOARDING_VERIFICATION_RESEND_CODE_BY_VOICE_BUTTON", -// comment: "Label for the 'resend code by voice' button in the 'onboarding verification' view."), -// style: .default) { _ in -// self.onboardingController.tryToRegister(fromViewController: self, smsVerification: false) -// }) -// actionSheet.addAction(OWSAlerts.cancelAction) -// -// self.present(actionSheet, animated: true) -// } -// -// private func tryToVerify() { -// Logger.info("") -// -// guard onboardingCodeView.isComplete else { -// return -// } -// -// setHasInvalidCode(false) -// -// onboardingController.tryToVerify(fromViewController: self, verificationCode: onboardingCodeView.verificationCode, pin: nil, isInvalidCodeCallback: { -// self.setHasInvalidCode(true) -// }) -// } -// -// private func setHasInvalidCode(_ value: Bool) { -// onboardingCodeView.setHasError(value) -// errorLabel.isHidden = !value -// } -//} -// -//// MARK: - -// -//extension OnboardingProfileViewController: OnboardingCodeViewDelegate { -// public func codeViewDidChange() { -// AssertIsOnMainThread() -// -// setHasInvalidCode(false) -// -// tryToVerify() -// } } // MARK: - extension OnboardingProfileViewController: UITextFieldDelegate { - public func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool { - // // TODO: Fix auto-format of phone numbers. - // ViewControllerUtils.phoneNumber(textField, shouldChangeCharactersIn: range, replacementString: string, countryCode: countryCode) - // - // isPhoneNumberInvalid = false - // updateValidationWarnings() - - // Inform our caller that we took care of performing the change. - return true - } - public func textFieldShouldReturn(_ textField: UITextField) -> Bool { - // parseAndTryToRegister() + tryToComplete() return false } } From 3ac77e5b01cf8c9c30894759ac5ee14b834496e6 Mon Sep 17 00:00:00 2001 From: Matthew Chen Date: Fri, 15 Feb 2019 17:21:23 -0500 Subject: [PATCH 3/3] Sketch out the 'onboarding profile' view. --- .../ViewControllers/HomeView/HomeViewController.m | 13 ------------- 1 file changed, 13 deletions(-) diff --git a/Signal/src/ViewControllers/HomeView/HomeViewController.m b/Signal/src/ViewControllers/HomeView/HomeViewController.m index a0b62b970..2e8ab8ba1 100644 --- a/Signal/src/ViewControllers/HomeView/HomeViewController.m +++ b/Signal/src/ViewControllers/HomeView/HomeViewController.m @@ -482,19 +482,6 @@ NSString *const kArchivedConversationsReuseIdentifier = @"kArchivedConversations [self.searchResultsController viewDidAppear:animated]; self.hasEverAppeared = YES; - - dispatch_async(dispatch_get_main_queue(), ^{ - OnboardingController *onboardingController = [OnboardingController new]; - [onboardingController - updateWithPhoneNumber:[[OnboardingPhoneNumber alloc] initWithE164:@"+13213214321" userInput:@"3213214321"]]; - - // UIViewController *view = [onboardingController initialViewController]; - UIViewController *view = - [[OnboardingProfileViewController alloc] initWithOnboardingController:onboardingController]; - OWSNavigationController *navigationController = - [[OWSNavigationController alloc] initWithRootViewController:view]; - [self presentViewController:navigationController animated:YES completion:nil]; - }); } - (void)viewDidDisappear:(BOOL)animated