Signal-iOS/Signal/src/ViewControllers/ThreadSettings/NicknameEditorViewController.swift
2024-03-27 10:38:03 -06:00

242 lines
7.9 KiB
Swift

//
// Copyright 2024 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
//
import SignalUI
import SignalServiceKit
class NicknameEditorViewController: OWSTableViewController2 {
// MARK: Properties
static let maxNoteLengthGlyphs: Int = 240
static let maxNoteLengthBytes: Int = 2048
/// The Nickname that was already set when the view appeared.
private var initialNickname: ProfileName?
/// The note that was already set when the view appeared.
private var initialNote: String?
private let thread: TSContactThread
init(
thread: TSContactThread,
initialNickname: ProfileName? = nil,
initialNote: String? = nil
) {
// [Nicknames] TODO: Read the initial values here instead of having them passed in
self.thread = thread
self.initialNickname = initialNickname
self.initialNote = initialNote
super.init()
if let initialNickname {
self.givenNameTextField.text = initialNickname.givenName
self.familyNameTextField.text = initialNickname.familyName
}
self.noteTextView.text = initialNote
}
/// The given name entered into the text field.
private var givenName: String? {
self.givenNameTextField.text?.nilIfEmpty
}
/// The family name entered into the text field.
private var familyName: String? {
self.familyNameTextField.text?.nilIfEmpty
}
/// The note entered into the text view.
private var note: String? {
self.noteTextView.text?.nilIfEmpty
}
/// The Nickname, if valid, that is entered into the text fields.
private var enteredNickname: ProfileName? {
ProfileName(givenName: self.givenName, familyName: self.familyName)
}
/// Whether there are unsaved changes to the Nickname or note.
private var hasUnsavedChanges: Bool {
self.note != self.initialNote ||
// `enteredNickname` only has a value when the nickname is valid, so we can't just check against that.
self.givenName != self.initialNickname?.givenName ||
self.familyName != self.initialNickname?.familyName
}
/// Whether the entered nickname is in a state that can be saved, either valid or empty.
private var enteredNicknameCanBeSaved: Bool {
(familyName == nil && givenName == nil) || enteredNickname != nil
}
/// Whether both there are unsaved changes and those changes can be saved.
private var canSaveChanges: Bool {
hasUnsavedChanges && enteredNicknameCanBeSaved
}
// MARK: Views
private lazy var avatarView: ConversationAvatarView = {
let avatarView = ConversationAvatarView(
sizeClass: .eightyEight,
localUserDisplayMode: .asUser,
badged: false
)
avatarView.updateWithSneakyTransactionIfNecessary { config in
config.dataSource = .thread(self.thread)
}
return avatarView
}()
private lazy var avatarViewContainer: UIView = {
let container = UIView.container()
container.addSubview(avatarView)
avatarView.autoCenterInSuperview()
avatarView.autoPinWidthToSuperview(relation: .lessThanOrEqual)
avatarView.autoPinHeightToSuperview(withMargin: 24, relation: .lessThanOrEqual)
return container
}()
private lazy var givenNameTextField = self.createNameTextField(
placeholder: OWSLocalizedString(
"NICKNAME_EDITOR_GIVEN_NAME_PLACEHOLDER",
comment: "Placeholder text it the text field for the given name in the profile nickname editor."
)
)
private lazy var familyNameTextField = self.createNameTextField(
placeholder: OWSLocalizedString(
"NICKNAME_EDITOR_FAMILY_NAME_PLACEHOLDER",
comment: "Placeholder text it the text field for the family name in the profile nickname editor."
)
)
private lazy var noteTextView: TextViewWithPlaceholder = {
let textView = TextViewWithPlaceholder()
textView.placeholderText = OWSLocalizedString(
"NICKNAME_EDITOR_NOTE_PLACEHOLDER",
comment: "Placeholder text it the text box for the note in the profile nickname editor."
)
textView.delegate = self
return textView
}()
private func createNameTextField(placeholder: String) -> OWSTextField {
OWSTextField(
placeholder: placeholder,
spellCheckingType: .no,
autocorrectionType: .no,
clearButtonMode: .whileEditing,
delegate: self,
editingChanged: { [weak self] in
self?.editingChanged()
}
)
}
private func editingChanged() {
navigationItem.rightBarButtonItem?.isEnabled = canSaveChanges
}
// MARK: Lifecycle
override func viewDidLoad() {
self.shouldAvoidKeyboard = true
super.viewDidLoad()
self.updateTableContents()
self.title = OWSLocalizedString(
"NICKNAME_EDITOR_TITLE",
comment: "The title for the profile nickname editor view."
)
navigationItem.leftBarButtonItem = .cancelButton(
dismissingFrom: self,
hasUnsavedChanges: { [weak self] in self?.hasUnsavedChanges }
)
navigationItem.rightBarButtonItem = .doneButton { [weak self] in
self?.saveChanges()
}
editingChanged()
}
private func updateTableContents() {
let contents = OWSTableContents()
let titleSection = OWSTableSection(
title: nil, items: [],
footerTitle: OWSLocalizedString(
"NICKNAME_EDITOR_DESCRIPTION",
comment: "The description below the title on the profile nickname editor view."
)
)
contents.add(titleSection)
let namesSectionItems: [OWSTableItem]
let givenNameItem = OWSTableItem.textFieldItem(
self.givenNameTextField,
textColor: Theme.primaryTextColor
)
let familyNameItem = OWSTableItem.textFieldItem(
self.familyNameTextField,
textColor: Theme.primaryTextColor
)
if NSLocale.current.isCJKV {
namesSectionItems = [familyNameItem, givenNameItem]
} else {
namesSectionItems = [givenNameItem, familyNameItem]
}
let namesSection = OWSTableSection(
items: namesSectionItems,
headerView: self.avatarViewContainer
)
contents.add(namesSection)
contents.add(.init(items: [
self.textViewItem(self.noteTextView),
]))
self.contents = contents
}
// MARK: Handlers
private func saveChanges() {
// [Nicknames] TODO: Save nickname and note
self.dismiss(animated: true)
}
}
extension NicknameEditorViewController: UITextFieldDelegate {
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
)
}
}
// MARK: - TextViewWithPlaceholderDelegate
extension NicknameEditorViewController: TextViewWithPlaceholderDelegate {
func textViewDidUpdateText(_ textView: TextViewWithPlaceholder) {
self.editingChanged()
_textViewDidUpdateText(textView)
}
func textView(_ textView: TextViewWithPlaceholder, uiTextView: UITextView, shouldChangeTextIn range: NSRange, replacementText text: String) -> Bool {
TextViewHelper.textView(
textView,
shouldChangeTextIn: range,
replacementText: text,
maxByteCount: Self.maxNoteLengthBytes,
maxGlyphCount: Self.maxNoteLengthGlyphs
)
}
}