407 lines
17 KiB
Swift
407 lines
17 KiB
Swift
//
|
|
// MastodonRegisterViewController.swift
|
|
// Mastodon
|
|
//
|
|
// Created by MainasuK Cirno on 2021-2-5.
|
|
//
|
|
|
|
import AlamofireImage
|
|
import Combine
|
|
import MastodonSDK
|
|
import os.log
|
|
import PhotosUI
|
|
import UIKit
|
|
import MastodonUI
|
|
import MastodonAsset
|
|
import MastodonLocalization
|
|
|
|
final class MastodonRegisterViewController: UIViewController, NeedsDependency, OnboardingViewControllerAppearance {
|
|
|
|
static let avatarImageMaxSizeInPixel = CGSize(width: 400, height: 400)
|
|
|
|
let logger = Logger(subsystem: "MastodonRegisterViewController", category: "ViewController")
|
|
|
|
var disposeBag = Set<AnyCancellable>()
|
|
private var observations = Set<NSKeyValueObservation>()
|
|
|
|
weak var context: AppContext! { willSet { precondition(!isViewLoaded) } }
|
|
weak var coordinator: SceneCoordinator! { willSet { precondition(!isViewLoaded) } }
|
|
|
|
var viewModel: MastodonRegisterViewModel!
|
|
|
|
// picker
|
|
private(set) lazy var imagePicker: PHPickerViewController = {
|
|
var configuration = PHPickerConfiguration()
|
|
configuration.filter = .images
|
|
configuration.selectionLimit = 1
|
|
|
|
let imagePicker = PHPickerViewController(configuration: configuration)
|
|
imagePicker.delegate = self
|
|
return imagePicker
|
|
}()
|
|
private(set) lazy var imagePickerController: UIImagePickerController = {
|
|
let imagePickerController = UIImagePickerController()
|
|
imagePickerController.sourceType = .camera
|
|
imagePickerController.delegate = self
|
|
return imagePickerController
|
|
}()
|
|
|
|
private(set) lazy var documentPickerController: UIDocumentPickerViewController = {
|
|
let documentPickerController = UIDocumentPickerViewController(forOpeningContentTypes: [.image])
|
|
documentPickerController.delegate = self
|
|
return documentPickerController
|
|
}()
|
|
|
|
let tapGestureRecognizer = UITapGestureRecognizer.singleTapGestureRecognizer
|
|
|
|
let tableView: UITableView = {
|
|
let tableView = UITableView()
|
|
tableView.rowHeight = UITableView.automaticDimension
|
|
tableView.separatorStyle = .none
|
|
tableView.backgroundColor = .clear
|
|
tableView.keyboardDismissMode = .onDrag
|
|
if #available(iOS 15.0, *) {
|
|
tableView.sectionHeaderTopPadding = .leastNonzeroMagnitude
|
|
} else {
|
|
// Fallback on earlier versions
|
|
}
|
|
return tableView
|
|
}()
|
|
|
|
let navigationActionView: NavigationActionView = {
|
|
let navigationActionView = NavigationActionView()
|
|
navigationActionView.backgroundColor = Asset.Scene.Onboarding.background.color
|
|
return navigationActionView
|
|
}()
|
|
|
|
deinit {
|
|
os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s", (#file as NSString).lastPathComponent, #line, #function)
|
|
}
|
|
|
|
}
|
|
|
|
extension MastodonRegisterViewController {
|
|
|
|
override func viewDidLoad() {
|
|
super.viewDidLoad()
|
|
|
|
navigationItem.leftBarButtonItem = UIBarButtonItem()
|
|
|
|
setupOnboardingAppearance()
|
|
defer {
|
|
setupNavigationBarBackgroundView()
|
|
}
|
|
|
|
tableView.translatesAutoresizingMaskIntoConstraints = false
|
|
view.addSubview(tableView)
|
|
NSLayoutConstraint.activate([
|
|
tableView.topAnchor.constraint(equalTo: view.topAnchor),
|
|
tableView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
|
|
tableView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
|
|
tableView.bottomAnchor.constraint(equalTo: view.bottomAnchor),
|
|
])
|
|
|
|
navigationActionView.translatesAutoresizingMaskIntoConstraints = false
|
|
view.addSubview(navigationActionView)
|
|
defer {
|
|
view.bringSubviewToFront(navigationActionView)
|
|
}
|
|
NSLayoutConstraint.activate([
|
|
navigationActionView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
|
|
navigationActionView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
|
|
view.bottomAnchor.constraint(equalTo: navigationActionView.bottomAnchor),
|
|
])
|
|
|
|
navigationActionView
|
|
.observe(\.bounds, options: [.initial, .new]) { [weak self] navigationActionView, _ in
|
|
guard let self = self else { return }
|
|
let inset = navigationActionView.frame.height
|
|
self.tableView.contentInset.bottom = inset
|
|
}
|
|
.store(in: &observations)
|
|
|
|
navigationActionView.backButton.addTarget(self, action: #selector(MastodonRegisterViewController.backButtonPressed(_:)), for: .touchUpInside)
|
|
navigationActionView.nextButton.addTarget(self, action: #selector(MastodonRegisterViewController.nextButtonPressed(_:)), for: .touchUpInside)
|
|
|
|
viewModel.$isAllValid
|
|
.receive(on: DispatchQueue.main)
|
|
.sink { [weak self] isAllValid in
|
|
guard let self = self else { return }
|
|
self.navigationActionView.nextButton.isEnabled = isAllValid
|
|
}
|
|
.store(in: &disposeBag)
|
|
|
|
viewModel.setupDiffableDataSource(tableView: tableView)
|
|
|
|
KeyboardResponderService
|
|
.configure(
|
|
scrollView: tableView,
|
|
layoutNeedsUpdate: viewModel.viewDidAppear.eraseToAnyPublisher()
|
|
)
|
|
.store(in: &disposeBag)
|
|
|
|
// gesture
|
|
view.addGestureRecognizer(tapGestureRecognizer)
|
|
tapGestureRecognizer.addTarget(self, action: #selector(tapGestureRecognizerHandler))
|
|
|
|
// // return
|
|
// if viewModel.approvalRequired {
|
|
// reasonTextField.returnKeyType = .done
|
|
// } else {
|
|
// passwordTextField.returnKeyType = .done
|
|
// }
|
|
//
|
|
// viewModel.usernameValidateState
|
|
// .receive(on: DispatchQueue.main)
|
|
// .sink { [weak self] validateState in
|
|
// guard let self = self else { return }
|
|
// self.setTextFieldValidAppearance(self.usernameTextField, validateState: validateState)
|
|
// }
|
|
// .store(in: &disposeBag)
|
|
// viewModel.usernameErrorPrompt
|
|
// .receive(on: DispatchQueue.main)
|
|
// .sink { [weak self] prompt in
|
|
// guard let self = self else { return }
|
|
// self.usernameErrorPromptLabel.attributedText = prompt
|
|
// }
|
|
// .store(in: &disposeBag)
|
|
// viewModel.displayNameValidateState
|
|
// .receive(on: DispatchQueue.main)
|
|
// .sink { [weak self] validateState in
|
|
// guard let self = self else { return }
|
|
// self.setTextFieldValidAppearance(self.displayNameTextField, validateState: validateState)
|
|
// }
|
|
// .store(in: &disposeBag)
|
|
// viewModel.emailValidateState
|
|
// .receive(on: DispatchQueue.main)
|
|
// .sink { [weak self] validateState in
|
|
// guard let self = self else { return }
|
|
// self.setTextFieldValidAppearance(self.emailTextField, validateState: validateState)
|
|
// }
|
|
// .store(in: &disposeBag)
|
|
// viewModel.emailErrorPrompt
|
|
// .receive(on: DispatchQueue.main)
|
|
// .sink { [weak self] prompt in
|
|
// guard let self = self else { return }
|
|
// self.emailErrorPromptLabel.attributedText = prompt
|
|
// }
|
|
// .store(in: &disposeBag)
|
|
// viewModel.passwordValidateState
|
|
// .receive(on: DispatchQueue.main)
|
|
// .sink { [weak self] validateState in
|
|
// guard let self = self else { return }
|
|
// self.setTextFieldValidAppearance(self.passwordTextField, validateState: validateState)
|
|
// self.passwordCheckLabel.attributedText = MastodonRegisterViewModel.attributeStringForPassword(validateState: validateState)
|
|
// }
|
|
// .store(in: &disposeBag)
|
|
// viewModel.passwordErrorPrompt
|
|
// .receive(on: DispatchQueue.main)
|
|
// .sink { [weak self] prompt in
|
|
// guard let self = self else { return }
|
|
// self.passwordErrorPromptLabel.attributedText = prompt
|
|
// }
|
|
// .store(in: &disposeBag)
|
|
// viewModel.reasonErrorPrompt
|
|
// .receive(on: DispatchQueue.main)
|
|
// .sink { [weak self] prompt in
|
|
// guard let self = self else { return }
|
|
// self.reasonErrorPromptLabel.attributedText = prompt
|
|
// }
|
|
// .store(in: &disposeBag)
|
|
// viewModel.error
|
|
// .receive(on: DispatchQueue.main)
|
|
// .sink { [weak self] error in
|
|
// guard let self = self else { return }
|
|
// guard let error = error as? Mastodon.API.Error else { return }
|
|
// let alertController = UIAlertController(for: error, title: "Sign Up Failure", preferredStyle: .alert)
|
|
// let okAction = UIAlertAction(title: L10n.Common.Controls.Actions.ok, style: .default, handler: nil)
|
|
// alertController.addAction(okAction)
|
|
// self.coordinator.present(
|
|
// scene: .alertController(alertController: alertController),
|
|
// from: nil,
|
|
// transition: .alertController(animated: true, completion: nil)
|
|
// )
|
|
// }
|
|
// .store(in: &disposeBag)
|
|
//
|
|
|
|
viewModel.avatarMediaMenuActionPublisher
|
|
.receive(on: DispatchQueue.main)
|
|
.sink { [weak self] action in
|
|
guard let self = self else { return }
|
|
switch action {
|
|
case .photoLibrary:
|
|
self.present(self.imagePicker, animated: true, completion: nil)
|
|
case .camera:
|
|
self.present(self.imagePickerController, animated: true, completion: nil)
|
|
case .browse:
|
|
self.present(self.documentPickerController, animated: true, completion: nil)
|
|
case .delete:
|
|
self.viewModel.avatarImage = nil
|
|
}
|
|
}
|
|
.store(in: &disposeBag)
|
|
|
|
viewModel.$isRegistering
|
|
.receive(on: DispatchQueue.main)
|
|
.sink { [weak self] isRegistering in
|
|
guard let self = self else { return }
|
|
isRegistering ? self.navigationActionView.nextButton.showLoading() : self.navigationActionView.nextButton.stopLoading()
|
|
}
|
|
.store(in: &disposeBag)
|
|
}
|
|
|
|
override func viewDidAppear(_ animated: Bool) {
|
|
super.viewDidAppear(animated)
|
|
|
|
viewModel.viewDidAppear.send()
|
|
}
|
|
|
|
}
|
|
|
|
extension MastodonRegisterViewController {
|
|
|
|
@objc private func tapGestureRecognizerHandler(_ sender: UITapGestureRecognizer) {
|
|
view.endEditing(true)
|
|
}
|
|
|
|
@objc private func backButtonPressed(_ sender: UIButton) {
|
|
logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public)")
|
|
navigationController?.popViewController(animated: true)
|
|
}
|
|
|
|
@objc private func nextButtonPressed(_ sender: UIButton) {
|
|
logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public)")
|
|
|
|
guard viewModel.isAllValid else { return }
|
|
|
|
guard !viewModel.isRegistering else { return }
|
|
viewModel.isRegistering = true
|
|
|
|
let username = viewModel.username
|
|
let email = viewModel.email
|
|
let password = viewModel.password
|
|
let reason = viewModel.reason
|
|
|
|
let locale: String = {
|
|
guard let url = Bundle.main.url(forResource: "local-codes", withExtension: "json"),
|
|
let data = try? Data(contentsOf: url),
|
|
let localCode = try? JSONDecoder().decode(MastodonLocalCode.self, from: data)
|
|
else {
|
|
assertionFailure()
|
|
return "en"
|
|
}
|
|
let fallbackLanguageCode: String = {
|
|
let code = Locale.current.languageCode ?? "en"
|
|
guard localCode[code] != nil else { return "en" }
|
|
return code
|
|
}()
|
|
|
|
// pick device preferred language
|
|
guard let identifier = Locale.preferredLanguages.first else {
|
|
return fallbackLanguageCode
|
|
}
|
|
// prepare languageCode and validate then return fallback if needs
|
|
let local = Locale(identifier: identifier)
|
|
guard let languageCode = local.languageCode,
|
|
localCode[languageCode] != nil
|
|
else {
|
|
return fallbackLanguageCode
|
|
}
|
|
// prepare extendCode and validate then return fallback if needs
|
|
let extendCodes: [String] = {
|
|
let locales = Locale.preferredLanguages.map { Locale(identifier: $0) }
|
|
return locales.compactMap { locale in
|
|
guard let languageCode = locale.languageCode,
|
|
let regionCode = locale.regionCode
|
|
else { return nil }
|
|
return languageCode + "-" + regionCode
|
|
}
|
|
}()
|
|
let _firstMatchExtendCode = extendCodes.first { code in
|
|
localCode[code] != nil
|
|
}
|
|
guard let firstMatchExtendCode = _firstMatchExtendCode else {
|
|
return languageCode
|
|
}
|
|
return firstMatchExtendCode
|
|
|
|
}()
|
|
let query = Mastodon.API.Account.RegisterQuery(
|
|
reason: reason,
|
|
username: username,
|
|
email: email,
|
|
password: password,
|
|
agreement: true, // user confirmed in the server rules scene
|
|
locale: locale
|
|
)
|
|
|
|
var retryCount = 0
|
|
|
|
// register without show server rules
|
|
context.apiService.accountRegister(
|
|
domain: viewModel.domain,
|
|
query: query,
|
|
authorization: viewModel.applicationAuthorization
|
|
)
|
|
.tryCatch { [weak self] error -> AnyPublisher<Mastodon.Response.Content<Mastodon.Entity.Token>, Error> in
|
|
guard let self = self else { throw error }
|
|
guard let error = self.viewModel.error as? Mastodon.API.Error,
|
|
case let .generic(errorEntity) = error.mastodonError,
|
|
errorEntity.error == "Validation failed: Locale is not included in the list"
|
|
else {
|
|
throw error
|
|
}
|
|
guard retryCount == 0 else {
|
|
throw error
|
|
}
|
|
let retryQuery = Mastodon.API.Account.RegisterQuery(
|
|
reason: query.reason,
|
|
username: query.username,
|
|
email: query.email,
|
|
password: query.password,
|
|
agreement: query.agreement,
|
|
locale: self.viewModel.instance.languages?.first ?? "en"
|
|
)
|
|
retryCount += 1
|
|
return self.context.apiService.accountRegister(
|
|
domain: self.viewModel.domain,
|
|
query: retryQuery,
|
|
authorization: self.viewModel.applicationAuthorization
|
|
)
|
|
}
|
|
.receive(on: DispatchQueue.main)
|
|
.sink { [weak self] completion in
|
|
guard let self = self else { return }
|
|
self.viewModel.isRegistering = false
|
|
switch completion {
|
|
case .failure(let error):
|
|
self.viewModel.error = error
|
|
case .finished:
|
|
break
|
|
}
|
|
} receiveValue: { [weak self] response in
|
|
guard let self = self else { return }
|
|
let userToken = response.value
|
|
let updateCredentialQuery: Mastodon.API.Account.UpdateCredentialQuery = {
|
|
let displayName: String? = self.viewModel.name.isEmpty ? nil : self.viewModel.name
|
|
let avatar: Mastodon.Query.MediaAttachment? = {
|
|
guard let avatarImage = self.viewModel.avatarImage else { return nil }
|
|
guard avatarImage.size.width <= MastodonRegisterViewController.avatarImageMaxSizeInPixel.width else {
|
|
return .png(avatarImage.af.imageScaled(to: MastodonRegisterViewController.avatarImageMaxSizeInPixel).pngData())
|
|
}
|
|
return .png(avatarImage.pngData())
|
|
}()
|
|
return Mastodon.API.Account.UpdateCredentialQuery(
|
|
displayName: displayName,
|
|
avatar: avatar
|
|
)
|
|
}()
|
|
let viewModel = MastodonConfirmEmailViewModel(context: self.context, email: email, authenticateInfo: self.viewModel.authenticateInfo, userToken: userToken, updateCredentialQuery: updateCredentialQuery)
|
|
self.coordinator.present(scene: .mastodonConfirmEmail(viewModel: viewModel), from: self, transition: .show)
|
|
}
|
|
.store(in: &disposeBag)
|
|
}
|
|
|
|
}
|