Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
signalapp
GitHub Repository: signalapp/Signal-iOS
Path: blob/main/Signal/src/ViewControllers/AppSettings/Profile/ProfileNameViewController.swift
1 views
//
// Copyright 2021 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
//

import SignalServiceKit
import SignalUI

protocol ProfileNameViewControllerDelegate: AnyObject {
    func profileNameViewDidComplete(givenName: OWSUserProfile.NameComponent, familyName: OWSUserProfile.NameComponent?)
}

// MARK: -

class ProfileNameViewController: OWSTableViewController2 {
    private lazy var givenNameTextField = OWSTextField(
        placeholder: OWSLocalizedString(
            "PROFILE_VIEW_GIVEN_NAME_DEFAULT_TEXT",
            comment: "Default text for the given name field of the profile view.",
        ),
        returnKeyType: .next,
        spellCheckingType: .no,
        autocorrectionType: .no,
        delegate: self,
        editingChanged: { [weak self] in
            self?.textFieldDidChange()
        },
    )
    private lazy var familyNameTextField = OWSTextField(
        placeholder: OWSLocalizedString(
            "PROFILE_VIEW_FAMILY_NAME_DEFAULT_TEXT",
            comment: "Default text for the family name field of the profile view.",
        ),
        returnKeyType: .done,
        spellCheckingType: .no,
        autocorrectionType: .no,
        delegate: self,
        editingChanged: { [weak self] in
            self?.textFieldDidChange()
        },
    )

    private let originalGivenName: String?
    private let originalFamilyName: String?

    private weak var profileDelegate: ProfileNameViewControllerDelegate?

    init(
        givenName: String?,
        familyName: String?,
        profileDelegate: ProfileNameViewControllerDelegate,
    ) {
        self.originalGivenName = givenName
        self.originalFamilyName = familyName
        self.profileDelegate = profileDelegate

        super.init()
    }

    // MARK: -

    override func viewDidLoad() {
        super.viewDidLoad()

        shouldAvoidKeyboard = true
        tableView.keyboardDismissMode = .interactive

        givenNameTextField.text = originalGivenName
        familyNameTextField.text = originalFamilyName

        title = OWSLocalizedString("PROFILE_NAME_VIEW_TITLE", comment: "Title for the profile name view.")

        navigationItem.leftBarButtonItem = .cancelButton(
            dismissingFrom: self,
            hasUnsavedChanges: { [weak self] in self?.hasUnsavedChanges },
        )

        navigationItem.rightBarButtonItem = .setButton { [weak self] in
            self?.didTapDone()
        }

        updateNavigation()
        updateTableContents()
    }

    private func givenNameComponent() -> ProfileName.NameComponent? {
        givenNameTextField.text.flatMap(OWSUserProfile.NameComponent.parse(truncating:))?.nameComponent
    }

    private func familyNameComponent() -> ProfileName.NameComponent? {
        familyNameTextField.text.flatMap(OWSUserProfile.NameComponent.parse(truncating:))?.nameComponent
    }

    private var hasUnsavedChanges: Bool {
        givenNameComponent()?.stringValue.rawValue != originalGivenName
            || familyNameComponent()?.stringValue.rawValue != originalFamilyName
    }

    // Don't allow interactive dismiss when there are unsaved changes.
    override var isModalInPresentation: Bool {
        get { hasUnsavedChanges }
        set {}
    }

    private func updateNavigation() {
        navigationItem.rightBarButtonItem?.isEnabled = hasUnsavedChanges
    }

    override func viewWillAppear(_ animated: Bool) {
        super.viewWillAppear(animated)

        updateNavigation()

        firstTextField.becomeFirstResponder()
    }

    override func viewDidAppear(_ animated: Bool) {
        super.viewDidAppear(animated)

        updateNavigation()

        // TODO: First responder
    }

    private static let bioButtonHeight: CGFloat = 28

    func updateTableContents() {
        let contents = OWSTableContents()

        let givenNameTextField = self.givenNameTextField
        let familyNameTextField = self.familyNameTextField

        let namesSection = OWSTableSection()
        func addTextField(_ textField: UITextField) {
            namesSection.add(.textFieldItem(textField))
        }

        // For CJKV locales, display family name field first.
        if NSLocale.current.isCJKV {
            addTextField(familyNameTextField)
            addTextField(givenNameTextField)

            // Otherwise, display given name field first.
        } else {
            addTextField(givenNameTextField)
            addTextField(familyNameTextField)
        }

        contents.add(namesSection)

        self.contents = contents
    }

    private func didTapDone() {
        switch ProfileName.createNameFrom(
            givenName: givenNameTextField.text,
            familyName: familyNameTextField.text,
        ) {
        case .failure(.givenNameTooLong):
            OWSActionSheets.showErrorAlert(message: OWSLocalizedString(
                "PROFILE_VIEW_ERROR_GIVEN_NAME_TOO_LONG",
                comment: "Error message shown when user tries to update profile with a given name that is too long.",
            ))
        case .failure(.familyNameTooLong):
            OWSActionSheets.showErrorAlert(message: OWSLocalizedString(
                "PROFILE_VIEW_ERROR_FAMILY_NAME_TOO_LONG",
                comment: "Error message shown when user tries to update profile with a family name that is too long.",
            ))
        case let .success(profileName):
            guard let givenName = profileName.givenNameComponent else { fallthrough }
            profileDelegate?.profileNameViewDidComplete(
                givenName: givenName,
                familyName: profileName.familyNameComponent,
            )
            dismiss(animated: true)
        case .failure(.nameEmpty):
            OWSActionSheets.showErrorAlert(message: OWSLocalizedString(
                "PROFILE_VIEW_ERROR_GIVEN_NAME_REQUIRED",
                comment: "Error message shown when user tries to update profile without a given name",
            ))
        }
    }
}

// MARK: -

extension ProfileNameViewController: UITextFieldDelegate {

    private var firstTextField: UITextField {
        NSLocale.current.isCJKV ? familyNameTextField : givenNameTextField
    }

    private var secondTextField: UITextField {
        NSLocale.current.isCJKV ? givenNameTextField : familyNameTextField
    }

    func textFieldShouldReturn(_ textField: UITextField) -> Bool {
        if textField === firstTextField {
            secondTextField.becomeFirstResponder()
        } else {
            didTapDone()
        }
        return false
    }

    func textFieldDidChange() {
        updateNavigation()
    }

    func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool {
        TextFieldHelper.textField(
            textField,
            shouldChangeCharactersInRange: range,
            replacementString: string,
            maxByteCount: OWSUserProfile.Constants.maxNameLengthBytes,
            maxGlyphCount: OWSUserProfile.Constants.maxNameLengthGlyphs,
        )
    }
}