2021-02-05 10:53:00 +01:00
|
|
|
//
|
|
|
|
// MastodonRegisterViewModel.swift
|
|
|
|
// Mastodon
|
|
|
|
//
|
|
|
|
// Created by MainasuK Cirno on 2021-2-5.
|
|
|
|
//
|
|
|
|
|
|
|
|
import Combine
|
2021-02-20 06:55:06 +01:00
|
|
|
import Foundation
|
2021-02-05 10:53:00 +01:00
|
|
|
import MastodonSDK
|
2021-02-20 06:55:06 +01:00
|
|
|
import UIKit
|
2021-02-05 10:53:00 +01:00
|
|
|
|
|
|
|
final class MastodonRegisterViewModel {
|
2021-02-20 10:13:16 +01:00
|
|
|
var disposeBag = Set<AnyCancellable>()
|
|
|
|
|
2021-02-05 10:53:00 +01:00
|
|
|
// input
|
2022-01-07 11:49:37 +01:00
|
|
|
let context: AppContext
|
2021-02-05 10:53:00 +01:00
|
|
|
let domain: String
|
2021-02-20 12:54:08 +01:00
|
|
|
let authenticateInfo: AuthenticationViewModel.AuthenticateInfo
|
2021-02-22 09:20:44 +01:00
|
|
|
let instance: Mastodon.Entity.Instance
|
2021-02-05 10:53:00 +01:00
|
|
|
let applicationToken: Mastodon.Entity.Token
|
2022-01-07 11:49:37 +01:00
|
|
|
let viewDidAppear = CurrentValueSubject<Void, Never>(Void())
|
|
|
|
|
|
|
|
@Published var avatarImage: UIImage? = nil
|
|
|
|
@Published var name = ""
|
|
|
|
@Published var username = ""
|
|
|
|
@Published var email = ""
|
|
|
|
@Published var password = ""
|
|
|
|
@Published var reason = ""
|
2021-02-22 10:48:44 +01:00
|
|
|
|
2021-03-04 08:29:46 +01:00
|
|
|
let usernameErrorPrompt = CurrentValueSubject<NSAttributedString?, Never>(nil)
|
|
|
|
let emailErrorPrompt = CurrentValueSubject<NSAttributedString?, Never>(nil)
|
|
|
|
let passwordErrorPrompt = CurrentValueSubject<NSAttributedString?, Never>(nil)
|
|
|
|
let reasonErrorPrompt = CurrentValueSubject<NSAttributedString?, Never>(nil)
|
|
|
|
|
2021-02-05 10:53:00 +01:00
|
|
|
// output
|
2022-01-07 11:49:37 +01:00
|
|
|
var diffableDataSource: UITableViewDiffableDataSource<RegisterSection, RegisterItem>?
|
2021-02-26 11:27:47 +01:00
|
|
|
let approvalRequired: Bool
|
2021-02-05 10:53:00 +01:00
|
|
|
let applicationAuthorization: Mastodon.API.OAuth.Authorization
|
2022-01-07 11:49:37 +01:00
|
|
|
|
|
|
|
@Published var usernameValidateState: ValidateState = .empty
|
|
|
|
@Published var displayNameValidateState: ValidateState = .empty
|
|
|
|
@Published var emailValidateState: ValidateState = .empty
|
|
|
|
@Published var passwordValidateState: ValidateState = .empty
|
|
|
|
@Published var reasonValidateState: ValidateState = .empty
|
2021-03-04 08:29:46 +01:00
|
|
|
|
2022-01-07 11:49:37 +01:00
|
|
|
@Published var isRegistering = false
|
|
|
|
@Published var isAllValid = false
|
|
|
|
@Published var error: Error? = nil
|
|
|
|
|
|
|
|
let avatarMediaMenuActionPublisher = PassthroughSubject<AvatarMediaMenuAction, Never>()
|
2021-02-05 10:53:00 +01:00
|
|
|
|
2021-02-20 12:54:08 +01:00
|
|
|
init(
|
2021-03-02 06:27:53 +01:00
|
|
|
context: AppContext,
|
2022-01-07 11:49:37 +01:00
|
|
|
domain: String,
|
2021-02-20 12:54:08 +01:00
|
|
|
authenticateInfo: AuthenticationViewModel.AuthenticateInfo,
|
2021-02-22 09:20:44 +01:00
|
|
|
instance: Mastodon.Entity.Instance,
|
2021-02-20 12:54:08 +01:00
|
|
|
applicationToken: Mastodon.Entity.Token
|
|
|
|
) {
|
2021-02-05 10:53:00 +01:00
|
|
|
self.domain = domain
|
2021-03-02 06:27:53 +01:00
|
|
|
self.context = context
|
2021-02-20 12:54:08 +01:00
|
|
|
self.authenticateInfo = authenticateInfo
|
2021-02-22 09:20:44 +01:00
|
|
|
self.instance = instance
|
2021-02-05 10:53:00 +01:00
|
|
|
self.applicationToken = applicationToken
|
2021-02-26 11:27:47 +01:00
|
|
|
self.approvalRequired = instance.approvalRequired ?? false
|
2021-02-05 10:53:00 +01:00
|
|
|
self.applicationAuthorization = Mastodon.API.OAuth.Authorization(accessToken: applicationToken.accessToken)
|
2021-02-20 10:13:16 +01:00
|
|
|
|
2022-01-07 11:49:37 +01:00
|
|
|
$name
|
|
|
|
.map { name in
|
|
|
|
guard !name.isEmpty else { return .empty }
|
|
|
|
return .valid
|
|
|
|
}
|
|
|
|
.assign(to: \.displayNameValidateState, on: self)
|
|
|
|
.store(in: &disposeBag)
|
|
|
|
|
|
|
|
$username
|
2021-02-20 10:13:16 +01:00
|
|
|
.map { username in
|
2021-02-22 10:48:44 +01:00
|
|
|
guard !username.isEmpty else { return .empty }
|
|
|
|
var isValid = true
|
|
|
|
|
|
|
|
// regex opt-out way to check validation
|
|
|
|
// allowed:
|
|
|
|
// a-z (isASCII && isLetter)
|
|
|
|
// A-Z (isASCII && isLetter)
|
|
|
|
// 0-9 (isASCII && isNumber)
|
|
|
|
// _ ("_")
|
|
|
|
for char in username {
|
2021-02-26 05:52:37 +01:00
|
|
|
guard char.isASCII, char.isLetter || char.isNumber || char == "_" else {
|
2021-02-22 10:48:44 +01:00
|
|
|
isValid = false
|
|
|
|
break
|
|
|
|
}
|
2021-02-20 10:13:16 +01:00
|
|
|
}
|
2021-02-22 10:48:44 +01:00
|
|
|
return isValid ? .valid : .invalid
|
2021-02-20 10:13:16 +01:00
|
|
|
}
|
2022-01-07 11:49:37 +01:00
|
|
|
.assign(to: \.usernameValidateState, on: self)
|
2021-03-02 06:27:53 +01:00
|
|
|
.store(in: &disposeBag)
|
|
|
|
|
2022-01-07 11:49:37 +01:00
|
|
|
// TODO: check username available
|
|
|
|
// username
|
|
|
|
// .filter { !$0.isEmpty }
|
|
|
|
// .debounce(for: .milliseconds(300), scheduler: DispatchQueue.main)
|
|
|
|
// .removeDuplicates()
|
|
|
|
// .compactMap { [weak self] text -> AnyPublisher<Result<Mastodon.Response.Content<Mastodon.Entity.Account>, Error>, Never>? in
|
|
|
|
// guard let self = self else { return nil }
|
|
|
|
// let query = Mastodon.API.Account.AccountLookupQuery(acct: text)
|
|
|
|
// return context.apiService.accountLookup(domain: domain, query: query, authorization: self.applicationAuthorization)
|
|
|
|
// .map {
|
|
|
|
// response -> Result<Mastodon.Response.Content<Mastodon.Entity.Account>, Error> in
|
|
|
|
// Result.success(response)
|
|
|
|
// }
|
|
|
|
// .catch { error in
|
|
|
|
// Just(Result.failure(error))
|
|
|
|
// }
|
|
|
|
// .eraseToAnyPublisher()
|
|
|
|
// }
|
|
|
|
// .switchToLatest()
|
|
|
|
// .sink { [weak self] result in
|
|
|
|
// guard let self = self else { return }
|
|
|
|
// switch result {
|
|
|
|
// case .success:
|
|
|
|
// let text = L10n.Scene.Register.Error.Reason.taken(L10n.Scene.Register.Error.Item.username)
|
|
|
|
// self.usernameErrorPrompt.value = MastodonRegisterViewModel.errorPromptAttributedString(for: text)
|
|
|
|
// self.usernameValidateState.value = .invalid
|
|
|
|
// case .failure:
|
|
|
|
// break
|
|
|
|
// }
|
|
|
|
// }
|
|
|
|
// .store(in: &disposeBag)
|
|
|
|
//
|
|
|
|
// usernameValidateState
|
|
|
|
// .sink { [weak self] validateState in
|
|
|
|
// if validateState == .valid {
|
|
|
|
// self?.usernameErrorPrompt.value = nil
|
|
|
|
// }
|
|
|
|
// }
|
|
|
|
// .store(in: &disposeBag)
|
2021-03-02 06:27:53 +01:00
|
|
|
|
2022-01-07 11:49:37 +01:00
|
|
|
$email
|
2021-02-22 10:48:44 +01:00
|
|
|
.map { email in
|
|
|
|
guard !email.isEmpty else { return .empty }
|
|
|
|
return MastodonRegisterViewModel.isValidEmail(email) ? .valid : .invalid
|
2021-02-20 11:24:23 +01:00
|
|
|
}
|
2022-01-07 11:49:37 +01:00
|
|
|
.assign(to: \.emailValidateState, on: self)
|
2021-02-20 11:24:23 +01:00
|
|
|
.store(in: &disposeBag)
|
2022-01-07 11:49:37 +01:00
|
|
|
|
|
|
|
$password
|
2021-02-22 10:48:44 +01:00
|
|
|
.map { password in
|
|
|
|
guard !password.isEmpty else { return .empty }
|
|
|
|
return password.count >= 8 ? .valid : .invalid
|
2021-02-20 11:24:23 +01:00
|
|
|
}
|
2022-01-07 11:49:37 +01:00
|
|
|
.assign(to: \.passwordValidateState, on: self)
|
2021-02-20 11:24:23 +01:00
|
|
|
.store(in: &disposeBag)
|
2022-01-07 11:49:37 +01:00
|
|
|
|
2021-02-26 08:39:05 +01:00
|
|
|
if approvalRequired {
|
2022-01-07 11:49:37 +01:00
|
|
|
$reason
|
2021-02-26 05:52:37 +01:00
|
|
|
.map { invite in
|
|
|
|
guard !invite.isEmpty else { return .empty }
|
|
|
|
return .valid
|
|
|
|
}
|
2022-01-07 11:49:37 +01:00
|
|
|
.assign(to: \.reasonValidateState, on: self)
|
2021-02-26 05:52:37 +01:00
|
|
|
.store(in: &disposeBag)
|
|
|
|
}
|
2021-03-04 08:29:46 +01:00
|
|
|
|
2022-01-07 11:49:37 +01:00
|
|
|
// error
|
|
|
|
// .sink { [weak self] error in
|
|
|
|
// guard let self = self else { return }
|
|
|
|
// let error = error as? Mastodon.API.Error
|
|
|
|
// let mastodonError = error?.mastodonError
|
|
|
|
// if case let .generic(genericMastodonError) = mastodonError,
|
|
|
|
// let details = genericMastodonError.details
|
|
|
|
// {
|
|
|
|
// self.usernameErrorPrompt.value = details.usernameErrorDescriptions.first.flatMap { MastodonRegisterViewModel.errorPromptAttributedString(for: $0) }
|
|
|
|
// self.emailErrorPrompt.value = details.emailErrorDescriptions.first.flatMap { MastodonRegisterViewModel.errorPromptAttributedString(for: $0) }
|
|
|
|
// self.passwordErrorPrompt.value = details.passwordErrorDescriptions.first.flatMap { MastodonRegisterViewModel.errorPromptAttributedString(for: $0) }
|
|
|
|
// self.reasonErrorPrompt.value = details.reasonErrorDescriptions.first.flatMap { MastodonRegisterViewModel.errorPromptAttributedString(for: $0) }
|
|
|
|
// } else {
|
|
|
|
// self.usernameErrorPrompt.value = nil
|
|
|
|
// self.emailErrorPrompt.value = nil
|
|
|
|
// self.passwordErrorPrompt.value = nil
|
|
|
|
// self.reasonErrorPrompt.value = nil
|
|
|
|
// }
|
|
|
|
// }
|
|
|
|
// .store(in: &disposeBag)
|
|
|
|
//
|
2021-02-26 08:39:05 +01:00
|
|
|
let publisherOne = Publishers.CombineLatest4(
|
2022-01-07 11:49:37 +01:00
|
|
|
$usernameValidateState,
|
|
|
|
$displayNameValidateState,
|
|
|
|
$emailValidateState,
|
|
|
|
$passwordValidateState
|
2021-03-04 08:29:46 +01:00
|
|
|
)
|
2022-01-07 11:49:37 +01:00
|
|
|
.map {
|
|
|
|
$0.0 == .valid &&
|
|
|
|
$0.1 == .valid &&
|
|
|
|
$0.2 == .valid &&
|
|
|
|
$0.3 == .valid
|
|
|
|
}
|
|
|
|
|
|
|
|
let publisherTwo = $reasonValidateState.map { reasonValidateState -> Bool in
|
|
|
|
guard self.approvalRequired else { return true }
|
|
|
|
return reasonValidateState == .valid
|
|
|
|
}
|
2021-02-26 08:39:05 +01:00
|
|
|
|
2021-02-26 05:52:37 +01:00
|
|
|
Publishers.CombineLatest(
|
|
|
|
publisherOne,
|
2022-01-07 11:49:37 +01:00
|
|
|
publisherTwo
|
2021-02-22 10:48:44 +01:00
|
|
|
)
|
2021-03-04 08:29:46 +01:00
|
|
|
.map { $0 && $1 }
|
2022-01-07 11:49:37 +01:00
|
|
|
.assign(to: \.isAllValid, on: self)
|
2021-02-22 10:48:44 +01:00
|
|
|
.store(in: &disposeBag)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
extension MastodonRegisterViewModel {
|
|
|
|
enum ValidateState {
|
|
|
|
case empty
|
|
|
|
case invalid
|
|
|
|
case valid
|
2021-02-05 10:53:00 +01:00
|
|
|
}
|
2021-02-20 06:55:06 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
extension MastodonRegisterViewModel {
|
2021-02-22 10:48:44 +01:00
|
|
|
static func isValidEmail(_ email: String) -> Bool {
|
2021-02-20 06:55:06 +01:00
|
|
|
let emailRegEx = "[A-Z0-9a-z._%+-]+@[A-Za-z0-9.-]+\\.[A-Za-z]{2,64}"
|
|
|
|
|
2021-02-20 11:24:23 +01:00
|
|
|
let emailPred = NSPredicate(format: "SELF MATCHES %@", emailRegEx)
|
2021-02-20 06:55:06 +01:00
|
|
|
return emailPred.evaluate(with: email)
|
|
|
|
}
|
|
|
|
|
2021-03-04 08:29:46 +01:00
|
|
|
static func checkmarkImage(font: UIFont = .preferredFont(forTextStyle: .caption1)) -> UIImage {
|
|
|
|
let configuration = UIImage.SymbolConfiguration(font: font)
|
|
|
|
return UIImage(systemName: "checkmark.circle.fill", withConfiguration: configuration)!
|
|
|
|
}
|
|
|
|
|
|
|
|
static func xmarkImage(font: UIFont = .preferredFont(forTextStyle: .caption1)) -> UIImage {
|
2021-02-20 06:55:06 +01:00
|
|
|
let configuration = UIImage.SymbolConfiguration(font: font)
|
2021-03-04 08:29:46 +01:00
|
|
|
return UIImage(systemName: "xmark.octagon.fill", withConfiguration: configuration)!
|
2021-02-20 06:55:06 +01:00
|
|
|
}
|
|
|
|
|
2021-03-04 08:29:46 +01:00
|
|
|
static func attributedStringImage(with image: UIImage, tintColor: UIColor) -> NSAttributedString {
|
|
|
|
let attachment = NSTextAttachment()
|
|
|
|
attachment.image = image.withTintColor(tintColor)
|
|
|
|
return NSAttributedString(attachment: attachment)
|
|
|
|
}
|
|
|
|
|
2021-03-05 12:30:52 +01:00
|
|
|
static func attributeStringForPassword(validateState: ValidateState) -> NSAttributedString {
|
2021-05-10 12:48:04 +02:00
|
|
|
let font = UIFontMetrics(forTextStyle: .caption1).scaledFont(for: .systemFont(ofSize: 13, weight: .regular), maximumPointSize: 18)
|
2021-02-20 06:55:06 +01:00
|
|
|
let attributeString = NSMutableAttributedString()
|
2021-03-04 08:29:46 +01:00
|
|
|
|
|
|
|
let image = MastodonRegisterViewModel.checkmarkImage(font: font)
|
2021-04-02 10:50:07 +02:00
|
|
|
attributeString.append(attributedStringImage(with: image, tintColor: validateState == .valid ? Asset.Colors.Label.primary.color : .clear))
|
2021-03-04 08:29:46 +01:00
|
|
|
attributeString.append(NSAttributedString(string: " "))
|
2021-04-02 10:50:07 +02:00
|
|
|
let eightCharactersDescription = NSAttributedString(string: L10n.Scene.Register.Input.Password.hint, attributes: [NSAttributedString.Key.font: font, NSAttributedString.Key.foregroundColor: Asset.Colors.Label.primary.color])
|
2021-02-20 06:55:06 +01:00
|
|
|
attributeString.append(eightCharactersDescription)
|
|
|
|
|
|
|
|
return attributeString
|
|
|
|
}
|
2021-03-04 08:29:46 +01:00
|
|
|
|
|
|
|
static func errorPromptAttributedString(for prompt: String) -> NSAttributedString {
|
2021-05-10 12:48:04 +02:00
|
|
|
let font = UIFontMetrics(forTextStyle: .caption1).scaledFont(for: .systemFont(ofSize: 13, weight: .regular), maximumPointSize: 18)
|
2021-03-04 08:29:46 +01:00
|
|
|
let attributeString = NSMutableAttributedString()
|
|
|
|
|
|
|
|
let image = MastodonRegisterViewModel.xmarkImage(font: font)
|
2021-03-26 12:16:19 +01:00
|
|
|
attributeString.append(attributedStringImage(with: image, tintColor: Asset.Colors.danger.color))
|
2021-03-04 08:29:46 +01:00
|
|
|
attributeString.append(NSAttributedString(string: " "))
|
|
|
|
|
2021-03-26 12:16:19 +01:00
|
|
|
let promptAttributedString = NSAttributedString(string: prompt, attributes: [NSAttributedString.Key.font: font, NSAttributedString.Key.foregroundColor: Asset.Colors.danger.color])
|
2021-03-04 08:29:46 +01:00
|
|
|
attributeString.append(promptAttributedString)
|
|
|
|
|
|
|
|
return attributeString
|
2021-02-20 06:55:06 +01:00
|
|
|
}
|
2021-02-05 10:53:00 +01:00
|
|
|
}
|