feat: resolve requested changes

This commit is contained in:
jk234ert 2021-02-25 16:38:24 +08:00
parent 7329a62582
commit 8476fc0e38
11 changed files with 271 additions and 187 deletions

View File

@ -41,6 +41,18 @@
},
"server_picker": {
"title": "Pick a Server,\nany server.",
"Button": {
"Category": {
"All": "All"
},
"SeeLess": "See Less",
"SeeMore": "See More"
},
"Label": {
"Language": "LANGUAGE",
"Users": "USERS",
"Category": "CATEGORY"
},
"input": {
"placeholder": "Find a server or join your own..."
}

View File

@ -29,7 +29,13 @@
"Scene.Register.Input.Username.DuplicatePrompt" = "This username is taken.";
"Scene.Register.Input.Username.Placeholder" = "username";
"Scene.Register.Title" = "Tell us about you.";
"Scene.ServerPicker.Button.Category.All" = "All";
"Scene.ServerPicker.Button.Seeless" = "See Less";
"Scene.ServerPicker.Button.Seemore" = "See More";
"Scene.ServerPicker.Input.Placeholder" = "Find a server or join your own...";
"Scene.ServerPicker.Label.Category" = "CATEGORY";
"Scene.ServerPicker.Label.Language" = "LANGUAGE";
"Scene.ServerPicker.Label.Users" = "USERS";
"Scene.ServerPicker.Title" = "Pick a Server,
any server.";
"Scene.ServerRules.Button.Confirm" = "I Agree";

View File

@ -41,6 +41,18 @@
},
"server_picker": {
"title": "Pick a Server,\nany server.",
"Button": {
"Category": {
"All": "All"
},
"SeeLess": "See Less",
"SeeMore": "See More"
},
"Label": {
"Language": "LANGUAGE",
"Users": "USERS",
"Category": "CATEGORY"
},
"input": {
"placeholder": "Find a server or join your own..."
}

View File

@ -111,10 +111,10 @@ internal enum L10n {
/// Pick a Server,\nany server.
internal static let title = L10n.tr("Localizable", "Scene.ServerPicker.Title")
internal enum Button {
/// See less
internal static let seeLess = L10n.tr("Localizable", "Scene.ServerPicker.Button.SeeLess")
/// See Less
internal static let seeless = L10n.tr("Localizable", "Scene.ServerPicker.Button.Seeless")
/// See More
internal static let seeMore = L10n.tr("Localizable", "Scene.ServerPicker.Button.SeeMore")
internal static let seemore = L10n.tr("Localizable", "Scene.ServerPicker.Button.Seemore")
internal enum Category {
/// All
internal static let all = L10n.tr("Localizable", "Scene.ServerPicker.Button.Category.All")

View File

@ -29,16 +29,15 @@
"Scene.Register.Input.Username.DuplicatePrompt" = "This username is taken.";
"Scene.Register.Input.Username.Placeholder" = "username";
"Scene.Register.Title" = "Tell us about you.";
"Scene.ServerPicker.Input.Placeholder" = "Find a server or join your own...";
"Scene.ServerPicker.Title" = "Pick a Server,
any server.";
"Scene.ServerPicker.Button.Category.All" = "All";
"Scene.ServerPicker.Button.SeeLess" = "See less";
"Scene.ServerPicker.Button.SeeMore" = "See More";
"Scene.ServerPicker.Button.Seeless" = "See Less";
"Scene.ServerPicker.Button.Seemore" = "See More";
"Scene.ServerPicker.Input.Placeholder" = "Find a server or join your own...";
"Scene.ServerPicker.Label.Category" = "CATEGORY";
"Scene.ServerPicker.Label.Language" = "LANGUAGE";
"Scene.ServerPicker.Label.Users" = "USERS";
"Scene.ServerPicker.Label.Category" = "CATEGORY";
"Scene.ServerPicker.Title" = "Pick a Server,
any server.";
"Scene.ServerRules.Button.Confirm" = "I Agree";
"Scene.ServerRules.Prompt" = "By continuing, you're subject to the terms of service and privacy policy for %@.";
"Scene.ServerRules.Subtitle" = "These rules are set by the admins of %@.";

View File

@ -19,6 +19,17 @@ final class PickServerViewController: UIViewController, NeedsDependency {
var viewModel: PickServerViewModel!
private var isAuthenticating = CurrentValueSubject<Bool, Never>(false)
private var expandServerDomainSet = Set<String>()
enum Section: CaseIterable {
case title
case categories
case search
case serverList
}
let tableView: UITableView = {
let tableView = ControlContainableTableView()
tableView.register(PickServerTitleCell.self, forCellReuseIdentifier: String(describing: PickServerTitleCell.self))
@ -69,16 +80,16 @@ extension PickServerViewController {
])
switch viewModel.mode {
case .SignIn:
case .signIn:
nextStepButton.setTitle(L10n.Common.Controls.Actions.signIn, for: .normal)
case .SignUp:
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.tableView = tableView
tableView.delegate = self
tableView.dataSource = self
viewModel
.searchedServers
@ -139,6 +150,16 @@ extension PickServerViewController {
}
.store(in: &disposeBag)
isAuthenticating
.receive(on: DispatchQueue.main)
.sink { [weak self] loading in
if loading {
self?.nextStepButton.showLoading()
} else {
self?.nextStepButton.stopLoading()
}
}
.store(in: &disposeBag)
viewModel.fetchAllServers()
}
@ -151,15 +172,16 @@ extension PickServerViewController {
@objc
private func nextStepButtonDidClicked(_ sender: UIButton) {
switch viewModel.mode {
case .SignIn:
case .signIn:
doSignIn()
case .SignUp:
case .signUp:
doSignUp()
}
}
private func doSignIn() {
guard let server = viewModel.selectedServer.value else { return }
isAuthenticating.send(true)
context.apiService.createApplication(domain: server.domain)
.tryMap { response -> PickServerViewModel.AuthenticateInfo in
let application = response.value
@ -171,7 +193,7 @@ extension PickServerViewController {
.receive(on: DispatchQueue.main)
.sink { [weak self] completion in
guard let self = self else { return }
// self.viewModel.isAuthenticating.value = false
self.isAuthenticating.send(false)
switch completion {
case .failure(let error):
@ -199,7 +221,7 @@ extension PickServerViewController {
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
isAuthenticating.send(true)
context.apiService.instance(domain: server.domain)
.compactMap { [weak self] response -> AnyPublisher<PickServerViewModel.SignUpResponseFirst, Error>? in
@ -235,7 +257,7 @@ extension PickServerViewController {
.receive(on: DispatchQueue.main)
.sink { [weak self] completion in
guard let self = self else { return }
// self.viewModel.isRegistering.value = false
self.isAuthenticating.send(false)
switch completion {
case .failure(let error):
@ -256,3 +278,140 @@ extension PickServerViewController {
.store(in: &disposeBag)
}
}
extension PickServerViewController: UITableViewDelegate {
func tableView(_ tableView: UITableView, heightForHeaderInSection section: Int) -> CGFloat {
let category = Section.allCases[section]
switch category {
case .title:
return 20
case .categories:
// Since category view has a blur shadow effect, its height need to be large than the actual height,
// Thus we reduce the section header's height by 10, and make the category cell height 60+20(10 inset for top and bottom)
return 10
case .search:
// Same reason as above
return 10
case .serverList:
// Header with 1 height as the separator
return 1
}
}
func tableView(_ tableView: UITableView, willSelectRowAt indexPath: IndexPath) -> IndexPath? {
if tableView.indexPathForSelectedRow == indexPath {
tableView.deselectRow(at: indexPath, animated: false)
viewModel.selectedServer.send(nil)
return nil
}
return indexPath
}
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
tableView.selectRow(at: indexPath, animated: false, scrollPosition: .none)
viewModel.selectedServer.send(viewModel.searchedServers.value[indexPath.row])
}
func tableView(_ tableView: UITableView, didDeselectRowAt indexPath: IndexPath) {
tableView.deselectRow(at: indexPath, animated: false)
viewModel.selectedServer.send(nil)
}
}
extension PickServerViewController: UITableViewDataSource {
func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? {
return UIView()
}
func numberOfSections(in tableView: UITableView) -> Int {
return Self.Section.allCases.count
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
let section = Self.Section.allCases[section]
switch section {
case .title,
.categories,
.search:
return 1
case .serverList:
return viewModel.searchedServers.value.count
}
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let section = Self.Section.allCases[indexPath.section]
switch section {
case .title:
let cell = tableView.dequeueReusableCell(withIdentifier: String(describing: PickServerTitleCell.self), for: indexPath) as! PickServerTitleCell
return cell
case .categories:
let cell = tableView.dequeueReusableCell(withIdentifier: String(describing: PickServerCategoriesCell.self), for: indexPath) as! PickServerCategoriesCell
cell.dataSource = self
cell.delegate = self
return cell
case .search:
let cell = tableView.dequeueReusableCell(withIdentifier: String(describing: PickServerSearchCell.self), for: indexPath) as! PickServerSearchCell
cell.delegate = self
return cell
case .serverList:
let cell = tableView.dequeueReusableCell(withIdentifier: String(describing: PickServerCell.self), for: indexPath) as! PickServerCell
let server = viewModel.searchedServers.value[indexPath.row]
cell.server = server
if expandServerDomainSet.contains(server.domain) {
cell.mode = .expand
} else {
cell.mode = .collapse
}
if server == viewModel.selectedServer.value {
tableView.selectRow(at: indexPath, animated: false, scrollPosition: .none)
} else {
tableView.deselectRow(at: indexPath, animated: false)
}
cell.delegate = self
return cell
}
}
}
extension PickServerViewController: PickServerCellDelegate {
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) { _ in
if let modeChangeIndex = self.viewModel.searchedServers.value.firstIndex(where: { $0 == server }) {
self.tableView.scrollToRow(at: IndexPath(row: modeChangeIndex, section: 3), at: .bottom, animated: true)
}
}
}
}
extension PickServerViewController: PickServerSearchCellDelegate {
func pickServerSearchCell(didChange searchText: String?) {
viewModel.searchText.send(searchText)
}
}
extension PickServerViewController: PickServerCategoriesDataSource, PickServerCategoriesDelegate {
func numberOfCategories() -> Int {
return viewModel.categories.count
}
func category(at index: Int) -> PickServerViewModel.Category {
return viewModel.categories[index]
}
func selectedIndex() -> Int {
return viewModel.selectCategoryIndex.value
}
func pickServerCategoriesCell(didSelect index: Int) {
return viewModel.selectCategoryIndex.send(index)
}
}

View File

@ -13,28 +13,21 @@ import CoreDataStack
class PickServerViewModel: NSObject {
enum PickServerMode {
case SignUp
case SignIn
}
enum Section: CaseIterable {
case title
case categories
case search
case serverList
case signUp
case signIn
}
enum Category {
// `All` means search for all categories
case All
// `Some` means search for specific category
case Some(Mastodon.Entity.Category)
// `all` means search for all categories
case all
// `some` means search for specific category
case some(Mastodon.Entity.Category)
var title: String {
switch self {
case .All:
case .all:
return L10n.Scene.ServerPicker.Button.Category.all
case .Some(let masCategory):
case .some(let masCategory):
// TODO: Use emoji as placeholders
switch masCategory.category {
case .academia:
@ -87,7 +80,7 @@ class PickServerViewModel: NSObject {
weak var tableView: UITableView?
private var expandServerDomainSet = Set<String>()
// private var expandServerDomainSet = Set<String>()
var mastodonPinBasedAuthenticationViewController: UIViewController?
@ -101,8 +94,8 @@ class PickServerViewModel: NSObject {
private func configure() {
let masCategories = context.apiService.stubCategories()
categories.append(.All)
categories.append(contentsOf: masCategories.map { Category.Some($0) })
categories.append(.all)
categories.append(contentsOf: masCategories.map { Category.some($0) })
Publishers.CombineLatest3(
selectCategoryIndex,
@ -148,8 +141,8 @@ class PickServerViewModel: NSObject {
func fetchAllServers() {
context.apiService.servers(language: nil, category: nil)
.sink { error in
print("11")
.sink { completion in
// TODO: Add a reload button when fails to fetch servers initially
} receiveValue: { [weak self] result in
self?.allServers.send(result.value)
}
@ -162,9 +155,9 @@ class PickServerViewModel: NSObject {
// 1. Filter the category
.filter {
switch category {
case .All:
case .all:
return true
case .Some(let masCategory):
case .some(let masCategory):
return $0.category.caseInsensitiveCompare(masCategory.category.rawValue) == .orderedSame
}
}
@ -179,140 +172,6 @@ class PickServerViewModel: NSObject {
}
}
extension PickServerViewModel: UITableViewDelegate {
func tableView(_ tableView: UITableView, heightForHeaderInSection section: Int) -> CGFloat {
let category = Section.allCases[section]
switch category {
case .title:
return 20
case .categories:
// Since category view has a blur shadow effect, its height need to be large than the actual height,
// Thus we reduce the section header's height by 10, and make the category cell height 60+20(10 inset for top and bottom)
return 10
case .search:
// Same reason as above
return 10
case .serverList:
// Header with 1 height as the separator
return 1
}
}
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 {
func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? {
return UIView()
}
func numberOfSections(in tableView: UITableView) -> Int {
return Self.Section.allCases.count
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
let section = Self.Section.allCases[section]
switch section {
case .title,
.categories,
.search:
return 1
case .serverList:
return searchedServers.value.count
}
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let section = Self.Section.allCases[indexPath.section]
switch section {
case .title:
let cell = tableView.dequeueReusableCell(withIdentifier: String(describing: PickServerTitleCell.self), for: indexPath) as! PickServerTitleCell
return cell
case .categories:
let cell = tableView.dequeueReusableCell(withIdentifier: String(describing: PickServerCategoriesCell.self), for: indexPath) as! PickServerCategoriesCell
cell.dataSource = self
cell.delegate = self
return cell
case .search:
let cell = tableView.dequeueReusableCell(withIdentifier: String(describing: PickServerSearchCell.self), for: indexPath) as! PickServerSearchCell
cell.delegate = self
return cell
case .serverList:
let cell = tableView.dequeueReusableCell(withIdentifier: String(describing: PickServerCell.self), for: indexPath) as! PickServerCell
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
}
}
}
extension PickServerViewModel: PickServerCategoriesDataSource, PickServerCategoriesDelegate {
func numberOfCategories() -> Int {
return categories.count
}
func category(at index: Int) -> Category {
return categories[index]
}
func selectedIndex() -> Int {
return selectCategoryIndex.value
}
func pickServerCategoriesCell(didSelect index: Int) {
selectCategoryIndex.send(index)
}
}
extension PickServerViewModel: PickServerSearchCellDelegate {
func pickServerSearchCell(didChange searchText: String?) {
self.searchText.send(searchText)
}
}
extension PickServerViewModel: PickServerCellDelegate {
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)
}
}
// MARK: - SignIn methods & structs
extension PickServerViewModel {
enum AuthenticationError: Error, LocalizedError {

View File

@ -83,8 +83,8 @@ class PickServerCell: UITableViewCell {
private var expandButton: UIButton = {
let button = UIButton(type: .custom)
button.setTitle(L10n.Scene.ServerPicker.Button.seeMore, for: .normal)
button.setTitle(L10n.Scene.ServerPicker.Button.seeLess, for: .selected)
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)
button.titleLabel?.font = .preferredFont(forTextStyle: .footnote)
button.translatesAutoresizingMaskIntoConstraints = false
@ -217,7 +217,7 @@ extension PickServerCell {
let expandButtonTopConstraintInCollapse = expandButton.topAnchor.constraint(equalTo: descriptionLabel.lastBaselineAnchor, constant: 12)
collapseConstraints.append(expandButtonTopConstraintInCollapse)
let expandButtonTopConstraintInExpand = expandButton.topAnchor.constraint(equalTo: expandBox.bottomAnchor, constant: 8)
let expandButtonTopConstraintInExpand = expandButton.topAnchor.constraint(equalTo: expandBox.bottomAnchor, constant: 8).priority(.defaultHigh)
expandConstraints.append(expandButtonTopConstraintInExpand)
NSLayoutConstraint.activate([
@ -255,7 +255,7 @@ extension PickServerCell {
thumbImageView.leadingAnchor.constraint(equalTo: expandBox.leadingAnchor),
expandBox.trailingAnchor.constraint(equalTo: thumbImageView.trailingAnchor),
thumbImageView.topAnchor.constraint(equalTo: expandBox.topAnchor),
thumbImageView.heightAnchor.constraint(equalTo: thumbImageView.widthAnchor, multiplier: 151.0 / 303.0),
thumbImageView.heightAnchor.constraint(equalTo: thumbImageView.widthAnchor, multiplier: 151.0 / 303.0).priority(.defaultHigh),
infoStackView.leadingAnchor.constraint(equalTo: expandBox.leadingAnchor),
expandBox.trailingAnchor.constraint(equalTo: infoStackView.trailingAnchor),

View File

@ -74,9 +74,9 @@ extension PickServerCategoryView {
guard let category = category else { return }
titleLabel.text = category.title
switch category {
case .All:
case .all:
titleLabel.font = UIFont.systemFont(ofSize: 17)
case .Some:
case .some:
titleLabel.font = UIFont.systemFont(ofSize: 28)
}
}
@ -85,13 +85,13 @@ extension PickServerCategoryView {
if selected {
bgView.backgroundColor = Asset.Colors.lightBrandBlue.color
bgView.applyShadow(color: Asset.Colors.lightBrandBlue.color, alpha: 1, x: 0, y: 0, blur: 4.0)
if case .All = category {
if case .all = category {
titleLabel.textColor = Asset.Colors.lightWhite.color
}
} else {
bgView.backgroundColor = Asset.Colors.lightWhite.color
bgView.applyShadow(color: Asset.Colors.lightBrandBlue.color, alpha: 0, x: 0, y: 0, blur: 0.0)
if case .All = category {
if case .all = category {
titleLabel.textColor = Asset.Colors.lightBrandBlue.color
}
}

View File

@ -8,6 +8,18 @@
import UIKit
class PrimaryActionButton: UIButton {
var isLoading: Bool = false
lazy var activityIndicator: UIActivityIndicatorView = {
let indicator = UIActivityIndicatorView(style: .medium)
indicator.hidesWhenStopped = true
indicator.translatesAutoresizingMaskIntoConstraints = false
return indicator
}()
private var originalButtonTitle: String?
override init(frame: CGRect) {
super.init(frame: frame)
_init()
@ -17,6 +29,31 @@ class PrimaryActionButton: UIButton {
super.init(coder: coder)
_init()
}
func showLoading() {
guard !isLoading else { return }
isEnabled = false
isLoading = true
originalButtonTitle = title(for: .disabled)
self.setTitle("", for: .disabled)
addSubview(activityIndicator)
NSLayoutConstraint.activate([
activityIndicator.centerXAnchor.constraint(equalTo: self.centerXAnchor),
activityIndicator.centerYAnchor.constraint(equalTo: self.centerYAnchor),
])
activityIndicator.startAnimating()
}
func stopLoading() {
guard isLoading else { return }
isLoading = false
if activityIndicator.superview == self {
activityIndicator.removeFromSuperview()
}
isEnabled = true
self.setTitle(originalButtonTitle, for: .disabled)
}
}
extension PrimaryActionButton {

View File

@ -102,11 +102,11 @@ extension WelcomeViewController {
extension WelcomeViewController {
@objc
private func signUpButtonDidClicked(_ sender: UIButton) {
coordinator.present(scene: .pickServer(viewMode: PickServerViewModel(context: context, mode: .SignUp)), from: self, transition: .show)
coordinator.present(scene: .pickServer(viewMode: PickServerViewModel(context: context, mode: .signUp)), from: self, transition: .show)
}
@objc
private func signInButtonDidClicked(_ sender: UIButton) {
coordinator.present(scene: .pickServer(viewMode: PickServerViewModel(context: context, mode: .SignIn)), from: self, transition: .show)
coordinator.present(scene: .pickServer(viewMode: PickServerViewModel(context: context, mode: .signIn)), from: self, transition: .show)
}
}