// // Copyright (c) 2020 Open Whisper Systems. All rights reserved. // #import "ProfileViewController.h" #import "AppDelegate.h" #import "AvatarViewHelper.h" #import "ConversationListViewController.h" #import "OWSNavigationController.h" #import "Signal-Swift.h" #import "UIFont+OWS.h" #import "UIView+OWS.h" #import #import #import #import #import #import #import #import @import SafariServices; NS_ASSUME_NONNULL_BEGIN NSString *const kProfileView_LastPresentedDate = @"kProfileView_LastPresentedDate"; @interface ProfileViewController () @property (nonatomic, readonly) AvatarViewHelper *avatarViewHelper; @property (nonatomic) UITextField *givenNameTextField; @property (nonatomic) UITextField *familyNameTextField; @property (nonatomic) UILabel *profileNamePreviewLabel; @property (nonatomic) UILabel *usernameLabel; @property (nonatomic) AvatarImageView *avatarView; @property (nonatomic) UIImageView *cameraImageView; @property (nonatomic) OWSFlatButton *saveButton; @property (nonatomic, nullable) NSData *avatarData; @property (nonatomic) BOOL hasUnsavedChanges; @property (nonatomic) ProfileViewMode profileViewMode; @property (nonatomic, readonly) void (^completionHandler)(ProfileViewController *); @end #pragma mark - @implementation ProfileViewController #pragma mark - Dependencies + (SDSDatabaseStorage *)databaseStorage { return SDSDatabaseStorage.shared; } + (SDSKeyValueStore *)keyValueStore { return [[SDSKeyValueStore alloc] initWithCollection:@"kProfileView_Collection"]; } - (id)reachabilityManager { return SSKEnvironment.shared.reachabilityManager; } #pragma mark - - (instancetype)initWithMode:(ProfileViewMode)profileViewMode completionHandler:(void (^)(ProfileViewController *))completionHandler { self = [super init]; if (!self) { return self; } _profileViewMode = profileViewMode; _completionHandler = completionHandler; DatabaseStorageAsyncWrite(SDSDatabaseStorage.shared, ^(SDSAnyWriteTransaction *transaction) { [ProfileViewController.keyValueStore setDate:[NSDate new] key:kProfileView_LastPresentedDate transaction:transaction]; }); return self; } - (void)loadView { [super loadView]; self.title = NSLocalizedString(@"PROFILE_VIEW_TITLE", @"Title for the profile view."); _avatarViewHelper = [AvatarViewHelper new]; _avatarViewHelper.delegate = self; _avatarData = [OWSProfileManager.sharedManager localProfileAvatarData]; [self createViews]; [self updateNavigationItem]; if (self.profileViewMode == ProfileViewMode_Registration) { // mark as dirty if re-registration has content if (self.familyNameTextField.text.length > 0 || self.givenNameTextField.text.length > 0 || self.avatarData != nil) { self.hasUnsavedChanges = YES; } } } - (void)createViews { self.view.backgroundColor = Theme.backgroundColor; UIStackView *stackView = [UIStackView new]; stackView.axis = UILayoutConstraintAxisVertical; stackView.spacing = 0; [self.view addSubview:stackView]; [stackView autoPinToTopLayoutGuideOfViewController:self withInset:15.f]; [stackView autoPinWidthToSuperview]; void (^addSeparator)(BOOL) = ^(BOOL withLeadingInset) { UIView *separatorWrapper = [UIView containerView]; [stackView addArrangedSubview:separatorWrapper]; UIView *separator = [UIView containerView]; separator.backgroundColor = Theme.cellSeparatorColor; [separatorWrapper addSubview:separator]; [separator autoPinHeightToSuperview]; [separator autoPinLeadingToSuperviewMarginWithInset:withLeadingInset ? 18 : 0]; [separator autoPinTrailingToSuperviewMargin]; [separator autoSetDimension:ALDimensionHeight toSize:CGHairlineWidth()]; }; CGFloat rowSpacing = 10; UIEdgeInsets rowMargins = UIEdgeInsetsMake(10, 18, 10, 18); // Avatar UIView *avatarRow = [UIView containerView]; [stackView addArrangedSubview:avatarRow]; self.avatarView = [AvatarImageView new]; self.avatarView.userInteractionEnabled = YES; self.avatarView.accessibilityIdentifier = ACCESSIBILITY_IDENTIFIER_WITH_NAME(self, @"avatarView"); [self.avatarView addGestureRecognizer:[[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(avatarViewTapped:)]]; [avatarRow addSubview:self.avatarView]; [self.avatarView autoHCenterInSuperview]; [self.avatarView autoPinEdgeToSuperviewEdge:ALEdgeTop]; [self.avatarView autoSetDimension:ALDimensionWidth toSize:self.avatarSize]; [self.avatarView autoSetDimension:ALDimensionHeight toSize:self.avatarSize]; self.cameraImageView = [UIImageView new]; [self.cameraImageView setTemplateImageName:@"camera-outline-24" tintColor:Theme.secondaryTextAndIconColor]; [self.cameraImageView autoSetDimensionsToSize:CGSizeMake(32, 32)]; self.cameraImageView.contentMode = UIViewContentModeCenter; self.cameraImageView.backgroundColor = Theme.backgroundColor; self.cameraImageView.layer.cornerRadius = 16; self.cameraImageView.layer.shadowColor = [(Theme.isDarkThemeEnabled ? Theme.darkThemeWashColor : Theme.primaryTextColor) CGColor]; self.cameraImageView.layer.shadowOffset = CGSizeMake(1, 1); self.cameraImageView.layer.shadowOpacity = 0.5; self.cameraImageView.layer.shadowRadius = 4; [avatarRow addSubview:self.cameraImageView]; [self.cameraImageView autoPinTrailingToEdgeOfView:self.avatarView]; [self.cameraImageView autoPinEdge:ALEdgeBottom toEdge:ALEdgeBottom ofView:self.avatarView]; self.profileNamePreviewLabel = [UILabel new]; self.profileNamePreviewLabel.textAlignment = NSTextAlignmentCenter; self.profileNamePreviewLabel.textColor = Theme.secondaryTextAndIconColor; self.profileNamePreviewLabel.font = UIFont.ows_dynamicTypeSubheadlineClampedFont; [avatarRow addSubview:self.profileNamePreviewLabel]; [self.profileNamePreviewLabel autoPinWidthToSuperviewWithMargin:16]; [self.profileNamePreviewLabel autoPinEdge:ALEdgeTop toEdge:ALEdgeBottom ofView:self.avatarView withOffset:16]; [self.profileNamePreviewLabel autoPinEdgeToSuperviewEdge:ALEdgeBottom withInset:28 relation:NSLayoutRelationGreaterThanOrEqual]; [self.profileNamePreviewLabel autoSetDimension:ALDimensionHeight toSize:16]; [self updateAvatarView]; addSeparator(NO); // Given Name void (^addGivenNameRow)(void) = ^{ UIStackView *givenNameRow = [UIStackView new]; givenNameRow.spacing = rowSpacing; givenNameRow.layoutMarginsRelativeArrangement = YES; givenNameRow.layoutMargins = rowMargins; [givenNameRow addGestureRecognizer:[[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(givenNameRowTapped:)]]; givenNameRow.accessibilityIdentifier = ACCESSIBILITY_IDENTIFIER_WITH_NAME(self, @"givenNameRow"); [stackView addArrangedSubview:givenNameRow]; UILabel *givenNameLabel = [UILabel new]; givenNameLabel.text = NSLocalizedString( @"PROFILE_VIEW_GIVEN_NAME_FIELD", @"Label for the given name field of the profile view."); givenNameLabel.textColor = Theme.primaryTextColor; givenNameLabel.font = [[UIFont ows_dynamicTypeBodyClampedFont] ows_semibold]; [givenNameRow addArrangedSubview:givenNameLabel]; UITextField *givenNameTextField; if (UIDevice.currentDevice.isIPhone5OrShorter) { givenNameTextField = [DismissableTextField new]; } else { givenNameTextField = [OWSTextField new]; } self.givenNameTextField = givenNameTextField; givenNameTextField.returnKeyType = UIReturnKeyNext; givenNameTextField.autocorrectionType = UITextAutocorrectionTypeNo; givenNameTextField.spellCheckingType = UITextSpellCheckingTypeNo; givenNameTextField.font = [UIFont ows_dynamicTypeBodyClampedFont]; givenNameTextField.textColor = Theme.primaryTextColor; givenNameTextField.placeholder = NSLocalizedString( @"PROFILE_VIEW_GIVEN_NAME_DEFAULT_TEXT", @"Default text for the given name field of the profile view."); givenNameTextField.delegate = self; givenNameTextField.text = OWSProfileManager.sharedManager.localGivenName; givenNameTextField.textAlignment = NSTextAlignmentRight; SET_SUBVIEW_ACCESSIBILITY_IDENTIFIER(self, givenNameTextField); [givenNameTextField addTarget:self action:@selector(textFieldDidChange:) forControlEvents:UIControlEventEditingChanged]; [givenNameRow addArrangedSubview:givenNameTextField]; }; // Family Name void (^addFamilyNameRow)(void) = ^{ UIStackView *familyNameRow = [UIStackView new]; familyNameRow.spacing = rowSpacing; familyNameRow.layoutMarginsRelativeArrangement = YES; familyNameRow.layoutMargins = rowMargins; [familyNameRow addGestureRecognizer:[[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(familyNameRowTapped:)]]; familyNameRow.accessibilityIdentifier = ACCESSIBILITY_IDENTIFIER_WITH_NAME(self, @"familyNameRow"); [stackView addArrangedSubview:familyNameRow]; UILabel *familyNameLabel = [UILabel new]; familyNameLabel.text = NSLocalizedString( @"PROFILE_VIEW_FAMILY_NAME_FIELD", @"Label for the family name field of the profile view."); familyNameLabel.textColor = Theme.primaryTextColor; familyNameLabel.font = [[UIFont ows_dynamicTypeBodyClampedFont] ows_semibold]; [familyNameRow addArrangedSubview:familyNameLabel]; UITextField *familyNameTextField; if (UIDevice.currentDevice.isIPhone5OrShorter) { familyNameTextField = [DismissableTextField new]; } else { familyNameTextField = [OWSTextField new]; } self.familyNameTextField = familyNameTextField; familyNameTextField.returnKeyType = UIReturnKeyDone; familyNameTextField.autocorrectionType = UITextAutocorrectionTypeNo; familyNameTextField.spellCheckingType = UITextSpellCheckingTypeNo; familyNameTextField.font = [UIFont ows_dynamicTypeBodyClampedFont]; familyNameTextField.textColor = Theme.primaryTextColor; familyNameTextField.placeholder = NSLocalizedString( @"PROFILE_VIEW_FAMILY_NAME_DEFAULT_TEXT", @"Default text for the family name field of the profile view."); familyNameTextField.delegate = self; familyNameTextField.text = OWSProfileManager.sharedManager.localFamilyName; familyNameTextField.textAlignment = NSTextAlignmentRight; SET_SUBVIEW_ACCESSIBILITY_IDENTIFIER(self, familyNameTextField); [familyNameTextField addTarget:self action:@selector(textFieldDidChange:) forControlEvents:UIControlEventEditingChanged]; [familyNameRow addArrangedSubview:familyNameTextField]; }; // For CJKV locales, display family name field first. if (NSLocale.currentLocale.isCJKV) { addFamilyNameRow(); addSeparator(YES); addGivenNameRow(); // Otherwise, display given name field first. } else { addGivenNameRow(); addSeparator(YES); addFamilyNameRow(); } [self updateProfileNamePreview]; // Username if (self.shouldShowUsernameRow) { addSeparator(YES); UIStackView *usernameRow = [UIStackView new]; usernameRow.spacing = rowSpacing; usernameRow.layoutMarginsRelativeArrangement = YES; usernameRow.layoutMargins = rowMargins; [usernameRow addGestureRecognizer:[[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(usernameRowTapped:)]]; usernameRow.accessibilityIdentifier = ACCESSIBILITY_IDENTIFIER_WITH_NAME(self, @"usernameRow"); [stackView addArrangedSubview:usernameRow]; UILabel *usernameTitleLabel = [UILabel new]; usernameTitleLabel.text = NSLocalizedString(@"PROFILE_VIEW_USERNAME_FIELD", @"Label for the username field of the profile view."); usernameTitleLabel.textColor = Theme.primaryTextColor; usernameTitleLabel.font = [[UIFont ows_dynamicTypeBodyClampedFont] ows_semibold]; [usernameRow addArrangedSubview:usernameTitleLabel]; UILabel *usernameLabel = [UILabel new]; usernameLabel.font = [UIFont ows_dynamicTypeBodyClampedFont]; usernameLabel.textAlignment = NSTextAlignmentRight; [usernameRow addArrangedSubview:usernameLabel]; _usernameLabel = usernameLabel; UIView *disclosureImageContainer = [UIView containerView]; [usernameRow addArrangedSubview:disclosureImageContainer]; NSString *disclosureImageName = CurrentAppContext().isRTL ? @"system_disclosure_indicator_rtl" : @"system_disclosure_indicator"; UIImageView *disclosureImageView = [UIImageView new]; [disclosureImageView setTemplateImageName:disclosureImageName tintColor:Theme.cellSeparatorColor]; [disclosureImageContainer addSubview:disclosureImageView]; [disclosureImageView autoPinWidthToSuperview]; [disclosureImageView autoVCenterInSuperview]; [disclosureImageView autoSetDimension:ALDimensionHeight toSize:13]; [disclosureImageView autoSetDimension:ALDimensionWidth toSize:11]; [self updateUsername]; addSeparator(NO); } else { addSeparator(NO); } // Information UIView *infoRow = [UIView new]; infoRow.layoutMargins = rowMargins; [infoRow addGestureRecognizer:[[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(infoRowTapped:)]]; infoRow.accessibilityIdentifier = ACCESSIBILITY_IDENTIFIER_WITH_NAME(self, @"infoRow"); [stackView addArrangedSubview:infoRow]; UILabel *infoLabel = [UILabel new]; infoLabel.textColor = Theme.secondaryTextAndIconColor; infoLabel.font = [UIFont ows_dynamicTypeCaption1ClampedFont]; NSMutableAttributedString *text = [NSMutableAttributedString new]; [text appendAttributedString:[[NSAttributedString alloc] initWithString:NSLocalizedString(@"PROFILE_VIEW_PROFILE_DESCRIPTION", @"Description of the user profile.") attributes:@{}]]; [text appendAttributedString:[[NSAttributedString alloc] initWithString:@" " attributes:@{}]]; [text appendAttributedString:[[NSAttributedString alloc] initWithString:CommonStrings.learnMore attributes:@{ NSUnderlineStyleAttributeName : @(NSUnderlineStyleNone), NSForegroundColorAttributeName : Theme.accentBlueColor, }]]; infoLabel.attributedText = text; infoLabel.numberOfLines = 0; infoLabel.lineBreakMode = NSLineBreakByWordWrapping; [infoRow addSubview:infoLabel]; [infoLabel autoPinEdgesToSuperviewMargins]; // Big Button switch (self.profileViewMode) { case ProfileViewMode_Registration: case ProfileViewMode_ExperienceUpgrade: { UIView *buttonRow = [UIView new]; buttonRow.layoutMargins = rowMargins; [stackView addArrangedSubview:buttonRow]; const CGFloat kButtonHeight = 47.f; OWSFlatButton *saveButton = [OWSFlatButton buttonWithTitle:NSLocalizedString(@"PROFILE_VIEW_SAVE_BUTTON", @"Button to save the profile view in the profile view.") font:[OWSFlatButton fontForHeight:kButtonHeight] titleColor:[UIColor whiteColor] backgroundColor:UIColor.ows_accentBlueColor target:self selector:@selector(saveButtonPressed)]; SET_SUBVIEW_ACCESSIBILITY_IDENTIFIER(self, saveButton); self.saveButton = saveButton; [buttonRow addSubview:saveButton]; [saveButton autoPinEdgesToSuperviewMargins]; [saveButton autoSetDimension:ALDimensionHeight toSize:47.f]; break; } case ProfileViewMode_AppSettings: break; } } - (void)viewWillAppear:(BOOL)animated { [super viewWillAppear:animated]; switch (self.profileViewMode) { case ProfileViewMode_AppSettings: break; case ProfileViewMode_Registration: case ProfileViewMode_ExperienceUpgrade: [self.givenNameTextField becomeFirstResponder]; break; } [self updateUsername]; } #pragma mark - Event Handling - (void)leaveViewCheckingForUnsavedChanges { [self.familyNameTextField resignFirstResponder]; [self.givenNameTextField resignFirstResponder]; if (!self.hasUnsavedChanges) { // If user made no changes, return to conversation settings view. [self profileCompleted]; return; } __weak ProfileViewController *weakSelf = self; [OWSActionSheets showPendingChangesActionSheetWithDiscardAction:^{ [weakSelf profileCompleted]; }]; } - (void)avatarTapped { [self.avatarViewHelper showChangeAvatarUI]; } - (void)setHasUnsavedChanges:(BOOL)hasUnsavedChanges { _hasUnsavedChanges = hasUnsavedChanges; [self updateNavigationItem]; } - (void)updateNavigationItem { // The navigation bar is hidden in the registration workflow. if (self.navigationController.navigationBarHidden) { [self.navigationController setNavigationBarHidden:NO animated:YES]; } BOOL forceSaveButtonEnabled = NO; switch (self.profileViewMode) { case ProfileViewMode_AppSettings: if (self.hasUnsavedChanges) { // If we have a unsaved changes, right item should be a "save" button. UIBarButtonItem *saveButton = [[UIBarButtonItem alloc] initWithBarButtonSystemItem:UIBarButtonSystemItemSave target:self action:@selector(updatePressed)]; SET_SUBVIEW_ACCESSIBILITY_IDENTIFIER(self, saveButton); self.navigationItem.rightBarButtonItem = saveButton; } else { self.navigationItem.rightBarButtonItem = nil; } break; case ProfileViewMode_Registration: self.navigationItem.hidesBackButton = YES; self.navigationItem.rightBarButtonItem = nil; // During registration, if you have a name pre-populatd we want // to enable the save button even if you haven't edited anything. if (self.givenNameTextField.text.length > 0) { forceSaveButtonEnabled = YES; } break; case ProfileViewMode_ExperienceUpgrade: self.navigationItem.rightBarButtonItem = nil; // During the experience upgrade, if you have a name we want // to enable the save button even if you haven't edited anything. if (self.givenNameTextField.text.length > 0) { forceSaveButtonEnabled = YES; } break; } if (self.hasUnsavedChanges || forceSaveButtonEnabled) { self.saveButton.enabled = YES; [self.saveButton setBackgroundColorsWithUpColor:UIColor.ows_accentBlueColor]; } else { self.saveButton.enabled = NO; [self.saveButton setBackgroundColorsWithUpColor:[UIColor.ows_accentBlueColor blendedWithColor:Theme.backgroundColor alpha:0.5f]]; } } - (void)updatePressed { [self updateProfile]; } - (void)updateProfile { __weak ProfileViewController *weakSelf = self; NSString *normalizedGivenName = [self normalizedGivenName]; NSString *normalizedFamilyName = [self normalizedFamilyName]; if (normalizedGivenName.length <= 0) { [OWSActionSheets showErrorAlertWithMessage: NSLocalizedString(@"PROFILE_VIEW_ERROR_GIVEN_NAME_REQUIRED", @"Error message shown when user tries to update profile without a given name")]; return; } if ([OWSProfileManager.sharedManager isProfileNameTooLong:normalizedGivenName]) { [OWSActionSheets showErrorAlertWithMessage:NSLocalizedString(@"PROFILE_VIEW_ERROR_GIVEN_NAME_TOO_LONG", @"Error message shown when user tries to update profile with a given name " @"that is too long.")]; return; } if ([OWSProfileManager.sharedManager isProfileNameTooLong:normalizedFamilyName]) { [OWSActionSheets showErrorAlertWithMessage:NSLocalizedString(@"PROFILE_VIEW_ERROR_FAMILY_NAME_TOO_LONG", @"Error message shown when user tries to update profile with a family name " @"that is too long.")]; return; } if (!self.reachabilityManager.isReachable) { [OWSActionSheets showErrorAlertWithMessage: NSLocalizedString(@"PROFILE_VIEW_NO_CONNECTION", @"Error shown when the user tries to update their profile when the app is not connected to the " @"internet.")]; return; } // Show an activity indicator to block the UI during the profile upload. [ModalActivityIndicatorViewController presentFromViewController:self canCancel:NO backgroundBlock:^(ModalActivityIndicatorViewController *modalActivityIndicator) { [OWSProfileManager updateLocalProfilePromiseObjWithProfileGivenName:normalizedGivenName profileFamilyName:normalizedFamilyName profileAvatarData:weakSelf.avatarData] .then(^{ [modalActivityIndicator dismissWithCompletion:^{ [weakSelf updateProfileCompleted]; }]; }) .catch(^(NSError *error) { OWSFailDebug(@"Error: %@", error); [modalActivityIndicator dismissWithCompletion:^{ // Don't show an error alert; the profile update // is enqueued and will be completed later. [weakSelf updateProfileCompleted]; }]; }); }]; } - (NSString *)normalizedGivenName { return [self.givenNameTextField.text ows_stripped]; } - (NSString *)normalizedFamilyName { return [self.familyNameTextField.text ows_stripped]; } - (void)updateProfileCompleted { OWSLogVerbose(@""); [self profileCompleted]; } - (void)profileCompleted { OWSLogVerbose(@""); self.completionHandler(self); } - (void)showConversationSplitView { OWSAssertIsOnMainThread(); OWSLogVerbose(@""); [SignalApp.sharedApp showConversationSplitView]; } #pragma mark - UITextFieldDelegate - (UITextField *)firstTextField { return NSLocale.currentLocale.isCJKV ? self.familyNameTextField : self.givenNameTextField; } - (UITextField *)secondTextField { return NSLocale.currentLocale.isCJKV ? self.givenNameTextField : self.familyNameTextField; } - (BOOL)textField:(UITextField *)textField shouldChangeCharactersInRange:(NSRange)editingRange replacementString:(NSString *)insertionText { // TODO: Possibly filter invalid input. return [TextFieldHelper textField:textField shouldChangeCharactersInRange:editingRange replacementString:insertionText.withoutBidiControlCharacters byteLimit:OWSUserProfile.kNameDataLength]; } - (BOOL)textFieldShouldReturn:(UITextField *)textField { if (textField == self.firstTextField) { [self.secondTextField becomeFirstResponder]; } else { [textField resignFirstResponder]; } return NO; } - (void)textFieldDidChange:(id)sender { self.hasUnsavedChanges = YES; [self updateProfileNamePreview]; // TODO: Update length warning. } #pragma mark - Avatar - (void)setAvatarImage:(nullable UIImage *)avatarImage { OWSAssertIsOnMainThread(); NSData *_Nullable avatarData = nil; if (avatarImage != nil) { avatarData = [OWSProfileManager avatarDataForAvatarImage:avatarImage]; } self.hasUnsavedChanges = ![NSObject isNullableObject:avatarData equalTo:_avatarData]; _avatarData = avatarData; [self updateAvatarView]; } - (NSUInteger)avatarSize { return 96; } - (void)updateAvatarView { if (self.avatarData != nil) { self.avatarView.image = [UIImage imageWithData:self.avatarData]; } else { self.avatarView.image = [[[OWSContactAvatarBuilder alloc] initForLocalUserWithDiameter:self.avatarSize] buildDefaultImage]; } } - (void)updateProfileNamePreview { NSPersonNameComponents *components = [NSPersonNameComponents new]; components.givenName = [self normalizedGivenName]; components.familyName = [self normalizedFamilyName]; self.profileNamePreviewLabel.text = [NSPersonNameComponentsFormatter localizedStringFromPersonNameComponents:components style:0 options:0]; } - (void)updateUsername { NSString *_Nullable username = [OWSProfileManager.sharedManager localUsername]; if (username) { self.usernameLabel.text = [CommonFormats formatUsername:username]; self.usernameLabel.textColor = Theme.primaryTextColor; } else { self.usernameLabel.text = NSLocalizedString(@"PROFILE_VIEW_CREATE_USERNAME", @"A string indicating that the user can create a username on the profile view."); self.usernameLabel.textColor = Theme.accentBlueColor; } } - (void)givenNameRowTapped:(UIGestureRecognizer *)sender { if (sender.state == UIGestureRecognizerStateRecognized) { [self.givenNameTextField becomeFirstResponder]; } } - (void)familyNameRowTapped:(UIGestureRecognizer *)sender { if (sender.state == UIGestureRecognizerStateRecognized) { [self.familyNameTextField becomeFirstResponder]; } } - (void)usernameRowTapped:(UIGestureRecognizer *)sender { UsernameViewController *usernameVC = [UsernameViewController new]; if (self.profileViewMode == ProfileViewMode_Registration) { usernameVC.modalPresentation = YES; [self presentFormSheetViewController:[[OWSNavigationController alloc] initWithRootViewController:usernameVC] animated:YES completion:nil]; } else { [self.navigationController pushViewController:usernameVC animated:YES]; } } - (BOOL)shouldShowUsernameRow { switch (self.profileViewMode) { case ProfileViewMode_ExperienceUpgrade: case ProfileViewMode_Registration: return false; case ProfileViewMode_AppSettings: return RemoteConfig.usernames; } } - (void)avatarViewTapped:(UIGestureRecognizer *)sender { if (sender.state == UIGestureRecognizerStateRecognized) { [self avatarTapped]; } } - (void)infoRowTapped:(UIGestureRecognizer *)sender { if (sender.state == UIGestureRecognizerStateRecognized) { SFSafariViewController *safariVC = [[SFSafariViewController alloc] initWithURL:[NSURL URLWithString:@"https://support.signal.org/hc/articles/360007459591"]]; [self presentViewController:safariVC animated:YES completion:nil]; } } - (void)saveButtonPressed { [self updatePressed]; } #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].localGivenName.length > 0) { return NO; } NSTimeInterval kProfileNagFrequency = kDayInterval * 30; __block NSDate *_Nullable lastPresentedDate; [self.databaseStorage readWithBlock:^(SDSAnyReadTransaction *transaction) { lastPresentedDate = [ProfileViewController.keyValueStore getDate:kProfileView_LastPresentedDate transaction:transaction]; }]; return (!lastPresentedDate || fabs([lastPresentedDate timeIntervalSinceNow]) > kProfileNagFrequency); } #pragma mark - AvatarViewHelperDelegate - (nullable NSString *)avatarActionSheetTitle { return NSLocalizedString( @"PROFILE_VIEW_AVATAR_ACTIONSHEET_TITLE", @"Action Sheet title prompting the user for a profile avatar"); } - (void)avatarDidChange:(UIImage *)image { OWSAssertIsOnMainThread(); OWSAssertDebug(image); [self setAvatarImage:[image resizedImageToFillPixelSize:CGSizeMake(kOWSProfileManager_MaxAvatarDiameter, kOWSProfileManager_MaxAvatarDiameter)]]; } - (UIViewController *)fromViewController { return self; } - (BOOL)hasClearAvatarAction { return self.avatarData != nil; } - (NSString *)clearAvatarActionLabel { return NSLocalizedString(@"PROFILE_VIEW_CLEAR_AVATAR", @"Label for action that clear's the user's profile avatar"); } - (void)clearAvatar { [self setAvatarImage:nil]; } #pragma mark - OWSNavigationView - (BOOL)shouldCancelNavigationBack { BOOL result = self.hasUnsavedChanges; if (result) { [self leaveViewCheckingForUnsavedChanges]; } return result; } #pragma mark - Orientation - (UIInterfaceOrientationMask)supportedInterfaceOrientations { if (UIDevice.currentDevice.isIPad) { return UIInterfaceOrientationMaskAll; } return (self.profileViewMode == ProfileViewMode_Registration ? UIInterfaceOrientationMaskPortrait : UIInterfaceOrientationMaskAllButUpsideDown); } @end NS_ASSUME_NONNULL_END