From a9fdd2efa3f6beb47258906dde41c48037923dac Mon Sep 17 00:00:00 2001 From: sunxiaojian Date: Tue, 2 Mar 2021 13:27:53 +0800 Subject: [PATCH 1/3] fix: acct lookup support --- .../MastodonPickServerViewController.swift | 1 + .../Register/MastodonRegisterViewModel.swift | 35 +++++++++++++ .../MastodonServerRulesViewController.swift | 2 +- .../APIService/APIService+Account.swift | 13 +++++ .../API/Mastodon+API+Account.swift | 51 +++++++++++++++++++ 5 files changed, 101 insertions(+), 1 deletion(-) diff --git a/Mastodon/Scene/Onboarding/PickServer/MastodonPickServerViewController.swift b/Mastodon/Scene/Onboarding/PickServer/MastodonPickServerViewController.swift index 685709719..638734c11 100644 --- a/Mastodon/Scene/Onboarding/PickServer/MastodonPickServerViewController.swift +++ b/Mastodon/Scene/Onboarding/PickServer/MastodonPickServerViewController.swift @@ -336,6 +336,7 @@ extension MastodonPickServerViewController { } else { let mastodonRegisterViewModel = MastodonRegisterViewModel( domain: server.domain, + context: self.context, authenticateInfo: response.authenticateInfo, instance: response.instance.value, applicationToken: response.applicationToken.value diff --git a/Mastodon/Scene/Onboarding/Register/MastodonRegisterViewModel.swift b/Mastodon/Scene/Onboarding/Register/MastodonRegisterViewModel.swift index cd6106c23..919443f2d 100644 --- a/Mastodon/Scene/Onboarding/Register/MastodonRegisterViewModel.swift +++ b/Mastodon/Scene/Onboarding/Register/MastodonRegisterViewModel.swift @@ -18,6 +18,7 @@ final class MastodonRegisterViewModel { let authenticateInfo: AuthenticationViewModel.AuthenticateInfo let instance: Mastodon.Entity.Instance let applicationToken: Mastodon.Entity.Token + let context: AppContext let username = CurrentValueSubject("") let displayName = CurrentValueSubject("") @@ -46,11 +47,13 @@ final class MastodonRegisterViewModel { init( domain: String, + context: AppContext, authenticateInfo: AuthenticationViewModel.AuthenticateInfo, instance: Mastodon.Entity.Instance, applicationToken: Mastodon.Entity.Token ) { self.domain = domain + self.context = context self.authenticateInfo = authenticateInfo self.instance = instance self.applicationToken = applicationToken @@ -78,6 +81,21 @@ final class MastodonRegisterViewModel { } .assign(to: \.value, on: usernameValidateState) .store(in: &disposeBag) + + username.debounce(for: .milliseconds(300), scheduler: DispatchQueue.main).removeDuplicates() + .sink { [weak self] text in + self?.lookupAccount(by: text) + } + .store(in: &disposeBag) + + usernameValidateState + .sink { [weak self] validateState in + if validateState == .valid { + self?.usernameErrorPrompt.value = nil + } + } + .store(in: &disposeBag) + displayName .map { displayname in guard !displayname.isEmpty else { return .empty } @@ -145,6 +163,23 @@ final class MastodonRegisterViewModel { .assign(to: \.value, on: isAllValid) .store(in: &disposeBag) } + + func lookupAccount(by acct: String) { + if acct.isEmpty { + return + } + let query = Mastodon.API.Account.AccountLookupQuery(acct: acct) + context.apiService.accountLookup(domain: domain, query: query, authorization: applicationAuthorization) + .sink { _ in + + } receiveValue: { [weak self] account in + guard let self = self else { return } + let text = L10n.Scene.Register.Error.Reason.taken(L10n.Scene.Register.Error.Item.username) + self.usernameErrorPrompt.value = MastodonRegisterViewModel.errorPromptAttributedString(for: text) + } + .store(in: &disposeBag) + + } } extension MastodonRegisterViewModel { diff --git a/Mastodon/Scene/Onboarding/ServerRules/MastodonServerRulesViewController.swift b/Mastodon/Scene/Onboarding/ServerRules/MastodonServerRulesViewController.swift index fb86e81e1..447896c22 100644 --- a/Mastodon/Scene/Onboarding/ServerRules/MastodonServerRulesViewController.swift +++ b/Mastodon/Scene/Onboarding/ServerRules/MastodonServerRulesViewController.swift @@ -204,7 +204,7 @@ extension MastodonServerRulesViewController { @objc private func confirmButtonPressed(_ sender: UIButton) { os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s", ((#file as NSString).lastPathComponent), #line, #function) - let viewModel = MastodonRegisterViewModel(domain: self.viewModel.domain, authenticateInfo: self.viewModel.authenticateInfo, instance: self.viewModel.instance, applicationToken: self.viewModel.applicationToken) + let viewModel = MastodonRegisterViewModel(domain: self.viewModel.domain,context: self.context, authenticateInfo: self.viewModel.authenticateInfo, instance: self.viewModel.instance, applicationToken: self.viewModel.applicationToken) self.coordinator.present(scene: .mastodonRegister(viewModel: viewModel), from: self, transition: .show) } } diff --git a/Mastodon/Service/APIService/APIService+Account.swift b/Mastodon/Service/APIService/APIService+Account.swift index 04908514b..7638f2444 100644 --- a/Mastodon/Service/APIService/APIService+Account.swift +++ b/Mastodon/Service/APIService/APIService+Account.swift @@ -152,4 +152,17 @@ extension APIService { ) } + func accountLookup( + domain: String, + query: Mastodon.API.Account.AccountLookupQuery, + authorization: Mastodon.API.OAuth.Authorization + ) -> AnyPublisher, Error> { + return Mastodon.API.Account.lookupAccount( + session: session, + domain: domain, + query: query, + authorization: authorization + ) + } + } diff --git a/MastodonSDK/Sources/MastodonSDK/API/Mastodon+API+Account.swift b/MastodonSDK/Sources/MastodonSDK/API/Mastodon+API+Account.swift index 0f98dbe05..78f338b6f 100644 --- a/MastodonSDK/Sources/MastodonSDK/API/Mastodon+API+Account.swift +++ b/MastodonSDK/Sources/MastodonSDK/API/Mastodon+API+Account.swift @@ -132,3 +132,54 @@ extension Mastodon.API.Account { } } + +extension Mastodon.API.Account { + static func accountsLookupEndpointURL(domain: String) -> URL { + return Mastodon.API.endpointURL(domain: domain).appendingPathComponent("accounts/lookup") + } + + public struct AccountLookupQuery: GetQuery { + + public var acct: String + + public init(acct: String) { + self.acct = acct + } + + var queryItems: [URLQueryItem]? { + var items: [URLQueryItem] = [] + items.append(URLQueryItem(name: "acct", value: acct)) + return items + } + } + + /// lookup account by acct. + /// + /// - Version: 3.3.1 + + /// - Parameters: + /// - session: `URLSession` + /// - domain: Mastodon instance domain. e.g. "example.com" + /// - query: `AccountInfoQuery` with account query information, + /// - authorization: user token + /// - Returns: `AnyPublisher` contains `Account` nested in the response + public static func lookupAccount( + session: URLSession, + domain: String, + query: AccountLookupQuery, + authorization: Mastodon.API.OAuth.Authorization? + ) -> AnyPublisher, Error> { + let request = Mastodon.API.get( + url: accountsLookupEndpointURL(domain: domain), + query: query, + authorization: authorization + ) + return session.dataTaskPublisher(for: request) + .tryMap { data, response in + let value = try Mastodon.API.decode(type: Mastodon.Entity.Account.self, from: data, response: response) + return Mastodon.Response.Content(value: value, response: response) + } + .eraseToAnyPublisher() + } + +} From 1e5daf5a7714c4a85aa8e4807fa3dfb26ebcda3f Mon Sep 17 00:00:00 2001 From: sunxiaojian Date: Thu, 29 Apr 2021 16:12:04 +0800 Subject: [PATCH 2/3] fix: the race-condition issue in username checking --- .../Register/MastodonRegisterViewModel.swift | 56 +++++++++++-------- .../MastodonServerRulesViewController.swift | 2 +- .../API/Mastodon+API+Account.swift | 2 +- 3 files changed, 34 insertions(+), 26 deletions(-) diff --git a/Mastodon/Scene/Onboarding/Register/MastodonRegisterViewModel.swift b/Mastodon/Scene/Onboarding/Register/MastodonRegisterViewModel.swift index 919443f2d..5fd8c31b6 100644 --- a/Mastodon/Scene/Onboarding/Register/MastodonRegisterViewModel.swift +++ b/Mastodon/Scene/Onboarding/Register/MastodonRegisterViewModel.swift @@ -82,10 +82,36 @@ final class MastodonRegisterViewModel { .assign(to: \.value, on: usernameValidateState) .store(in: &disposeBag) - username.debounce(for: .milliseconds(300), scheduler: DispatchQueue.main).removeDuplicates() - .sink { [weak self] text in - self?.lookupAccount(by: text) + username + .filter { !$0.isEmpty } + .debounce(for: .milliseconds(300), scheduler: DispatchQueue.main) + .removeDuplicates() + .compactMap { [weak self] text -> AnyPublisher, 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, Error>in + Result.success(response) + } + .catch { error in + Just(Result.failure(error)) + } + .eraseToAnyPublisher() } + .switchToLatest() + .sink(receiveCompletion: { _ in + + }, receiveValue: { [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) + case .failure: + break + } + }) .store(in: &disposeBag) usernameValidateState @@ -133,7 +159,8 @@ final class MastodonRegisterViewModel { let error = error as? Mastodon.API.Error let mastodonError = error?.mastodonError if case let .generic(genericMastodonError) = mastodonError, - let details = genericMastodonError.details { + 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) } @@ -157,29 +184,12 @@ final class MastodonRegisterViewModel { Publishers.CombineLatest( publisherOne, - approvalRequired ? reasonValidateState.map {$0 == .valid}.eraseToAnyPublisher() : Just(true).eraseToAnyPublisher() + approvalRequired ? reasonValidateState.map { $0 == .valid }.eraseToAnyPublisher() : Just(true).eraseToAnyPublisher() ) .map { $0 && $1 } .assign(to: \.value, on: isAllValid) .store(in: &disposeBag) } - - func lookupAccount(by acct: String) { - if acct.isEmpty { - return - } - let query = Mastodon.API.Account.AccountLookupQuery(acct: acct) - context.apiService.accountLookup(domain: domain, query: query, authorization: applicationAuthorization) - .sink { _ in - - } receiveValue: { [weak self] account in - guard let self = self else { return } - let text = L10n.Scene.Register.Error.Reason.taken(L10n.Scene.Register.Error.Item.username) - self.usernameErrorPrompt.value = MastodonRegisterViewModel.errorPromptAttributedString(for: text) - } - .store(in: &disposeBag) - - } } extension MastodonRegisterViewModel { @@ -191,7 +201,6 @@ extension MastodonRegisterViewModel { } extension MastodonRegisterViewModel { - static func isValidEmail(_ email: String) -> Bool { let emailRegEx = "[A-Z0-9a-z._%+-]+@[A-Za-z0-9.-]+\\.[A-Za-z]{2,64}" @@ -241,5 +250,4 @@ extension MastodonRegisterViewModel { return attributeString } - } diff --git a/Mastodon/Scene/Onboarding/ServerRules/MastodonServerRulesViewController.swift b/Mastodon/Scene/Onboarding/ServerRules/MastodonServerRulesViewController.swift index 447896c22..d8638421a 100644 --- a/Mastodon/Scene/Onboarding/ServerRules/MastodonServerRulesViewController.swift +++ b/Mastodon/Scene/Onboarding/ServerRules/MastodonServerRulesViewController.swift @@ -204,7 +204,7 @@ extension MastodonServerRulesViewController { @objc private func confirmButtonPressed(_ sender: UIButton) { os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s", ((#file as NSString).lastPathComponent), #line, #function) - let viewModel = MastodonRegisterViewModel(domain: self.viewModel.domain,context: self.context, authenticateInfo: self.viewModel.authenticateInfo, instance: self.viewModel.instance, applicationToken: self.viewModel.applicationToken) + let viewModel = MastodonRegisterViewModel(domain: self.viewModel.domain, context: self.context, authenticateInfo: self.viewModel.authenticateInfo, instance: self.viewModel.instance, applicationToken: self.viewModel.applicationToken) self.coordinator.present(scene: .mastodonRegister(viewModel: viewModel), from: self, transition: .show) } } diff --git a/MastodonSDK/Sources/MastodonSDK/API/Mastodon+API+Account.swift b/MastodonSDK/Sources/MastodonSDK/API/Mastodon+API+Account.swift index 78f338b6f..d1c5458c4 100644 --- a/MastodonSDK/Sources/MastodonSDK/API/Mastodon+API+Account.swift +++ b/MastodonSDK/Sources/MastodonSDK/API/Mastodon+API+Account.swift @@ -161,7 +161,7 @@ extension Mastodon.API.Account { /// - session: `URLSession` /// - domain: Mastodon instance domain. e.g. "example.com" /// - query: `AccountInfoQuery` with account query information, - /// - authorization: user token + /// - authorization: app token /// - Returns: `AnyPublisher` contains `Account` nested in the response public static func lookupAccount( session: URLSession, From ca320c555aa22d7322ffd35304c85ddbf7d32f91 Mon Sep 17 00:00:00 2001 From: sunxiaojian Date: Thu, 29 Apr 2021 19:24:03 +0800 Subject: [PATCH 3/3] fix: code format --- .../Onboarding/Register/MastodonRegisterViewModel.swift | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/Mastodon/Scene/Onboarding/Register/MastodonRegisterViewModel.swift b/Mastodon/Scene/Onboarding/Register/MastodonRegisterViewModel.swift index 5fd8c31b6..309204a9a 100644 --- a/Mastodon/Scene/Onboarding/Register/MastodonRegisterViewModel.swift +++ b/Mastodon/Scene/Onboarding/Register/MastodonRegisterViewModel.swift @@ -91,7 +91,7 @@ final class MastodonRegisterViewModel { let query = Mastodon.API.Account.AccountLookupQuery(acct: text) return context.apiService.accountLookup(domain: domain, query: query, authorization: self.applicationAuthorization) .map { - response -> Result, Error>in + response -> Result, Error> in Result.success(response) } .catch { error in @@ -100,9 +100,7 @@ final class MastodonRegisterViewModel { .eraseToAnyPublisher() } .switchToLatest() - .sink(receiveCompletion: { _ in - - }, receiveValue: { [weak self] result in + .sink { [weak self] result in guard let self = self else { return } switch result { case .success: @@ -111,7 +109,7 @@ final class MastodonRegisterViewModel { case .failure: break } - }) + } .store(in: &disposeBag) usernameValidateState