mirror of
https://github.com/mastodon/mastodon-ios.git
synced 2025-01-31 09:35:13 +01:00
feat: implement pick server feature
This commit is contained in:
parent
027fec1cc9
commit
50035c1359
@ -7,6 +7,8 @@
|
||||
|
||||
import UIKit
|
||||
import Combine
|
||||
import OSLog
|
||||
import MastodonSDK
|
||||
|
||||
final class PickServerViewController: UIViewController, NeedsDependency {
|
||||
|
||||
@ -17,17 +19,6 @@ final class PickServerViewController: UIViewController, NeedsDependency {
|
||||
|
||||
var viewModel: PickServerViewModel!
|
||||
|
||||
let titleLabel: UILabel = {
|
||||
let label = UILabel()
|
||||
label.font = .boldSystemFont(ofSize: 34)
|
||||
label.textColor = Asset.Colors.Label.primary.color
|
||||
label.text = L10n.Scene.ServerPicker.title
|
||||
label.adjustsFontForContentSizeCategory = true
|
||||
label.translatesAutoresizingMaskIntoConstraints = false
|
||||
label.numberOfLines = 0
|
||||
return label
|
||||
}()
|
||||
|
||||
let tableView: UITableView = {
|
||||
let tableView = ControlContainableTableView()
|
||||
tableView.register(PickServerTitleCell.self, forCellReuseIdentifier: String(describing: PickServerTitleCell.self))
|
||||
@ -37,7 +28,7 @@ final class PickServerViewController: UIViewController, NeedsDependency {
|
||||
tableView.rowHeight = UITableView.automaticDimension
|
||||
tableView.separatorStyle = .none
|
||||
tableView.backgroundColor = .clear
|
||||
|
||||
tableView.keyboardDismissMode = .onDrag
|
||||
tableView.translatesAutoresizingMaskIntoConstraints = false
|
||||
|
||||
return tableView
|
||||
@ -83,21 +74,185 @@ extension PickServerViewController {
|
||||
case .SignUp:
|
||||
nextStepButton.setTitle(L10n.Common.Controls.Actions.continue, for: .normal)
|
||||
}
|
||||
nextStepButton.addTarget(self, action: #selector(nextStepButtonDidClicked(_:)), for: .touchUpInside)
|
||||
|
||||
viewModel.tableView = tableView
|
||||
tableView.delegate = viewModel
|
||||
tableView.dataSource = viewModel
|
||||
|
||||
viewModel.searchedServers
|
||||
viewModel
|
||||
.searchedServers
|
||||
.receive(on: DispatchQueue.main)
|
||||
.sink { completion in
|
||||
print("22")
|
||||
.sink { _ in
|
||||
|
||||
} receiveValue: { [weak self] servers in
|
||||
self?.tableView.reloadSections(IndexSet(integer: 3), with: .automatic)
|
||||
if let selectedServer = self?.viewModel.selectedServer.value, servers.contains(selectedServer) {
|
||||
// Previously selected server is still in the list, do nothing
|
||||
} else {
|
||||
// Previously selected server is not in the updated list, reset the selectedServer's value
|
||||
self?.viewModel.selectedServer.send(nil)
|
||||
}
|
||||
}
|
||||
.store(in: &disposeBag)
|
||||
|
||||
|
||||
viewModel
|
||||
.selectedServer
|
||||
.map {
|
||||
$0 != nil
|
||||
}
|
||||
.assign(to: \.isEnabled, on: nextStepButton)
|
||||
.store(in: &disposeBag)
|
||||
|
||||
viewModel.error
|
||||
.compactMap { $0 }
|
||||
.receive(on: DispatchQueue.main)
|
||||
.sink { [weak self] error in
|
||||
guard let self = self else { return }
|
||||
let alertController = UIAlertController(error, 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
|
||||
.authenticated
|
||||
.receive(on: DispatchQueue.main)
|
||||
.flatMap { [weak self] (domain, user) -> AnyPublisher<Result<Bool, Error>, Never> in
|
||||
guard let self = self else { return Just(.success(false)).eraseToAnyPublisher() }
|
||||
return self.context.authenticationService.activeMastodonUser(domain: domain, userID: user.id)
|
||||
}
|
||||
.sink { [weak self] result in
|
||||
guard let self = self else { return }
|
||||
switch result {
|
||||
case .failure(let error):
|
||||
assertionFailure(error.localizedDescription)
|
||||
case .success(let isActived):
|
||||
assert(isActived)
|
||||
self.coordinator.setup()
|
||||
}
|
||||
}
|
||||
.store(in: &disposeBag)
|
||||
|
||||
|
||||
viewModel.fetchAllServers()
|
||||
}
|
||||
|
||||
override func viewWillAppear(_ animated: Bool) {
|
||||
super.viewWillAppear(animated)
|
||||
navigationController?.setNavigationBarHidden(false, animated: animated)
|
||||
}
|
||||
|
||||
@objc
|
||||
private func nextStepButtonDidClicked(_ sender: UIButton) {
|
||||
switch viewModel.mode {
|
||||
case .SignIn:
|
||||
doSignIn()
|
||||
case .SignUp:
|
||||
doSignUp()
|
||||
}
|
||||
}
|
||||
|
||||
private func doSignIn() {
|
||||
guard let server = viewModel.selectedServer.value else { return }
|
||||
context.apiService.createApplication(domain: server.domain)
|
||||
.tryMap { response -> PickServerViewModel.AuthenticateInfo in
|
||||
let application = response.value
|
||||
guard let info = PickServerViewModel.AuthenticateInfo(domain: server.domain, application: application) else {
|
||||
throw APIService.APIError.explicit(.badResponse)
|
||||
}
|
||||
return info
|
||||
}
|
||||
.receive(on: DispatchQueue.main)
|
||||
.sink { [weak self] completion in
|
||||
guard let self = self else { return }
|
||||
// self.viewModel.isAuthenticating.value = false
|
||||
|
||||
switch completion {
|
||||
case .failure(let error):
|
||||
os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s: sign in fail: %s", ((#file as NSString).lastPathComponent), #line, #function, error.localizedDescription)
|
||||
self.viewModel.error.send(error)
|
||||
case .finished:
|
||||
break
|
||||
}
|
||||
} receiveValue: { [weak self] info in
|
||||
guard let self = self else { return }
|
||||
let mastodonPinBasedAuthenticationViewModel = MastodonPinBasedAuthenticationViewModel(authenticateURL: info.authorizeURL)
|
||||
self.viewModel.authenticate(
|
||||
info: info,
|
||||
pinCodePublisher: mastodonPinBasedAuthenticationViewModel.pinCodePublisher
|
||||
)
|
||||
self.viewModel.mastodonPinBasedAuthenticationViewController = self.coordinator.present(
|
||||
scene: .mastodonPinBasedAuthentication(viewModel: mastodonPinBasedAuthenticationViewModel),
|
||||
from: nil,
|
||||
transition: .modal(animated: true, completion: nil)
|
||||
)
|
||||
}
|
||||
.store(in: &disposeBag)
|
||||
}
|
||||
|
||||
private func doSignUp() {
|
||||
os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s", ((#file as NSString).lastPathComponent), #line, #function)
|
||||
guard let server = viewModel.selectedServer.value else { return }
|
||||
// viewModel.isRegistering.value = true
|
||||
|
||||
context.apiService.instance(domain: server.domain)
|
||||
.compactMap { [weak self] response -> AnyPublisher<PickServerViewModel.SignUpResponseFirst, Error>? in
|
||||
guard let self = self else { return nil }
|
||||
guard response.value.registrations != false else {
|
||||
return Fail(error: AuthenticationViewModel.AuthenticationError.registrationClosed).eraseToAnyPublisher()
|
||||
}
|
||||
return self.context.apiService.createApplication(domain: server.domain)
|
||||
.map { PickServerViewModel.SignUpResponseFirst(instance: response, application: $0) }
|
||||
.eraseToAnyPublisher()
|
||||
}
|
||||
.switchToLatest()
|
||||
.tryMap { response -> PickServerViewModel.SignUpResponseSecond in
|
||||
let application = response.application.value
|
||||
guard let authenticateInfo = AuthenticationViewModel.AuthenticateInfo(domain: server.domain, application: application) else {
|
||||
throw APIService.APIError.explicit(.badResponse)
|
||||
}
|
||||
return PickServerViewModel.SignUpResponseSecond(instance: response.instance, authenticateInfo: authenticateInfo)
|
||||
}
|
||||
.compactMap { [weak self] response -> AnyPublisher<PickServerViewModel.SignUpResponseThird, Error>? in
|
||||
guard let self = self else { return nil }
|
||||
let instance = response.instance
|
||||
let authenticateInfo = response.authenticateInfo
|
||||
return self.context.apiService.applicationAccessToken(
|
||||
domain: server.domain,
|
||||
clientID: authenticateInfo.clientID,
|
||||
clientSecret: authenticateInfo.clientSecret
|
||||
)
|
||||
.map { PickServerViewModel.SignUpResponseThird(instance: instance, authenticateInfo: authenticateInfo, applicationToken: $0) }
|
||||
.eraseToAnyPublisher()
|
||||
}
|
||||
.switchToLatest()
|
||||
.receive(on: DispatchQueue.main)
|
||||
.sink { [weak self] completion in
|
||||
guard let self = self else { return }
|
||||
// self.viewModel.isRegistering.value = false
|
||||
|
||||
switch completion {
|
||||
case .failure(let error):
|
||||
self.viewModel.error.send(error)
|
||||
case .finished:
|
||||
break
|
||||
}
|
||||
} receiveValue: { [weak self] response in
|
||||
guard let self = self else { return }
|
||||
let mastodonRegisterViewModel = MastodonRegisterViewModel(
|
||||
domain: server.domain,
|
||||
authenticateInfo: response.authenticateInfo,
|
||||
instance: response.instance.value,
|
||||
applicationToken: response.applicationToken.value
|
||||
)
|
||||
self.coordinator.present(scene: .mastodonRegister(viewModel: mastodonRegisterViewModel), from: self, transition: .show)
|
||||
}
|
||||
.store(in: &disposeBag)
|
||||
}
|
||||
}
|
||||
|
@ -6,8 +6,10 @@
|
||||
//
|
||||
|
||||
import UIKit
|
||||
import OSLog
|
||||
import Combine
|
||||
import MastodonSDK
|
||||
import CoreDataStack
|
||||
|
||||
class PickServerViewModel: NSObject {
|
||||
enum PickServerMode {
|
||||
@ -77,12 +79,18 @@ class PickServerViewModel: NSObject {
|
||||
let allServers = CurrentValueSubject<[Mastodon.Entity.Server], Never>([])
|
||||
let searchedServers = CurrentValueSubject<[Mastodon.Entity.Server], Error>([])
|
||||
|
||||
let nextButtonEnable = CurrentValueSubject<Bool, Never>(false)
|
||||
let selectedServer = CurrentValueSubject<Mastodon.Entity.Server?, Never>(nil)
|
||||
let error = PassthroughSubject<Error, Never>()
|
||||
let authenticated = PassthroughSubject<(domain: String, account: Mastodon.Entity.Account), Never>()
|
||||
|
||||
private var disposeBag = Set<AnyCancellable>()
|
||||
|
||||
weak var tableView: UITableView?
|
||||
|
||||
private var expandServerDomainSet = Set<String>()
|
||||
|
||||
var mastodonPinBasedAuthenticationViewController: UIViewController?
|
||||
|
||||
init(context: AppContext, mode: PickServerMode) {
|
||||
self.context = context
|
||||
self.mode = mode
|
||||
@ -101,26 +109,37 @@ class PickServerViewModel: NSObject {
|
||||
searchText.debounce(for: .milliseconds(300), scheduler: DispatchQueue.main).removeDuplicates(),
|
||||
allServers
|
||||
)
|
||||
.flatMap { [weak self] (selectCategoryIndex, searchText, allServers) -> AnyPublisher<[Mastodon.Entity.Server], Error> in
|
||||
guard let self = self else { return Just([]).setFailureType(to: Error.self).eraseToAnyPublisher() }
|
||||
.flatMap { [weak self] (selectCategoryIndex, searchText, allServers) -> AnyPublisher<Result<[Mastodon.Entity.Server], Error>, Never> in
|
||||
guard let self = self else { return Just(Result.success([])).eraseToAnyPublisher() }
|
||||
|
||||
// 1. Search from the servers recorded in joinmastodon.org
|
||||
let searchedServersFromAPI = self.searchServersFromAPI(category: self.categories[selectCategoryIndex], searchText: searchText, allServers: allServers)
|
||||
if !searchedServersFromAPI.isEmpty {
|
||||
// If found servers, just return
|
||||
return Just(searchedServersFromAPI).setFailureType(to: Error.self).eraseToAnyPublisher()
|
||||
return Just(Result.success(searchedServersFromAPI)).eraseToAnyPublisher()
|
||||
}
|
||||
// 2. No server found in the recorded list, check if searchText is a valid mastodon server domain
|
||||
if let toSearchText = searchText, !toSearchText.isEmpty {
|
||||
return self.context.apiService.instance(domain: toSearchText)
|
||||
.map { return [Mastodon.Entity.Server(instance: $0.value)] }.eraseToAnyPublisher()
|
||||
.map { return Result.success([Mastodon.Entity.Server(instance: $0.value)]) }
|
||||
.catch({ error -> Just<Result<[Mastodon.Entity.Server], Error>> in
|
||||
return Just(Result.failure(error))
|
||||
})
|
||||
.eraseToAnyPublisher()
|
||||
}
|
||||
return Just(searchedServersFromAPI).setFailureType(to: Error.self).eraseToAnyPublisher()
|
||||
return Just(Result.success(searchedServersFromAPI)).eraseToAnyPublisher()
|
||||
}
|
||||
.sink { completion in
|
||||
print("1")
|
||||
} receiveValue: { [weak self] servers in
|
||||
self?.searchedServers.send(servers)
|
||||
.sink { _ in
|
||||
|
||||
} receiveValue: { [weak self] result in
|
||||
switch result {
|
||||
case .success(let servers):
|
||||
self?.searchedServers.send(servers)
|
||||
case .failure(let error):
|
||||
// TODO: What should be presented when user inputs invalid search text?
|
||||
self?.searchedServers.send([])
|
||||
}
|
||||
|
||||
}
|
||||
.store(in: &disposeBag)
|
||||
|
||||
@ -129,7 +148,6 @@ class PickServerViewModel: NSObject {
|
||||
|
||||
func fetchAllServers() {
|
||||
context.apiService.servers(language: nil, category: nil)
|
||||
.receive(on: DispatchQueue.main)
|
||||
.sink { error in
|
||||
print("11")
|
||||
} receiveValue: { [weak self] result in
|
||||
@ -152,8 +170,8 @@ class PickServerViewModel: NSObject {
|
||||
}
|
||||
// 2. Filter the searchText
|
||||
.filter {
|
||||
if let searchText = searchText {
|
||||
return $0.domain.contains(searchText)
|
||||
if let searchText = searchText, !searchText.isEmpty {
|
||||
return $0.domain.lowercased().contains(searchText.lowercased())
|
||||
} else {
|
||||
return true
|
||||
}
|
||||
@ -181,6 +199,24 @@ extension PickServerViewModel: UITableViewDelegate {
|
||||
}
|
||||
}
|
||||
|
||||
func tableView(_ tableView: UITableView, willSelectRowAt indexPath: IndexPath) -> IndexPath? {
|
||||
if tableView.indexPathForSelectedRow == indexPath {
|
||||
tableView.deselectRow(at: indexPath, animated: false)
|
||||
selectedServer.send(nil)
|
||||
return nil
|
||||
}
|
||||
return indexPath
|
||||
}
|
||||
|
||||
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
|
||||
tableView.selectRow(at: indexPath, animated: false, scrollPosition: .none)
|
||||
selectedServer.send(searchedServers.value[indexPath.row])
|
||||
}
|
||||
|
||||
func tableView(_ tableView: UITableView, didDeselectRowAt indexPath: IndexPath) {
|
||||
tableView.deselectRow(at: indexPath, animated: false)
|
||||
selectedServer.send(nil)
|
||||
}
|
||||
}
|
||||
|
||||
extension PickServerViewModel: UITableViewDataSource {
|
||||
@ -222,7 +258,19 @@ extension PickServerViewModel: UITableViewDataSource {
|
||||
return cell
|
||||
case .serverList:
|
||||
let cell = tableView.dequeueReusableCell(withIdentifier: String(describing: PickServerCell.self), for: indexPath) as! PickServerCell
|
||||
cell.server = searchedServers.value[indexPath.row]
|
||||
let server = searchedServers.value[indexPath.row]
|
||||
cell.server = server
|
||||
if expandServerDomainSet.contains(server.domain) {
|
||||
cell.mode = .expand
|
||||
} else {
|
||||
cell.mode = .collapse
|
||||
}
|
||||
if server == selectedServer.value {
|
||||
tableView.selectRow(at: indexPath, animated: false, scrollPosition: .none)
|
||||
} else {
|
||||
tableView.deselectRow(at: indexPath, animated: false)
|
||||
}
|
||||
|
||||
cell.delegate = self
|
||||
return cell
|
||||
}
|
||||
@ -254,9 +302,181 @@ extension PickServerViewModel: PickServerSearchCellDelegate {
|
||||
}
|
||||
|
||||
extension PickServerViewModel: PickServerCellDelegate {
|
||||
func pickServerCell(modeChange updates: (() -> Void)) {
|
||||
tableView?.beginUpdates()
|
||||
func pickServerCell(modeChange server: Mastodon.Entity.Server, newMode: PickServerCell.Mode, updates: (() -> Void)) {
|
||||
if newMode == .collapse {
|
||||
expandServerDomainSet.remove(server.domain)
|
||||
} else {
|
||||
expandServerDomainSet.insert(server.domain)
|
||||
}
|
||||
|
||||
tableView?.performBatchUpdates(updates, completion: nil)
|
||||
tableView?.endUpdates()
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - SignIn methods & structs
|
||||
extension PickServerViewModel {
|
||||
enum AuthenticationError: Error, LocalizedError {
|
||||
case badCredentials
|
||||
case registrationClosed
|
||||
|
||||
var errorDescription: String? {
|
||||
switch self {
|
||||
case .badCredentials: return "Bad Credentials"
|
||||
case .registrationClosed: return "Registration Closed"
|
||||
}
|
||||
}
|
||||
|
||||
var failureReason: String? {
|
||||
switch self {
|
||||
case .badCredentials: return "Credentials invalid."
|
||||
case .registrationClosed: return "Server disallow registration."
|
||||
}
|
||||
}
|
||||
|
||||
var helpAnchor: String? {
|
||||
switch self {
|
||||
case .badCredentials: return "Please try again."
|
||||
case .registrationClosed: return "Please try another domain."
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct AuthenticateInfo {
|
||||
let domain: String
|
||||
let clientID: String
|
||||
let clientSecret: String
|
||||
let authorizeURL: URL
|
||||
|
||||
init?(domain: String, application: Mastodon.Entity.Application) {
|
||||
self.domain = domain
|
||||
guard let clientID = application.clientID,
|
||||
let clientSecret = application.clientSecret else { return nil }
|
||||
self.clientID = clientID
|
||||
self.clientSecret = clientSecret
|
||||
self.authorizeURL = {
|
||||
let query = Mastodon.API.OAuth.AuthorizeQuery(clientID: clientID)
|
||||
let url = Mastodon.API.OAuth.authorizeURL(domain: domain, query: query)
|
||||
return url
|
||||
}()
|
||||
}
|
||||
}
|
||||
|
||||
func authenticate(info: AuthenticateInfo, pinCodePublisher: PassthroughSubject<String, Never>) {
|
||||
pinCodePublisher
|
||||
.handleEvents(receiveOutput: { [weak self] _ in
|
||||
guard let self = self else { return }
|
||||
// self.isAuthenticating.value = true
|
||||
self.mastodonPinBasedAuthenticationViewController?.dismiss(animated: true, completion: nil)
|
||||
self.mastodonPinBasedAuthenticationViewController = nil
|
||||
})
|
||||
.compactMap { [weak self] code -> AnyPublisher<Mastodon.Response.Content<Mastodon.Entity.Account>, Error>? in
|
||||
guard let self = self else { return nil }
|
||||
return self.context.apiService
|
||||
.userAccessToken(
|
||||
domain: info.domain,
|
||||
clientID: info.clientID,
|
||||
clientSecret: info.clientSecret,
|
||||
code: code
|
||||
)
|
||||
.flatMap { response -> AnyPublisher<Mastodon.Response.Content<Mastodon.Entity.Account>, Error> in
|
||||
let token = response.value
|
||||
os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s: sign in success. Token: %s", ((#file as NSString).lastPathComponent), #line, #function, token.accessToken)
|
||||
return Self.verifyAndSaveAuthentication(
|
||||
context: self.context,
|
||||
info: info,
|
||||
userToken: token
|
||||
)
|
||||
}
|
||||
.eraseToAnyPublisher()
|
||||
}
|
||||
.switchToLatest()
|
||||
.receive(on: DispatchQueue.main)
|
||||
.sink { [weak self] completion in
|
||||
guard let self = self else { return }
|
||||
switch completion {
|
||||
case .failure(let error):
|
||||
os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s: swap user access token swap fail: %s", ((#file as NSString).lastPathComponent), #line, #function, error.localizedDescription)
|
||||
// self.isAuthenticating.value = false
|
||||
self.error.send(error)
|
||||
case .finished:
|
||||
break
|
||||
}
|
||||
} receiveValue: { [weak self] response in
|
||||
guard let self = self else { return }
|
||||
let account = response.value
|
||||
os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s: user %s sign in success", ((#file as NSString).lastPathComponent), #line, #function, account.username)
|
||||
|
||||
self.authenticated.send((domain: info.domain, account: account))
|
||||
}
|
||||
.store(in: &self.disposeBag)
|
||||
}
|
||||
|
||||
static func verifyAndSaveAuthentication(
|
||||
context: AppContext,
|
||||
info: AuthenticateInfo,
|
||||
userToken: Mastodon.Entity.Token
|
||||
) -> AnyPublisher<Mastodon.Response.Content<Mastodon.Entity.Account>, Error> {
|
||||
let authorization = Mastodon.API.OAuth.Authorization(accessToken: userToken.accessToken)
|
||||
let managedObjectContext = context.backgroundManagedObjectContext
|
||||
|
||||
return context.apiService.accountVerifyCredentials(
|
||||
domain: info.domain,
|
||||
authorization: authorization
|
||||
)
|
||||
.flatMap { response -> AnyPublisher<Mastodon.Response.Content<Mastodon.Entity.Account>, Error> in
|
||||
let account = response.value
|
||||
let mastodonUserRequest = MastodonUser.sortedFetchRequest
|
||||
mastodonUserRequest.predicate = MastodonUser.predicate(domain: info.domain, id: account.id)
|
||||
mastodonUserRequest.fetchLimit = 1
|
||||
guard let mastodonUser = try? managedObjectContext.fetch(mastodonUserRequest).first else {
|
||||
return Fail(error: AuthenticationError.badCredentials).eraseToAnyPublisher()
|
||||
}
|
||||
|
||||
let property = MastodonAuthentication.Property(
|
||||
domain: info.domain,
|
||||
userID: mastodonUser.id,
|
||||
username: mastodonUser.username,
|
||||
appAccessToken: userToken.accessToken, // TODO: swap app token
|
||||
userAccessToken: userToken.accessToken,
|
||||
clientID: info.clientID,
|
||||
clientSecret: info.clientSecret
|
||||
)
|
||||
return managedObjectContext.performChanges {
|
||||
_ = APIService.CoreData.createOrMergeMastodonAuthentication(
|
||||
into: managedObjectContext,
|
||||
for: mastodonUser,
|
||||
in: info.domain,
|
||||
property: property,
|
||||
networkDate: response.networkDate
|
||||
)
|
||||
}
|
||||
.tryMap { result in
|
||||
switch result {
|
||||
case .failure(let error): throw error
|
||||
case .success: return response
|
||||
}
|
||||
}
|
||||
.eraseToAnyPublisher()
|
||||
}
|
||||
.eraseToAnyPublisher()
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - SignUp methods & structs
|
||||
extension PickServerViewModel {
|
||||
struct SignUpResponseFirst {
|
||||
let instance: Mastodon.Response.Content<Mastodon.Entity.Instance>
|
||||
let application: Mastodon.Response.Content<Mastodon.Entity.Application>
|
||||
}
|
||||
|
||||
struct SignUpResponseSecond {
|
||||
let instance: Mastodon.Response.Content<Mastodon.Entity.Instance>
|
||||
let authenticateInfo: AuthenticationViewModel.AuthenticateInfo
|
||||
}
|
||||
|
||||
struct SignUpResponseThird {
|
||||
let instance: Mastodon.Response.Content<Mastodon.Entity.Instance>
|
||||
let authenticateInfo: AuthenticationViewModel.AuthenticateInfo
|
||||
let applicationToken: Mastodon.Response.Content<Mastodon.Entity.Token>
|
||||
}
|
||||
}
|
||||
|
@ -10,7 +10,7 @@ import MastodonSDK
|
||||
import Kingfisher
|
||||
|
||||
protocol PickServerCellDelegate: class {
|
||||
func pickServerCell(modeChange updates: (() -> Void))
|
||||
func pickServerCell(modeChange server: Mastodon.Entity.Server, newMode: PickServerCell.Mode, updates: (() -> Void))
|
||||
}
|
||||
|
||||
class PickServerCell: UITableViewCell {
|
||||
@ -40,6 +40,8 @@ class PickServerCell: UITableViewCell {
|
||||
|
||||
private var checkbox: UIImageView = {
|
||||
let imageView = UIImageView()
|
||||
imageView.preferredSymbolConfiguration = UIImage.SymbolConfiguration(textStyle: .body)
|
||||
imageView.tintColor = Asset.Colors.lightSecondaryText.color
|
||||
imageView.contentMode = .scaleAspectFill
|
||||
imageView.translatesAutoresizingMaskIntoConstraints = false
|
||||
return imageView
|
||||
@ -80,7 +82,7 @@ class PickServerCell: UITableViewCell {
|
||||
}()
|
||||
|
||||
private var expandButton: UIButton = {
|
||||
let button = UIButton(type: .system)
|
||||
let button = UIButton(type: .custom)
|
||||
button.setTitle(L10n.Scene.ServerPicker.Button.seeMore, for: .normal)
|
||||
button.setTitle(L10n.Scene.ServerPicker.Button.seeLess, for: .selected)
|
||||
button.setTitleColor(Asset.Colors.lightBrandBlue.color, for: .normal)
|
||||
@ -238,6 +240,7 @@ extension PickServerCell {
|
||||
checkbox.heightAnchor.constraint(equalToConstant: 22),
|
||||
bgView.trailingAnchor.constraint(equalTo: checkbox.trailingAnchor, constant: 16),
|
||||
checkbox.leadingAnchor.constraint(equalTo: domainLabel.trailingAnchor, constant: 16),
|
||||
checkbox.centerYAnchor.constraint(equalTo: domainLabel.centerYAnchor),
|
||||
|
||||
descriptionLabel.leadingAnchor.constraint(equalTo: bgView.leadingAnchor, constant: 16),
|
||||
descriptionLabel.topAnchor.constraint(equalTo: domainLabel.firstBaselineAnchor, constant: 8),
|
||||
@ -284,20 +287,31 @@ extension PickServerCell {
|
||||
switch mode {
|
||||
case .collapse:
|
||||
expandBox.isHidden = true
|
||||
expandButton.isSelected = false
|
||||
NSLayoutConstraint.deactivate(expandConstraints)
|
||||
NSLayoutConstraint.activate(collapseConstraints)
|
||||
case .expand:
|
||||
expandBox.isHidden = false
|
||||
expandButton.isSelected = true
|
||||
NSLayoutConstraint.activate(expandConstraints)
|
||||
NSLayoutConstraint.deactivate(collapseConstraints)
|
||||
}
|
||||
}
|
||||
|
||||
override func setSelected(_ selected: Bool, animated: Bool) {
|
||||
super.setSelected(selected, animated: animated)
|
||||
if selected {
|
||||
checkbox.image = UIImage(systemName: "checkmark.circle.fill")
|
||||
} else {
|
||||
checkbox.image = UIImage(systemName: "circle")
|
||||
}
|
||||
}
|
||||
|
||||
@objc
|
||||
private func expandButtonDidClicked(_ sender: UIButton) {
|
||||
delegate?.pickServerCell(modeChange: {
|
||||
let newMode: Mode = mode == .collapse ? .expand : .collapse
|
||||
self.mode = newMode
|
||||
let newMode: Mode = mode == .collapse ? .expand : .collapse
|
||||
delegate?.pickServerCell(modeChange: server!, newMode: newMode, updates: { [weak self] in
|
||||
self?.mode = newMode
|
||||
})
|
||||
}
|
||||
}
|
||||
@ -316,7 +330,17 @@ extension PickServerCell {
|
||||
.transition(.fade(1))
|
||||
])
|
||||
langValueLabel.text = serverInfo.language.uppercased()
|
||||
usersValueLabel.text = "\(serverInfo.totalUsers)"
|
||||
usersValueLabel.text = parseUsersCount(serverInfo.totalUsers)
|
||||
categoryValueLabel.text = serverInfo.category.uppercased()
|
||||
}
|
||||
|
||||
private func parseUsersCount(_ usersCount: Int) -> String {
|
||||
switch usersCount {
|
||||
case 0..<1000:
|
||||
return "\(usersCount)"
|
||||
default:
|
||||
let usersCountInThousand = Float(usersCount) / 1000.0
|
||||
return String(format: "%.1fK", usersCountInThousand)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -50,6 +50,8 @@ class PickServerSearchCell: UITableViewCell {
|
||||
attributes: [.font: UIFont.preferredFont(forTextStyle: .headline),
|
||||
.foregroundColor: Asset.Colors.lightSecondaryText.color.withAlphaComponent(0.6)])
|
||||
textField.clearButtonMode = .whileEditing
|
||||
textField.autocapitalizationType = .none
|
||||
textField.autocorrectionType = .no
|
||||
return textField
|
||||
}()
|
||||
|
||||
|
@ -23,7 +23,8 @@ extension PrimaryActionButton {
|
||||
private func _init() {
|
||||
titleLabel?.font = .preferredFont(forTextStyle: .headline)
|
||||
setTitleColor(Asset.Colors.lightWhite.color, for: .normal)
|
||||
backgroundColor = Asset.Colors.lightBrandBlue.color
|
||||
setBackgroundImage(UIImage.placeholder(color: Asset.Colors.lightBrandBlue.color), for: .normal)
|
||||
setBackgroundImage(UIImage.placeholder(color: Asset.Colors.lightDisabled.color), for: .disabled)
|
||||
applyCornerRadius(radius: 10)
|
||||
setInsets(forContentPadding: UIEdgeInsets(top: 12, left: 0, bottom: 12, right: 0), imageTitlePadding: 0)
|
||||
}
|
||||
|
@ -60,6 +60,10 @@ extension WelcomeViewController {
|
||||
|
||||
overrideUserInterfaceStyle = .light
|
||||
view.backgroundColor = Asset.Colors.Background.onboardingBackground.color
|
||||
navigationController?.navigationBar.setBackgroundImage(UIImage(), for: .default)
|
||||
navigationController?.navigationBar.shadowImage = UIImage()
|
||||
navigationController?.navigationBar.isTranslucent = true
|
||||
navigationController?.view.backgroundColor = .clear
|
||||
|
||||
view.addSubview(logoImageView)
|
||||
NSLayoutConstraint.activate([
|
||||
|
@ -9,7 +9,7 @@ import Foundation
|
||||
|
||||
extension Mastodon.Entity {
|
||||
|
||||
public struct Server: Codable {
|
||||
public struct Server: Codable, Equatable {
|
||||
public let domain: String
|
||||
public let version: String
|
||||
public let description: String
|
||||
@ -40,8 +40,8 @@ extension Mastodon.Entity {
|
||||
|
||||
public init(instance: Instance) {
|
||||
self.domain = instance.title
|
||||
self.version = "\(instance.version)"
|
||||
self.description = instance.description
|
||||
self.version = instance.version ?? ""
|
||||
self.description = instance.shortDescription ?? instance.description
|
||||
self.language = instance.languages?.first ?? ""
|
||||
self.languages = instance.languages ?? []
|
||||
self.region = "Unknown" // TODO: how to handle properties not in an instance
|
||||
@ -52,6 +52,10 @@ extension Mastodon.Entity {
|
||||
self.lastWeekUsers = 0
|
||||
self.approvalRequired = instance.approvalRequired ?? false
|
||||
}
|
||||
|
||||
public static func == (lhs: Self, rhs: Self) -> Bool {
|
||||
return lhs.domain.caseInsensitiveCompare(rhs.domain) == .orderedSame
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user