session-ios/Session/View Controllers/SettingsVC.swift

425 lines
21 KiB
Swift
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

final class SettingsVC : BaseVC, AvatarViewHelperDelegate {
private var profilePictureToBeUploaded: UIImage?
private var displayNameToBeUploaded: String?
private var isEditingDisplayName = false { didSet { handleIsEditingDisplayNameChanged() } }
// MARK: Components
private lazy var profilePictureView: ProfilePictureView = {
let result = ProfilePictureView()
let size = Values.largeProfilePictureSize
result.size = size
result.set(.width, to: size)
result.set(.height, to: size)
return result
}()
private lazy var profilePictureUtilities: AvatarViewHelper = {
let result = AvatarViewHelper()
result.delegate = self
return result
}()
private lazy var displayNameLabel: UILabel = {
let result = UILabel()
result.textColor = Colors.text
result.font = .boldSystemFont(ofSize: Values.veryLargeFontSize)
result.lineBreakMode = .byTruncatingTail
result.textAlignment = .center
return result
}()
private lazy var displayNameTextField: TextField = {
let result = TextField(placeholder: NSLocalizedString("vc_settings_display_name_text_field_hint", comment: ""), usesDefaultHeight: false)
result.textAlignment = .center
return result
}()
private lazy var copyButton: Button = {
let result = Button(style: .prominentOutline, size: .medium)
result.setTitle(NSLocalizedString("copy", comment: ""), for: UIControl.State.normal)
result.addTarget(self, action: #selector(copyPublicKey), for: UIControl.Event.touchUpInside)
return result
}()
private lazy var settingButtonsStackView: UIStackView = {
let result = UIStackView()
result.axis = .vertical
result.alignment = .fill
return result
}()
// MARK: Lifecycle
override func viewDidLoad() {
super.viewDidLoad()
setUpGradientBackground()
setUpNavBarStyle()
setNavBarTitle(NSLocalizedString("vc_settings_title", comment: ""))
// Set up navigation bar buttons
let backButton = UIBarButtonItem(title: "Back", style: .plain, target: nil, action: nil)
backButton.tintColor = Colors.text
navigationItem.backBarButtonItem = backButton
updateNavigationBarButtons()
// Set up profile picture view
let profilePictureTapGestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(showEditProfilePictureUI))
profilePictureView.addGestureRecognizer(profilePictureTapGestureRecognizer)
profilePictureView.hexEncodedPublicKey = getUserHexEncodedPublicKey()
profilePictureView.update()
// Set up display name label
displayNameLabel.text = OWSProfileManager.shared().profileNameForRecipient(withID: getUserHexEncodedPublicKey())
// Set up display name container
let displayNameContainer = UIView()
displayNameContainer.addSubview(displayNameLabel)
displayNameLabel.pin(to: displayNameContainer)
displayNameContainer.addSubview(displayNameTextField)
displayNameTextField.pin(to: displayNameContainer)
displayNameContainer.set(.height, to: 40)
displayNameTextField.alpha = 0
let displayNameContainerTapGestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(showEditDisplayNameUI))
displayNameContainer.addGestureRecognizer(displayNameContainerTapGestureRecognizer)
// Set up header view
let headerStackView = UIStackView(arrangedSubviews: [ profilePictureView, displayNameContainer ])
headerStackView.axis = .vertical
headerStackView.spacing = Values.smallSpacing
headerStackView.alignment = .center
// Set up separator
let separator = Separator(title: NSLocalizedString("your_session_id", comment: ""))
// Set up public key label
let publicKeyLabel = UILabel()
publicKeyLabel.textColor = Colors.text
publicKeyLabel.font = Fonts.spaceMono(ofSize: isIPhone5OrSmaller ? Values.mediumFontSize : Values.largeFontSize)
publicKeyLabel.numberOfLines = 0
publicKeyLabel.textAlignment = .center
publicKeyLabel.lineBreakMode = .byCharWrapping
publicKeyLabel.text = getUserHexEncodedPublicKey()
// Set up share button
let shareButton = Button(style: .regular, size: .medium)
shareButton.setTitle(NSLocalizedString("share", comment: ""), for: UIControl.State.normal)
shareButton.addTarget(self, action: #selector(sharePublicKey), for: UIControl.Event.touchUpInside)
// Set up button container
let buttonContainer = UIStackView(arrangedSubviews: [ copyButton, shareButton ])
buttonContainer.axis = .horizontal
buttonContainer.spacing = Values.mediumSpacing
buttonContainer.distribution = .fillEqually
// Set up top stack view
let topStackView = UIStackView(arrangedSubviews: [ headerStackView, separator, publicKeyLabel, buttonContainer ])
topStackView.axis = .vertical
topStackView.spacing = Values.largeSpacing
topStackView.alignment = .fill
topStackView.layoutMargins = UIEdgeInsets(top: 0, left: Values.largeSpacing, bottom: 0, right: Values.largeSpacing)
topStackView.isLayoutMarginsRelativeArrangement = true
// Set up setting buttons stack view
getSettingButtons().forEach { settingButtonOrSeparator in
settingButtonsStackView.addArrangedSubview(settingButtonOrSeparator)
}
// Set up version label
let versionLabel = UILabel()
versionLabel.textColor = Colors.text.withAlphaComponent(Values.unimportantElementOpacity)
versionLabel.font = .systemFont(ofSize: Values.verySmallFontSize)
versionLabel.numberOfLines = 0
versionLabel.textAlignment = .center
versionLabel.lineBreakMode = .byCharWrapping
let version = Bundle.main.infoDictionary!["CFBundleShortVersionString"]!
let buildNumber = Bundle.main.infoDictionary!["CFBundleVersion"]!
versionLabel.text = "Version \(version) (\(buildNumber))"
// Set up stack view
let stackView = UIStackView(arrangedSubviews: [ topStackView, settingButtonsStackView, versionLabel ])
stackView.axis = .vertical
stackView.spacing = Values.largeSpacing
stackView.alignment = .fill
stackView.layoutMargins = UIEdgeInsets(top: Values.mediumSpacing, left: 0, bottom: Values.mediumSpacing, right: 0)
stackView.isLayoutMarginsRelativeArrangement = true
stackView.set(.width, to: UIScreen.main.bounds.width)
// Set up scroll view
let scrollView = UIScrollView()
scrollView.showsVerticalScrollIndicator = false
scrollView.addSubview(stackView)
stackView.pin(to: scrollView)
view.addSubview(scrollView)
scrollView.pin(to: view)
}
private func getSettingButtons() -> [UIView] {
func getSeparator() -> UIView {
let result = UIView()
result.backgroundColor = Colors.separator
result.set(.height, to: Values.separatorThickness)
return result
}
func getSettingButton(withTitle title: String, color: UIColor, action selector: Selector) -> UIButton {
let button = UIButton()
button.setTitle(title, for: UIControl.State.normal)
button.setTitleColor(color, for: UIControl.State.normal)
button.titleLabel!.font = .boldSystemFont(ofSize: Values.mediumFontSize)
button.titleLabel!.textAlignment = .center
func getImage(withColor color: UIColor) -> UIImage {
let rect = CGRect(origin: CGPoint.zero, size: CGSize(width: 1, height: 1))
UIGraphicsBeginImageContext(rect.size)
let context = UIGraphicsGetCurrentContext()!
context.setFillColor(color.cgColor)
context.fill(rect)
let image = UIGraphicsGetImageFromCurrentImageContext()
UIGraphicsEndImageContext()
return image!
}
let backgroundColor = isLightMode ? UIColor(hex: 0xFCFCFC) : UIColor(hex: 0x1B1B1B)
button.setBackgroundImage(getImage(withColor: backgroundColor), for: UIControl.State.normal)
let selectedColor = isLightMode ? UIColor(hex: 0xDFDFDF) : UIColor(hex: 0x0C0C0C)
button.setBackgroundImage(getImage(withColor: selectedColor), for: UIControl.State.highlighted)
button.addTarget(self, action: selector, for: UIControl.Event.touchUpInside)
button.set(.height, to: Values.settingButtonHeight)
return button
}
var result = [
getSeparator(),
getSettingButton(withTitle: NSLocalizedString("vc_settings_privacy_button_title", comment: ""), color: Colors.text, action: #selector(showPrivacySettings)),
getSeparator(),
getSettingButton(withTitle: NSLocalizedString("vc_settings_notifications_button_title", comment: ""), color: Colors.text, action: #selector(showNotificationSettings)),
getSeparator(),
getSettingButton(withTitle: "Invite", color: Colors.text, action: #selector(sendInvitation)),
getSeparator()
]
if !KeyPairUtilities.hasV2KeyPair() {
result += [
getSettingButton(withTitle: "Upgrade Session ID", color: Colors.text, action: #selector(upgradeSessionID)),
getSeparator()
]
}
result += [
getSettingButton(withTitle: NSLocalizedString("vc_settings_recovery_phrase_button_title", comment: ""), color: Colors.text, action: #selector(showSeed)),
getSeparator(),
getSettingButton(withTitle: NSLocalizedString("vc_settings_clear_all_data_button_title", comment: ""), color: Colors.destructive, action: #selector(clearAllData)),
getSeparator()
]
return result
}
// MARK: General
@objc private func enableCopyButton() {
copyButton.isUserInteractionEnabled = true
UIView.transition(with: copyButton, duration: 0.25, options: .transitionCrossDissolve, animations: {
self.copyButton.setTitle(NSLocalizedString("copy", comment: ""), for: UIControl.State.normal)
}, completion: nil)
}
func avatarActionSheetTitle() -> String? {
return "Update Profile Picture"
}
func fromViewController() -> UIViewController {
return self
}
func hasClearAvatarAction() -> Bool {
return false
}
func clearAvatarActionLabel() -> String {
return "Clear"
}
// MARK: Updating
private func handleIsEditingDisplayNameChanged() {
updateNavigationBarButtons()
UIView.animate(withDuration: 0.25) {
self.displayNameLabel.alpha = self.isEditingDisplayName ? 0 : 1
self.displayNameTextField.alpha = self.isEditingDisplayName ? 1 : 0
}
if isEditingDisplayName {
displayNameTextField.becomeFirstResponder()
} else {
displayNameTextField.resignFirstResponder()
}
}
private func updateNavigationBarButtons() {
if isEditingDisplayName {
let cancelButton = UIBarButtonItem(barButtonSystemItem: .cancel, target: self, action: #selector(handleCancelDisplayNameEditingButtonTapped))
cancelButton.tintColor = Colors.text
navigationItem.leftBarButtonItem = cancelButton
let doneButton = UIBarButtonItem(barButtonSystemItem: .done, target: self, action: #selector(handleSaveDisplayNameButtonTapped))
doneButton.tintColor = Colors.text
navigationItem.rightBarButtonItem = doneButton
} else {
let closeButton = UIBarButtonItem(image: #imageLiteral(resourceName: "X"), style: .plain, target: self, action: #selector(close))
closeButton.tintColor = Colors.text
navigationItem.leftBarButtonItem = closeButton
if #available(iOS 13, *) { // Pre iOS 13 the user can't switch actively but the app still responds to system changes
let appModeIcon = isDarkMode ? #imageLiteral(resourceName: "ic_dark_theme_on").withTintColor(.white) : #imageLiteral(resourceName: "ic_dark_theme_off").withTintColor(.black)
let appModeButton = UIButton()
appModeButton.setImage(appModeIcon, for: UIControl.State.normal)
appModeButton.tintColor = Colors.text
appModeButton.addTarget(self, action: #selector(switchAppMode), for: UIControl.Event.touchUpInside)
let qrCodeIcon = isDarkMode ? #imageLiteral(resourceName: "QRCode").withTintColor(.white) : #imageLiteral(resourceName: "QRCode").withTintColor(.black)
let qrCodeButton = UIButton()
qrCodeButton.setImage(qrCodeIcon, for: UIControl.State.normal)
qrCodeButton.tintColor = Colors.text
qrCodeButton.addTarget(self, action: #selector(showQRCode), for: UIControl.Event.touchUpInside)
let stackView = UIStackView(arrangedSubviews: [ appModeButton, qrCodeButton ])
stackView.axis = .horizontal
stackView.spacing = Values.mediumSpacing
navigationItem.rightBarButtonItem = UIBarButtonItem(customView: stackView)
} else {
let qrCodeIcon = isDarkMode ? #imageLiteral(resourceName: "QRCode").asTintedImage(color: .white) : #imageLiteral(resourceName: "QRCode").asTintedImage(color: .black)
let qrCodeButton = UIBarButtonItem(image: qrCodeIcon, style: .plain, target: self, action: #selector(showQRCode))
qrCodeButton.tintColor = Colors.text
navigationItem.rightBarButtonItem = qrCodeButton
}
}
}
func avatarDidChange(_ image: UIImage) {
let maxSize = Int(kOWSProfileManager_MaxAvatarDiameter)
profilePictureToBeUploaded = image.resizedImage(toFillPixelSize: CGSize(width: maxSize, height: maxSize))
updateProfile(isUpdatingDisplayName: false, isUpdatingProfilePicture: true)
}
func clearAvatar() {
profilePictureToBeUploaded = nil
updateProfile(isUpdatingDisplayName: false, isUpdatingProfilePicture: true)
}
private func updateProfile(isUpdatingDisplayName: Bool, isUpdatingProfilePicture: Bool) {
let displayName = displayNameToBeUploaded ?? OWSProfileManager.shared().profileNameForRecipient(withID: getUserHexEncodedPublicKey())
let profilePicture = profilePictureToBeUploaded ?? OWSProfileManager.shared().profileAvatar(forRecipientId: getUserHexEncodedPublicKey())
ModalActivityIndicatorViewController.present(fromViewController: navigationController!, canCancel: false) { [weak self] modalActivityIndicator in
OWSProfileManager.shared().updateLocalProfileName(displayName, avatarImage: profilePicture, success: {
DispatchQueue.main.async {
modalActivityIndicator.dismiss {
guard let self = self else { return }
self.profilePictureView.update()
self.displayNameLabel.text = displayName
self.profilePictureToBeUploaded = nil
self.displayNameToBeUploaded = nil
}
}
}, failure: { error in
DispatchQueue.main.async {
modalActivityIndicator.dismiss {
var isMaxFileSizeExceeded = false
if let error = error as? DotNetAPI.Error {
isMaxFileSizeExceeded = (error == .maxFileSizeExceeded)
}
let title = isMaxFileSizeExceeded ? "Maximum File Size Exceeded" : "Couldn't Update Profile"
let message = isMaxFileSizeExceeded ? "Please select a smaller photo and try again" : "Please check your internet connection and try again"
let alert = UIAlertController(title: title, message: message, preferredStyle: .alert)
alert.addAction(UIAlertAction(title: NSLocalizedString("OK", comment: ""), style: .default, handler: nil))
self?.present(alert, animated: true, completion: nil)
}
}
}, requiresSync: true)
}
}
@objc override internal func handleAppModeChangedNotification(_ notification: Notification) {
super.handleAppModeChangedNotification(notification)
updateNavigationBarButtons()
settingButtonsStackView.arrangedSubviews.forEach { settingButton in
settingButtonsStackView.removeArrangedSubview(settingButton)
settingButton.removeFromSuperview()
}
getSettingButtons().forEach { settingButtonOrSeparator in
settingButtonsStackView.addArrangedSubview(settingButtonOrSeparator) // Re-do the setting buttons
}
}
// MARK: Interaction
@objc private func close() {
dismiss(animated: true, completion: nil)
}
@objc private func switchAppMode() {
let newAppMode: AppMode = isLightMode ? .dark : .light
AppModeManager.shared.setCurrentAppMode(to: newAppMode)
}
@objc private func showQRCode() {
let qrCodeVC = QRCodeVC()
navigationController!.pushViewController(qrCodeVC, animated: true)
}
@objc private func handleCancelDisplayNameEditingButtonTapped() {
isEditingDisplayName = false
}
@objc private func handleSaveDisplayNameButtonTapped() {
func showError(title: String, message: String = "") {
let alert = UIAlertController(title: title, message: message, preferredStyle: .alert)
alert.addAction(UIAlertAction(title: NSLocalizedString("OK", comment: ""), style: .default, handler: nil))
presentAlert(alert)
}
let displayName = displayNameTextField.text!.trimmingCharacters(in: CharacterSet.whitespacesAndNewlines)
guard !displayName.isEmpty else {
return showError(title: NSLocalizedString("vc_settings_display_name_missing_error", comment: ""))
}
guard !OWSProfileManager.shared().isProfileNameTooLong(displayName) else {
return showError(title: NSLocalizedString("vc_settings_display_name_too_long_error", comment: ""))
}
isEditingDisplayName = false
displayNameToBeUploaded = displayName
updateProfile(isUpdatingDisplayName: true, isUpdatingProfilePicture: false)
}
@objc private func showEditProfilePictureUI() {
profilePictureUtilities.showChangeAvatarUI()
}
@objc private func showEditDisplayNameUI() {
isEditingDisplayName = true
}
@objc private func copyPublicKey() {
UIPasteboard.general.string = getUserHexEncodedPublicKey()
copyButton.isUserInteractionEnabled = false
UIView.transition(with: copyButton, duration: 0.25, options: .transitionCrossDissolve, animations: {
self.copyButton.setTitle("Copied", for: UIControl.State.normal)
}, completion: nil)
Timer.scheduledTimer(timeInterval: 4, target: self, selector: #selector(enableCopyButton), userInfo: nil, repeats: false)
}
@objc private func sharePublicKey() {
let shareVC = UIActivityViewController(activityItems: [ getUserHexEncodedPublicKey() ], applicationActivities: nil)
navigationController!.present(shareVC, animated: true, completion: nil)
}
@objc private func showPrivacySettings() {
let privacySettingsVC = PrivacySettingsTableViewController()
navigationController!.pushViewController(privacySettingsVC, animated: true)
}
@objc private func showNotificationSettings() {
let notificationSettingsVC = NotificationSettingsViewController()
navigationController!.pushViewController(notificationSettingsVC, animated: true)
}
@objc private func sendInvitation() {
let invitation = "Hey, I've been using Session to chat with complete privacy and security. Come join me! Download it at https://getsession.org/. My Session ID is \(getUserHexEncodedPublicKey())!"
let shareVC = UIActivityViewController(activityItems: [ invitation ], applicationActivities: nil)
navigationController!.present(shareVC, animated: true, completion: nil)
}
@objc private func upgradeSessionID() {
let message = "Youre upgrading to a new Session ID. This will give you improved privacy and security, but it will clear ALL app data. Contacts and conversations will be lost. Proceed?"
let alert = UIAlertController(title: "Upgrade Session ID?", message: message, preferredStyle: .alert)
alert.addAction(UIAlertAction(title: "Yes", style: .destructive) { _ in
Storage.prepareForV2KeyPairMigration()
})
alert.addAction(UIAlertAction(title: "Cancel", style: .default, handler: nil))
present(alert, animated: true, completion: nil)
}
@objc private func showSeed() {
let seedModal = SeedModal()
seedModal.modalPresentationStyle = .overFullScreen
seedModal.modalTransitionStyle = .crossDissolve
present(seedModal, animated: true, completion: nil)
}
@objc private func clearAllData() {
let nukeDataModal = NukeDataModal()
nukeDataModal.modalPresentationStyle = .overFullScreen
nukeDataModal.modalTransitionStyle = .crossDissolve
present(nukeDataModal, animated: true, completion: nil)
}
}