Merge pull request #23 from tootsuite/feat/serverPicker
Feat/server picker
This commit is contained in:
commit
2498254c38
|
@ -0,0 +1,95 @@
|
|||
{
|
||||
"common": {
|
||||
"alerts": {},
|
||||
"controls": {
|
||||
"actions": {
|
||||
"add": "Add",
|
||||
"remove": "Remove",
|
||||
"edit": "Edit",
|
||||
"save": "Save",
|
||||
"ok": "OK",
|
||||
"confirm": "Confirm",
|
||||
"continue": "Continue",
|
||||
"cancel": "Cancel",
|
||||
"take_photo": "Take photo",
|
||||
"save_photo": "Save photo",
|
||||
"sign_in": "Sign in",
|
||||
"sign_up": "Sign up",
|
||||
"see_more": "See More",
|
||||
"preview": "Preview",
|
||||
"open_in_safari": "Open in Safari"
|
||||
},
|
||||
"status": {
|
||||
"user_boosted": "%s boosted",
|
||||
"content_warning": "content warning",
|
||||
"show_post": "Show Post"
|
||||
},
|
||||
"timeline": {
|
||||
"load_more": "Load More"
|
||||
}
|
||||
},
|
||||
"countable": {
|
||||
"photo": {
|
||||
"single": "photo",
|
||||
"multiple": "photos"
|
||||
}
|
||||
}
|
||||
},
|
||||
"scene": {
|
||||
"welcome": {
|
||||
"slogan": "Social networking\nback in your hands."
|
||||
},
|
||||
"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..."
|
||||
}
|
||||
},
|
||||
"register": {
|
||||
"title": "Tell us about you.",
|
||||
"input": {
|
||||
"username": {
|
||||
"placeholder": "username",
|
||||
"duplicate_prompt": "This username is taken."
|
||||
},
|
||||
"display_name": {
|
||||
"placeholder": "display name"
|
||||
},
|
||||
"email": {
|
||||
"placeholder": "email"
|
||||
},
|
||||
"password": {
|
||||
"placeholder": "password",
|
||||
"prompt": "Your password needs at least:",
|
||||
"prompt_eight_characters": "Eight characters"
|
||||
}
|
||||
}
|
||||
},
|
||||
"server_rules": {
|
||||
"title": "Some ground rules.",
|
||||
"subtitle": "These rules are set by the admins of %s.",
|
||||
"prompt": "By continuing, you're subject to the terms of service and privacy policy for %s.",
|
||||
"button": {
|
||||
"confirm": "I Agree"
|
||||
}
|
||||
},
|
||||
"home_timeline": {
|
||||
"title": "Home"
|
||||
},
|
||||
"public_timeline": {
|
||||
"title": "Public"
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,46 @@
|
|||
"Common.Controls.Actions.Add" = "Add";
|
||||
"Common.Controls.Actions.Cancel" = "Cancel";
|
||||
"Common.Controls.Actions.Confirm" = "Confirm";
|
||||
"Common.Controls.Actions.Continue" = "Continue";
|
||||
"Common.Controls.Actions.Edit" = "Edit";
|
||||
"Common.Controls.Actions.Ok" = "OK";
|
||||
"Common.Controls.Actions.OpenInSafari" = "Open in Safari";
|
||||
"Common.Controls.Actions.Preview" = "Preview";
|
||||
"Common.Controls.Actions.Remove" = "Remove";
|
||||
"Common.Controls.Actions.Save" = "Save";
|
||||
"Common.Controls.Actions.SavePhoto" = "Save photo";
|
||||
"Common.Controls.Actions.SeeMore" = "See More";
|
||||
"Common.Controls.Actions.SignIn" = "Sign in";
|
||||
"Common.Controls.Actions.SignUp" = "Sign up";
|
||||
"Common.Controls.Actions.TakePhoto" = "Take photo";
|
||||
"Common.Controls.Status.ContentWarning" = "content warning";
|
||||
"Common.Controls.Status.ShowPost" = "Show Post";
|
||||
"Common.Controls.Status.UserBoosted" = "%@ boosted";
|
||||
"Common.Controls.Timeline.LoadMore" = "Load More";
|
||||
"Common.Countable.Photo.Multiple" = "photos";
|
||||
"Common.Countable.Photo.Single" = "photo";
|
||||
"Scene.HomeTimeline.Title" = "Home";
|
||||
"Scene.PublicTimeline.Title" = "Public";
|
||||
"Scene.Register.Input.DisplayName.Placeholder" = "display name";
|
||||
"Scene.Register.Input.Email.Placeholder" = "email";
|
||||
"Scene.Register.Input.Password.Placeholder" = "password";
|
||||
"Scene.Register.Input.Password.Prompt" = "Your password needs at least:";
|
||||
"Scene.Register.Input.Password.PromptEightCharacters" = "Eight characters";
|
||||
"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";
|
||||
"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 %@.";
|
||||
"Scene.ServerRules.Title" = "Some ground rules.";
|
||||
"Scene.Welcome.Slogan" = "Social networking
|
||||
back in your hands.";
|
|
@ -51,6 +51,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..."
|
||||
}
|
||||
|
|
|
@ -11,6 +11,13 @@
|
|||
0FAA101225E105390017CCDE /* PrimaryActionButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0FAA101125E105390017CCDE /* PrimaryActionButton.swift */; };
|
||||
0FAA101C25E10E760017CCDE /* UIFont.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0FAA101B25E10E760017CCDE /* UIFont.swift */; };
|
||||
0FAA102725E1126A0017CCDE /* PickServerViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0FAA102625E1126A0017CCDE /* PickServerViewController.swift */; };
|
||||
0FB3D2F725E4C24D00AAD544 /* PickServerViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0FB3D2F625E4C24D00AAD544 /* PickServerViewModel.swift */; };
|
||||
0FB3D2FE25E4CB6400AAD544 /* PickServerTitleCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0FB3D2FD25E4CB6400AAD544 /* PickServerTitleCell.swift */; };
|
||||
0FB3D30825E524C600AAD544 /* PickServerCategoriesCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0FB3D30725E524C600AAD544 /* PickServerCategoriesCell.swift */; };
|
||||
0FB3D30F25E525CD00AAD544 /* PickServerCategoryView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0FB3D30E25E525CD00AAD544 /* PickServerCategoryView.swift */; };
|
||||
0FB3D31E25E534C700AAD544 /* PickServerCategoryCollectionViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0FB3D31D25E534C700AAD544 /* PickServerCategoryCollectionViewCell.swift */; };
|
||||
0FB3D33225E5F50E00AAD544 /* PickServerSearchCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0FB3D33125E5F50E00AAD544 /* PickServerSearchCell.swift */; };
|
||||
0FB3D33825E6401400AAD544 /* PickServerCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0FB3D33725E6401400AAD544 /* PickServerCell.swift */; };
|
||||
18BC7629F65E6DB12CB8416D /* Pods_Mastodon_MastodonUITests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 3C030226D3C73DCC23D67452 /* Pods_Mastodon_MastodonUITests.framework */; };
|
||||
2D04F42525C255B9003F936F /* APIService+PublicTimeline.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D04F42425C255B9003F936F /* APIService+PublicTimeline.swift */; };
|
||||
2D152A8C25C295CC009AA50C /* StatusView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D152A8B25C295CC009AA50C /* StatusView.swift */; };
|
||||
|
@ -211,6 +218,13 @@
|
|||
0FAA101125E105390017CCDE /* PrimaryActionButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PrimaryActionButton.swift; sourceTree = "<group>"; };
|
||||
0FAA101B25E10E760017CCDE /* UIFont.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UIFont.swift; sourceTree = "<group>"; };
|
||||
0FAA102625E1126A0017CCDE /* PickServerViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PickServerViewController.swift; sourceTree = "<group>"; };
|
||||
0FB3D2F625E4C24D00AAD544 /* PickServerViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PickServerViewModel.swift; sourceTree = "<group>"; };
|
||||
0FB3D2FD25E4CB6400AAD544 /* PickServerTitleCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PickServerTitleCell.swift; sourceTree = "<group>"; };
|
||||
0FB3D30725E524C600AAD544 /* PickServerCategoriesCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PickServerCategoriesCell.swift; sourceTree = "<group>"; };
|
||||
0FB3D30E25E525CD00AAD544 /* PickServerCategoryView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PickServerCategoryView.swift; sourceTree = "<group>"; };
|
||||
0FB3D31D25E534C700AAD544 /* PickServerCategoryCollectionViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PickServerCategoryCollectionViewCell.swift; sourceTree = "<group>"; };
|
||||
0FB3D33125E5F50E00AAD544 /* PickServerSearchCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PickServerSearchCell.swift; sourceTree = "<group>"; };
|
||||
0FB3D33725E6401400AAD544 /* PickServerCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PickServerCell.swift; sourceTree = "<group>"; };
|
||||
2D04F42425C255B9003F936F /* APIService+PublicTimeline.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "APIService+PublicTimeline.swift"; sourceTree = "<group>"; };
|
||||
2D152A8B25C295CC009AA50C /* StatusView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatusView.swift; sourceTree = "<group>"; };
|
||||
2D152A9125C2980C009AA50C /* UIFont.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UIFont.swift; sourceTree = "<group>"; };
|
||||
|
@ -428,11 +442,42 @@
|
|||
0FAA102525E1125D0017CCDE /* PickServer */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
0FB3D31825E525DE00AAD544 /* CollectionViewCell */,
|
||||
0FB3D30D25E525C000AAD544 /* View */,
|
||||
0FB3D2FC25E4CB4B00AAD544 /* TableViewCell */,
|
||||
0FAA102625E1126A0017CCDE /* PickServerViewController.swift */,
|
||||
0FB3D2F625E4C24D00AAD544 /* PickServerViewModel.swift */,
|
||||
);
|
||||
path = PickServer;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
0FB3D2FC25E4CB4B00AAD544 /* TableViewCell */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
0FB3D2FD25E4CB6400AAD544 /* PickServerTitleCell.swift */,
|
||||
0FB3D30725E524C600AAD544 /* PickServerCategoriesCell.swift */,
|
||||
0FB3D33125E5F50E00AAD544 /* PickServerSearchCell.swift */,
|
||||
0FB3D33725E6401400AAD544 /* PickServerCell.swift */,
|
||||
);
|
||||
path = TableViewCell;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
0FB3D30D25E525C000AAD544 /* View */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
0FB3D30E25E525CD00AAD544 /* PickServerCategoryView.swift */,
|
||||
);
|
||||
path = View;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
0FB3D31825E525DE00AAD544 /* CollectionViewCell */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
0FB3D31D25E534C700AAD544 /* PickServerCategoryCollectionViewCell.swift */,
|
||||
);
|
||||
path = CollectionViewCell;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
1EBA4F56E920856A3FC84ACB /* Pods */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
|
@ -1367,8 +1412,10 @@
|
|||
2D59819B25E4A581000FB903 /* MastodonConfirmEmailViewController.swift in Sources */,
|
||||
DB45FB1D25CA9D23005A8AC7 /* APIService+HomeTimeline.swift in Sources */,
|
||||
2D7631B325C159F700929FB9 /* Item.swift in Sources */,
|
||||
0FB3D2F725E4C24D00AAD544 /* PickServerViewModel.swift in Sources */,
|
||||
2D61335E25C1894B00CAE157 /* APIService.swift in Sources */,
|
||||
2D38F1F725CD47AC00561493 /* HomeTimelineViewModel+LoadOldestState.swift in Sources */,
|
||||
0FB3D33225E5F50E00AAD544 /* PickServerSearchCell.swift in Sources */,
|
||||
2D5A3D3825CF8D9F002347D6 /* ScrollViewContainer.swift in Sources */,
|
||||
0FAA101225E105390017CCDE /* PrimaryActionButton.swift in Sources */,
|
||||
DB8AF53025C13561002E6C99 /* AppContext.swift in Sources */,
|
||||
|
@ -1403,6 +1450,7 @@
|
|||
2D82B9FF25E7863200E36F0F /* OnboardingViewControllerAppearance.swift in Sources */,
|
||||
2D38F1E525CD46C100561493 /* HomeTimelineViewModel.swift in Sources */,
|
||||
2D76316B25C14D4C00929FB9 /* PublicTimelineViewModel.swift in Sources */,
|
||||
0FB3D2FE25E4CB6400AAD544 /* PickServerTitleCell.swift in Sources */,
|
||||
2D38F1DF25CD46A400561493 /* HomeTimelineViewController+StatusProvider.swift in Sources */,
|
||||
2D46975E25C2A54100CF4AA9 /* NSLayoutConstraint.swift in Sources */,
|
||||
2D45E5BF25C9549700A6D639 /* PublicTimelineViewModel+State.swift in Sources */,
|
||||
|
@ -1418,6 +1466,8 @@
|
|||
DB8AF55025C13703002E6C99 /* MainTabBarController.swift in Sources */,
|
||||
DB9D6BE925E4F5340051B173 /* SearchViewController.swift in Sources */,
|
||||
2D38F1C625CD37F400561493 /* ContentOffsetAdjustableTimelineViewControllerDelegate.swift in Sources */,
|
||||
0FB3D30F25E525CD00AAD544 /* PickServerCategoryView.swift in Sources */,
|
||||
0FB3D33825E6401400AAD544 /* PickServerCell.swift in Sources */,
|
||||
2D364F7825E66D8300204FDC /* MastodonResendEmailViewModel.swift in Sources */,
|
||||
DB8AF54525C13647002E6C99 /* NeedsDependency.swift in Sources */,
|
||||
DB9D6BF825E4F5690051B173 /* NotificationViewController.swift in Sources */,
|
||||
|
@ -1461,7 +1511,10 @@
|
|||
2D32EAAC25CB96DC00C9ED86 /* TimelineMiddleLoaderTableViewCell.swift in Sources */,
|
||||
2D5A3D6225CFD9CB002347D6 /* HomeTimelineViewController+DebugAction.swift in Sources */,
|
||||
DB9D6BFF25E4F5940051B173 /* ProfileViewController.swift in Sources */,
|
||||
0FB3D30825E524C600AAD544 /* PickServerCategoriesCell.swift in Sources */,
|
||||
2D38F1FE25CD481700561493 /* StatusProvider.swift in Sources */,
|
||||
2D5A3D1125CF87AA002347D6 /* AvatarBarButtonItem.swift in Sources */,
|
||||
0FB3D31E25E534C700AAD544 /* PickServerCategoryCollectionViewCell.swift in Sources */,
|
||||
DB45FB0F25CA87D0005A8AC7 /* AuthenticationService.swift in Sources */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
|
|
|
@ -39,6 +39,7 @@ extension SceneCoordinator {
|
|||
|
||||
enum Scene {
|
||||
case welcome
|
||||
case pickServer(viewMode: PickServerViewModel)
|
||||
case authentication(viewModel: AuthenticationViewModel)
|
||||
case mastodonPinBasedAuthentication(viewModel: MastodonPinBasedAuthenticationViewModel)
|
||||
case mastodonRegister(viewModel: MastodonRegisterViewModel)
|
||||
|
@ -142,6 +143,10 @@ private extension SceneCoordinator {
|
|||
case .welcome:
|
||||
let _viewController = WelcomeViewController()
|
||||
viewController = _viewController
|
||||
case .pickServer(let viewModel):
|
||||
let _viewController = PickServerViewController()
|
||||
_viewController.viewModel = viewModel
|
||||
viewController = _viewController
|
||||
case .authentication(let viewModel):
|
||||
let _viewController = AuthenticationViewController()
|
||||
_viewController.viewModel = viewModel
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
// UIFont.swift
|
||||
// Mastodon
|
||||
//
|
||||
// Created by 高原 on 2021/2/20.
|
||||
// Created by BradGao on 2021/2/20.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
|
|
|
@ -31,4 +31,28 @@ extension UIView {
|
|||
layer.cornerCurve = .continuous
|
||||
return self
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
func applyShadow(
|
||||
color: UIColor,
|
||||
alpha: Float,
|
||||
x: CGFloat,
|
||||
y: CGFloat,
|
||||
blur: CGFloat,
|
||||
spread: CGFloat = 0) -> Self
|
||||
{
|
||||
layer.masksToBounds = false
|
||||
layer.shadowColor = color.cgColor
|
||||
layer.shadowOpacity = alpha
|
||||
layer.shadowOffset = CGSize(width: x, height: y)
|
||||
layer.shadowRadius = blur / 2.0
|
||||
if spread == 0 {
|
||||
layer.shadowPath = nil
|
||||
} else {
|
||||
let dx = -spread
|
||||
let rect = bounds.insetBy(dx: dx, dy: dx)
|
||||
layer.shadowPath = UIBezierPath(rect: rect).cgPath
|
||||
}
|
||||
return self
|
||||
}
|
||||
}
|
||||
|
|
|
@ -160,10 +160,28 @@ internal enum L10n {
|
|||
internal enum ServerPicker {
|
||||
/// 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 More
|
||||
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")
|
||||
}
|
||||
}
|
||||
internal enum Input {
|
||||
/// Find a server or join your own...
|
||||
internal static let placeholder = L10n.tr("Localizable", "Scene.ServerPicker.Input.Placeholder")
|
||||
}
|
||||
internal enum Label {
|
||||
/// CATEGORY
|
||||
internal static let category = L10n.tr("Localizable", "Scene.ServerPicker.Label.Category")
|
||||
/// LANGUAGE
|
||||
internal static let language = L10n.tr("Localizable", "Scene.ServerPicker.Label.Language")
|
||||
/// USERS
|
||||
internal static let users = L10n.tr("Localizable", "Scene.ServerPicker.Label.Users")
|
||||
}
|
||||
}
|
||||
internal enum ServerRules {
|
||||
/// By continuing, you're subject to the terms of service and privacy policy for %@.
|
||||
|
|
|
@ -47,7 +47,13 @@ tap the link to confirm your account.";
|
|||
"Scene.Register.Input.Username.Placeholder" = "username";
|
||||
"Scene.Register.Success" = "Success";
|
||||
"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";
|
||||
|
|
|
@ -0,0 +1,52 @@
|
|||
//
|
||||
// PickServerCategoryCollectionViewCell.swift
|
||||
// Mastodon
|
||||
//
|
||||
// Created by BradGao on 2021/2/23.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
|
||||
class PickServerCategoryCollectionViewCell: UICollectionViewCell {
|
||||
|
||||
var category: PickServerViewModel.Category? {
|
||||
didSet {
|
||||
categoryView.category = category
|
||||
}
|
||||
}
|
||||
|
||||
var categoryView: PickServerCategoryView = {
|
||||
let view = PickServerCategoryView()
|
||||
view.translatesAutoresizingMaskIntoConstraints = false
|
||||
return view
|
||||
}()
|
||||
|
||||
override var isSelected: Bool {
|
||||
didSet {
|
||||
categoryView.selected = isSelected
|
||||
}
|
||||
}
|
||||
|
||||
override init(frame: CGRect) {
|
||||
super.init(frame: .zero)
|
||||
configure()
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
super.init(coder: coder)
|
||||
configure()
|
||||
}
|
||||
}
|
||||
|
||||
extension PickServerCategoryCollectionViewCell {
|
||||
private func configure() {
|
||||
contentView.addSubview(categoryView)
|
||||
|
||||
NSLayoutConstraint.activate([
|
||||
categoryView.leadingAnchor.constraint(equalTo: contentView.leadingAnchor),
|
||||
categoryView.trailingAnchor.constraint(equalTo: contentView.trailingAnchor),
|
||||
categoryView.topAnchor.constraint(equalTo: contentView.topAnchor, constant: 10),
|
||||
contentView.bottomAnchor.constraint(equalTo: categoryView.bottomAnchor, constant: 10),
|
||||
])
|
||||
}
|
||||
}
|
|
@ -2,20 +2,418 @@
|
|||
// PickServerViewController.swift
|
||||
// Mastodon
|
||||
//
|
||||
// Created by 高原 on 2021/2/20.
|
||||
// Created by BradGao on 2021/2/20.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
import Combine
|
||||
import OSLog
|
||||
import MastodonSDK
|
||||
|
||||
class PickServerViewController: UIViewController {
|
||||
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
|
||||
final class PickServerViewController: UIViewController, NeedsDependency {
|
||||
|
||||
private var disposeBag = Set<AnyCancellable>()
|
||||
|
||||
weak var context: AppContext! { willSet { precondition(!isViewLoaded) } }
|
||||
weak var coordinator: SceneCoordinator! { willSet { precondition(!isViewLoaded) } }
|
||||
|
||||
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))
|
||||
tableView.register(PickServerCategoriesCell.self, forCellReuseIdentifier: String(describing: PickServerCategoriesCell.self))
|
||||
tableView.register(PickServerSearchCell.self, forCellReuseIdentifier: String(describing: PickServerSearchCell.self))
|
||||
tableView.register(PickServerCell.self, forCellReuseIdentifier: String(describing: PickServerCell.self))
|
||||
tableView.rowHeight = UITableView.automaticDimension
|
||||
tableView.separatorStyle = .none
|
||||
tableView.backgroundColor = .clear
|
||||
tableView.keyboardDismissMode = .onDrag
|
||||
tableView.translatesAutoresizingMaskIntoConstraints = false
|
||||
|
||||
return tableView
|
||||
}()
|
||||
|
||||
let nextStepButton: PrimaryActionButton = {
|
||||
let button = PrimaryActionButton(type: .system)
|
||||
button.setTitle(L10n.Common.Controls.Actions.signUp, for: .normal)
|
||||
button.translatesAutoresizingMaskIntoConstraints = false
|
||||
return button
|
||||
}()
|
||||
}
|
||||
|
||||
extension PickServerViewController {
|
||||
|
||||
override var preferredStatusBarStyle: UIStatusBarStyle {
|
||||
return .darkContent
|
||||
}
|
||||
|
||||
override func viewDidLoad() {
|
||||
super.viewDidLoad()
|
||||
|
||||
view.backgroundColor = Asset.Colors.Background.onboardingBackground.color
|
||||
|
||||
view.addSubview(nextStepButton)
|
||||
NSLayoutConstraint.activate([
|
||||
nextStepButton.leadingAnchor.constraint(equalTo: view.readableContentGuide.leadingAnchor, constant: 12),
|
||||
view.readableContentGuide.trailingAnchor.constraint(equalTo: nextStepButton.trailingAnchor, constant: 12),
|
||||
view.bottomAnchor.constraint(equalTo: nextStepButton.bottomAnchor, constant: 34),
|
||||
])
|
||||
|
||||
view.addSubview(tableView)
|
||||
NSLayoutConstraint.activate([
|
||||
tableView.topAnchor.constraint(equalTo: view.topAnchor),
|
||||
tableView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
|
||||
tableView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
|
||||
nextStepButton.topAnchor.constraint(equalTo: tableView.bottomAnchor, constant: 7)
|
||||
])
|
||||
|
||||
switch viewModel.mode {
|
||||
case .signIn:
|
||||
nextStepButton.setTitle(L10n.Common.Controls.Actions.signIn, for: .normal)
|
||||
case .signUp:
|
||||
nextStepButton.setTitle(L10n.Common.Controls.Actions.continue, for: .normal)
|
||||
}
|
||||
nextStepButton.addTarget(self, action: #selector(nextStepButtonDidClicked(_:)), for: .touchUpInside)
|
||||
|
||||
// viewModel.tableView = tableView
|
||||
tableView.delegate = self
|
||||
tableView.dataSource = self
|
||||
|
||||
viewModel
|
||||
.searchedServers
|
||||
.receive(on: DispatchQueue.main)
|
||||
.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(for: error, title: "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)
|
||||
|
||||
isAuthenticating
|
||||
.receive(on: DispatchQueue.main)
|
||||
.sink { [weak self] loading in
|
||||
if loading {
|
||||
self?.nextStepButton.showLoading()
|
||||
} else {
|
||||
self?.nextStepButton.stopLoading()
|
||||
}
|
||||
}
|
||||
.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 }
|
||||
isAuthenticating.send(true)
|
||||
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.isAuthenticating.send(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 }
|
||||
isAuthenticating.send(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.isAuthenticating.send(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)
|
||||
}
|
||||
}
|
||||
|
||||
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.beginUpdates()
|
||||
updates()
|
||||
tableView.endUpdates()
|
||||
|
||||
if newMode == .expand, let modeChangeIndex = self.viewModel.searchedServers.value.firstIndex(where: { $0 == server }), self.tableView.indexPathsForVisibleRows?.last?.row == modeChangeIndex {
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,341 @@
|
|||
//
|
||||
// PickServerViewModel.swift
|
||||
// Mastodon
|
||||
//
|
||||
// Created by BradGao on 2021/2/23.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
import OSLog
|
||||
import Combine
|
||||
import MastodonSDK
|
||||
import CoreDataStack
|
||||
|
||||
class PickServerViewModel: NSObject {
|
||||
enum PickServerMode {
|
||||
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)
|
||||
|
||||
var title: String {
|
||||
switch self {
|
||||
case .all:
|
||||
return L10n.Scene.ServerPicker.Button.Category.all
|
||||
case .some(let masCategory):
|
||||
// TODO: Use emoji as placeholders
|
||||
switch masCategory.category {
|
||||
case .academia:
|
||||
return "📚"
|
||||
case .activism:
|
||||
return "✊"
|
||||
case .food:
|
||||
return "🍕"
|
||||
case .furry:
|
||||
return "🦁"
|
||||
case .games:
|
||||
return "🕹"
|
||||
case .general:
|
||||
return "GE"
|
||||
case .journalism:
|
||||
return "📰"
|
||||
case .lgbt:
|
||||
return "🏳️🌈"
|
||||
case .regional:
|
||||
return "📍"
|
||||
case .art:
|
||||
return "🎨"
|
||||
case .music:
|
||||
return "🎼"
|
||||
case .tech:
|
||||
return "📱"
|
||||
case ._other:
|
||||
return "❓"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let mode: PickServerMode
|
||||
let context: AppContext
|
||||
|
||||
var categories = [Category]()
|
||||
let selectCategoryIndex = CurrentValueSubject<Int, Never>(0)
|
||||
|
||||
let searchText = CurrentValueSubject<String?, Never>(nil)
|
||||
|
||||
let allServers = CurrentValueSubject<[Mastodon.Entity.Server], Never>([])
|
||||
let searchedServers = CurrentValueSubject<[Mastodon.Entity.Server], Error>([])
|
||||
|
||||
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
|
||||
super.init()
|
||||
|
||||
configure()
|
||||
}
|
||||
|
||||
private func configure() {
|
||||
let masCategories = context.apiService.stubCategories()
|
||||
categories.append(.all)
|
||||
categories.append(contentsOf: masCategories.map { Category.some($0) })
|
||||
|
||||
Publishers.CombineLatest3(
|
||||
selectCategoryIndex,
|
||||
searchText.debounce(for: .milliseconds(300), scheduler: DispatchQueue.main).removeDuplicates(),
|
||||
allServers
|
||||
)
|
||||
.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(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, let _ = URL(string: "https://\(toSearchText)") {
|
||||
return self.context.apiService.instance(domain: toSearchText)
|
||||
.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(Result.success(searchedServersFromAPI)).eraseToAnyPublisher()
|
||||
}
|
||||
.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)
|
||||
|
||||
|
||||
}
|
||||
|
||||
func fetchAllServers() {
|
||||
context.apiService.servers(language: nil, category: nil)
|
||||
.sink { completion in
|
||||
// TODO: Add a reload button when fails to fetch servers initially
|
||||
} receiveValue: { [weak self] result in
|
||||
self?.allServers.send(result.value)
|
||||
}
|
||||
.store(in: &disposeBag)
|
||||
|
||||
}
|
||||
|
||||
private func searchServersFromAPI(category: Category, searchText: String?, allServers: [Mastodon.Entity.Server]) -> [Mastodon.Entity.Server] {
|
||||
return allServers
|
||||
// 1. Filter the category
|
||||
.filter {
|
||||
switch category {
|
||||
case .all:
|
||||
return true
|
||||
case .some(let masCategory):
|
||||
return $0.category.caseInsensitiveCompare(masCategory.category.rawValue) == .orderedSame
|
||||
}
|
||||
}
|
||||
// 2. Filter the searchText
|
||||
.filter {
|
||||
if let searchText = searchText, !searchText.isEmpty {
|
||||
return $0.domain.lowercased().contains(searchText.lowercased())
|
||||
} else {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 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>
|
||||
}
|
||||
}
|
|
@ -0,0 +1,110 @@
|
|||
//
|
||||
// PickServerCategoriesCell.swift
|
||||
// Mastodon
|
||||
//
|
||||
// Created by BradGao on 2021/2/23.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
import MastodonSDK
|
||||
|
||||
protocol PickServerCategoriesDataSource: class {
|
||||
func numberOfCategories() -> Int
|
||||
func category(at index: Int) -> PickServerViewModel.Category
|
||||
func selectedIndex() -> Int
|
||||
}
|
||||
|
||||
protocol PickServerCategoriesDelegate: class {
|
||||
func pickServerCategoriesCell(didSelect index: Int)
|
||||
}
|
||||
|
||||
final class PickServerCategoriesCell: UITableViewCell {
|
||||
|
||||
weak var dataSource: PickServerCategoriesDataSource!
|
||||
weak var delegate: PickServerCategoriesDelegate!
|
||||
|
||||
let collectionView: UICollectionView = {
|
||||
let flowLayout = UICollectionViewFlowLayout()
|
||||
flowLayout.scrollDirection = .horizontal
|
||||
let view = ControlContainableCollectionView(frame: .zero, collectionViewLayout: flowLayout)
|
||||
view.backgroundColor = .clear
|
||||
view.showsHorizontalScrollIndicator = false
|
||||
view.register(PickServerCategoryCollectionViewCell.self, forCellWithReuseIdentifier: String(describing: PickServerCategoryCollectionViewCell.self))
|
||||
view.showsVerticalScrollIndicator = false
|
||||
view.translatesAutoresizingMaskIntoConstraints = false
|
||||
return view
|
||||
}()
|
||||
|
||||
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
|
||||
super.init(style: style, reuseIdentifier: reuseIdentifier)
|
||||
_init()
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
super.init(coder: coder)
|
||||
_init()
|
||||
}
|
||||
}
|
||||
|
||||
extension PickServerCategoriesCell {
|
||||
|
||||
private func _init() {
|
||||
self.selectionStyle = .none
|
||||
backgroundColor = .clear
|
||||
|
||||
contentView.addSubview(collectionView)
|
||||
NSLayoutConstraint.activate([
|
||||
collectionView.leadingAnchor.constraint(equalTo: contentView.leadingAnchor),
|
||||
collectionView.trailingAnchor.constraint(equalTo: contentView.trailingAnchor),
|
||||
collectionView.topAnchor.constraint(equalTo: contentView.topAnchor),
|
||||
collectionView.bottomAnchor.constraint(equalTo: contentView.bottomAnchor),
|
||||
|
||||
collectionView.heightAnchor.constraint(equalToConstant: 80),
|
||||
])
|
||||
|
||||
collectionView.delegate = self
|
||||
collectionView.dataSource = self
|
||||
}
|
||||
}
|
||||
|
||||
extension PickServerCategoriesCell: UICollectionViewDelegateFlowLayout {
|
||||
func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
|
||||
collectionView.selectItem(at: indexPath, animated: true, scrollPosition: .centeredHorizontally)
|
||||
delegate.pickServerCategoriesCell(didSelect: indexPath.row)
|
||||
}
|
||||
|
||||
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, insetForSectionAt section: Int) -> UIEdgeInsets {
|
||||
return UIEdgeInsets(top: 0, left: 20, bottom: 0, right: 20)
|
||||
}
|
||||
|
||||
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, minimumInteritemSpacingForSectionAt section: Int) -> CGFloat {
|
||||
return 16
|
||||
}
|
||||
|
||||
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
|
||||
return CGSize(width: 60, height: 80)
|
||||
}
|
||||
}
|
||||
|
||||
extension PickServerCategoriesCell: UICollectionViewDataSource {
|
||||
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
|
||||
return dataSource.numberOfCategories()
|
||||
}
|
||||
|
||||
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
|
||||
let category = dataSource.category(at: indexPath.row)
|
||||
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: String(describing: PickServerCategoryCollectionViewCell.self), for: indexPath) as! PickServerCategoryCollectionViewCell
|
||||
cell.category = category
|
||||
|
||||
// Select the default category by default
|
||||
if indexPath.row == dataSource.selectedIndex() {
|
||||
// Use `[]` as the scrollPosition to avoid contentOffset change
|
||||
collectionView.selectItem(at: indexPath, animated: false, scrollPosition: [])
|
||||
cell.isSelected = true
|
||||
}
|
||||
return cell
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,355 @@
|
|||
//
|
||||
// PickServerCell.swift
|
||||
// Mastodon
|
||||
//
|
||||
// Created by BradGao on 2021/2/24.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
import MastodonSDK
|
||||
import Kingfisher
|
||||
|
||||
protocol PickServerCellDelegate: class {
|
||||
func pickServerCell(modeChange server: Mastodon.Entity.Server, newMode: PickServerCell.Mode, updates: (() -> Void))
|
||||
}
|
||||
|
||||
class PickServerCell: UITableViewCell {
|
||||
|
||||
weak var delegate: PickServerCellDelegate?
|
||||
|
||||
enum Mode {
|
||||
case collapse
|
||||
case expand
|
||||
}
|
||||
|
||||
private var bgView: UIView = {
|
||||
let view = UIView()
|
||||
view.backgroundColor = Asset.Colors.lightWhite.color
|
||||
view.translatesAutoresizingMaskIntoConstraints = false
|
||||
return view
|
||||
}()
|
||||
|
||||
private var domainLabel: UILabel = {
|
||||
let label = UILabel()
|
||||
label.font = .preferredFont(forTextStyle: .headline)
|
||||
label.textColor = Asset.Colors.lightDarkGray.color
|
||||
label.adjustsFontForContentSizeCategory = true
|
||||
label.translatesAutoresizingMaskIntoConstraints = false
|
||||
return label
|
||||
}()
|
||||
|
||||
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
|
||||
}()
|
||||
|
||||
private var descriptionLabel: UILabel = {
|
||||
let label = UILabel()
|
||||
label.font = .preferredFont(forTextStyle: .subheadline)
|
||||
label.numberOfLines = 0
|
||||
label.textColor = Asset.Colors.lightDarkGray.color
|
||||
label.adjustsFontForContentSizeCategory = true
|
||||
label.translatesAutoresizingMaskIntoConstraints = false
|
||||
return label
|
||||
}()
|
||||
|
||||
private var thumbImageView: UIImageView = {
|
||||
let imageView = UIImageView()
|
||||
imageView.clipsToBounds = true
|
||||
imageView.contentMode = .scaleAspectFill
|
||||
imageView.translatesAutoresizingMaskIntoConstraints = false
|
||||
return imageView
|
||||
}()
|
||||
|
||||
private var infoStackView: UIStackView = {
|
||||
let stackView = UIStackView()
|
||||
stackView.axis = .horizontal
|
||||
stackView.alignment = .fill
|
||||
stackView.distribution = .fillEqually
|
||||
stackView.translatesAutoresizingMaskIntoConstraints = false
|
||||
return stackView
|
||||
}()
|
||||
|
||||
private var expandBox: UIView = {
|
||||
let view = UIView()
|
||||
view.backgroundColor = .clear
|
||||
view.translatesAutoresizingMaskIntoConstraints = false
|
||||
return view
|
||||
}()
|
||||
|
||||
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.setTitleColor(Asset.Colors.lightBrandBlue.color, for: .normal)
|
||||
button.titleLabel?.font = .preferredFont(forTextStyle: .footnote)
|
||||
button.translatesAutoresizingMaskIntoConstraints = false
|
||||
return button
|
||||
}()
|
||||
|
||||
private var seperator: UIView = {
|
||||
let view = UIView()
|
||||
view.backgroundColor = Asset.Colors.lightBackground.color
|
||||
view.translatesAutoresizingMaskIntoConstraints = false
|
||||
return view
|
||||
}()
|
||||
|
||||
private var langValueLabel: UILabel = {
|
||||
let label = UILabel()
|
||||
label.textColor = Asset.Colors.lightDarkGray.color
|
||||
label.font = UIFontMetrics(forTextStyle: .title2).scaledFont(for: UIFont.systemFont(ofSize: 22, weight: .semibold))
|
||||
label.textAlignment = .center
|
||||
label.adjustsFontForContentSizeCategory = true
|
||||
label.translatesAutoresizingMaskIntoConstraints = false
|
||||
return label
|
||||
}()
|
||||
|
||||
private var usersValueLabel: UILabel = {
|
||||
let label = UILabel()
|
||||
label.textColor = Asset.Colors.lightDarkGray.color
|
||||
label.font = UIFontMetrics(forTextStyle: .title2).scaledFont(for: UIFont.systemFont(ofSize: 22, weight: .semibold))
|
||||
label.textAlignment = .center
|
||||
label.adjustsFontForContentSizeCategory = true
|
||||
label.translatesAutoresizingMaskIntoConstraints = false
|
||||
return label
|
||||
}()
|
||||
|
||||
private var categoryValueLabel: UILabel = {
|
||||
let label = UILabel()
|
||||
label.textColor = Asset.Colors.lightDarkGray.color
|
||||
label.font = UIFontMetrics(forTextStyle: .title2).scaledFont(for: UIFont.systemFont(ofSize: 22, weight: .semibold))
|
||||
label.textAlignment = .center
|
||||
label.adjustsFontForContentSizeCategory = true
|
||||
label.translatesAutoresizingMaskIntoConstraints = false
|
||||
return label
|
||||
}()
|
||||
|
||||
private var langTitleLabel: UILabel = {
|
||||
let label = UILabel()
|
||||
label.textColor = Asset.Colors.lightDarkGray.color
|
||||
label.font = .preferredFont(forTextStyle: .caption2)
|
||||
label.text = L10n.Scene.ServerPicker.Label.language
|
||||
label.textAlignment = .center
|
||||
label.adjustsFontForContentSizeCategory = true
|
||||
label.translatesAutoresizingMaskIntoConstraints = false
|
||||
return label
|
||||
}()
|
||||
|
||||
private var usersTitleLabel: UILabel = {
|
||||
let label = UILabel()
|
||||
label.textColor = Asset.Colors.lightDarkGray.color
|
||||
label.font = .preferredFont(forTextStyle: .caption2)
|
||||
label.text = L10n.Scene.ServerPicker.Label.users
|
||||
label.textAlignment = .center
|
||||
label.adjustsFontForContentSizeCategory = true
|
||||
label.translatesAutoresizingMaskIntoConstraints = false
|
||||
return label
|
||||
}()
|
||||
|
||||
private var categoryTitleLabel: UILabel = {
|
||||
let label = UILabel()
|
||||
label.textColor = Asset.Colors.lightDarkGray.color
|
||||
label.font = .preferredFont(forTextStyle: .caption2)
|
||||
label.text = L10n.Scene.ServerPicker.Label.category
|
||||
label.textAlignment = .center
|
||||
label.adjustsFontForContentSizeCategory = true
|
||||
label.translatesAutoresizingMaskIntoConstraints = false
|
||||
return label
|
||||
}()
|
||||
|
||||
private var collapseConstraints: [NSLayoutConstraint] = []
|
||||
private var expandConstraints: [NSLayoutConstraint] = []
|
||||
|
||||
var mode: PickServerCell.Mode = .collapse {
|
||||
didSet {
|
||||
updateMode()
|
||||
}
|
||||
}
|
||||
|
||||
var server: Mastodon.Entity.Server? {
|
||||
didSet {
|
||||
updateServerInfo()
|
||||
}
|
||||
}
|
||||
|
||||
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
|
||||
super.init(style: style, reuseIdentifier: reuseIdentifier)
|
||||
_init()
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
super.init(coder: coder)
|
||||
_init()
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Methods to configure appearance
|
||||
extension PickServerCell {
|
||||
private func _init() {
|
||||
selectionStyle = .none
|
||||
backgroundColor = .clear
|
||||
|
||||
contentView.addSubview(bgView)
|
||||
contentView.addSubview(domainLabel)
|
||||
contentView.addSubview(checkbox)
|
||||
contentView.addSubview(descriptionLabel)
|
||||
contentView.addSubview(seperator)
|
||||
|
||||
contentView.addSubview(expandButton)
|
||||
|
||||
// Always add the expandbox which contains elements only visible in expand mode
|
||||
contentView.addSubview(expandBox)
|
||||
expandBox.addSubview(thumbImageView)
|
||||
expandBox.addSubview(infoStackView)
|
||||
expandBox.isHidden = true
|
||||
|
||||
let verticalInfoStackViewLang = makeVerticalInfoStackView(arrangedView: langValueLabel, langTitleLabel)
|
||||
let verticalInfoStackViewUsers = makeVerticalInfoStackView(arrangedView: usersValueLabel, usersTitleLabel)
|
||||
let verticalInfoStackViewCategory = makeVerticalInfoStackView(arrangedView: categoryValueLabel, categoryTitleLabel)
|
||||
infoStackView.addArrangedSubview(verticalInfoStackViewLang)
|
||||
infoStackView.addArrangedSubview(verticalInfoStackViewUsers)
|
||||
infoStackView.addArrangedSubview(verticalInfoStackViewCategory)
|
||||
|
||||
let expandButtonTopConstraintInCollapse = expandButton.topAnchor.constraint(equalTo: descriptionLabel.lastBaselineAnchor, constant: 12).priority(.required)
|
||||
collapseConstraints.append(expandButtonTopConstraintInCollapse)
|
||||
|
||||
let expandButtonTopConstraintInExpand = expandButton.topAnchor.constraint(equalTo: expandBox.bottomAnchor, constant: 8).priority(.required)
|
||||
expandConstraints.append(expandButtonTopConstraintInExpand)
|
||||
|
||||
// domainLabel.setContentHuggingPriority(.required - 1, for: .vertical)
|
||||
// domainLabel.setContentCompressionResistancePriority(.required - 1, for: .vertical)
|
||||
// descriptionLabel.setContentHuggingPriority(.required - 2, for: .vertical)
|
||||
// descriptionLabel.setContentCompressionResistancePriority(.required - 2, for: .vertical)
|
||||
domainLabel.setContentHuggingPriority(.required, for: .vertical)
|
||||
domainLabel.setContentCompressionResistancePriority(.required, for: .vertical)
|
||||
descriptionLabel.setContentHuggingPriority(.defaultHigh, for: .vertical)
|
||||
descriptionLabel.setContentCompressionResistancePriority(.defaultHigh, for: .vertical)
|
||||
|
||||
NSLayoutConstraint.activate([
|
||||
// Set background view
|
||||
bgView.leadingAnchor.constraint(equalTo: contentView.readableContentGuide.leadingAnchor),
|
||||
bgView.topAnchor.constraint(equalTo: contentView.topAnchor),
|
||||
contentView.readableContentGuide.trailingAnchor.constraint(equalTo: bgView.trailingAnchor),
|
||||
contentView.bottomAnchor.constraint(equalTo: bgView.bottomAnchor, constant: 1),
|
||||
|
||||
// Set bottom separator
|
||||
seperator.leadingAnchor.constraint(equalTo: contentView.readableContentGuide.leadingAnchor),
|
||||
contentView.readableContentGuide.trailingAnchor.constraint(equalTo: seperator.trailingAnchor),
|
||||
contentView.bottomAnchor.constraint(equalTo: seperator.bottomAnchor),
|
||||
seperator.heightAnchor.constraint(equalToConstant: 1),
|
||||
|
||||
domainLabel.leadingAnchor.constraint(equalTo: bgView.leadingAnchor, constant: 16),
|
||||
domainLabel.topAnchor.constraint(equalTo: bgView.topAnchor, constant: 16),
|
||||
|
||||
checkbox.widthAnchor.constraint(equalToConstant: 23),
|
||||
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).priority(.required),
|
||||
bgView.trailingAnchor.constraint(equalTo: descriptionLabel.trailingAnchor, constant: 16),
|
||||
|
||||
// Set expandBox constraints
|
||||
expandBox.leadingAnchor.constraint(equalTo: bgView.leadingAnchor, constant: 16),
|
||||
bgView.trailingAnchor.constraint(equalTo: expandBox.trailingAnchor, constant: 16),
|
||||
expandBox.topAnchor.constraint(equalTo: descriptionLabel.bottomAnchor, constant: 8),
|
||||
expandBox.bottomAnchor.constraint(equalTo: infoStackView.bottomAnchor).priority(.defaultHigh),
|
||||
|
||||
thumbImageView.leadingAnchor.constraint(equalTo: expandBox.leadingAnchor),
|
||||
expandBox.trailingAnchor.constraint(equalTo: thumbImageView.trailingAnchor),
|
||||
thumbImageView.topAnchor.constraint(equalTo: expandBox.topAnchor).priority(.defaultHigh),
|
||||
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),
|
||||
infoStackView.topAnchor.constraint(equalTo: thumbImageView.bottomAnchor, constant: 16),
|
||||
|
||||
expandButton.leadingAnchor.constraint(equalTo: bgView.leadingAnchor, constant: 16),
|
||||
bgView.trailingAnchor.constraint(equalTo: expandButton.trailingAnchor, constant: 16),
|
||||
bgView.bottomAnchor.constraint(equalTo: expandButton.bottomAnchor, constant: 8),
|
||||
])
|
||||
|
||||
NSLayoutConstraint.activate(collapseConstraints)
|
||||
|
||||
expandButton.addTarget(self, action: #selector(expandButtonDidClicked(_:)), for: .touchUpInside)
|
||||
|
||||
}
|
||||
|
||||
private func makeVerticalInfoStackView(arrangedView: UIView...) -> UIStackView {
|
||||
let stackView = UIStackView()
|
||||
stackView.translatesAutoresizingMaskIntoConstraints = false
|
||||
stackView.axis = .vertical
|
||||
stackView.alignment = .center
|
||||
stackView.distribution = .equalCentering
|
||||
stackView.spacing = 2
|
||||
arrangedView.forEach { stackView.addArrangedSubview($0) }
|
||||
return stackView
|
||||
}
|
||||
|
||||
private func updateMode() {
|
||||
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) {
|
||||
let newMode: Mode = mode == .collapse ? .expand : .collapse
|
||||
delegate?.pickServerCell(modeChange: server!, newMode: newMode, updates: { [weak self] in
|
||||
self?.mode = newMode
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Methods to update data
|
||||
extension PickServerCell {
|
||||
private func updateServerInfo() {
|
||||
guard let serverInfo = server else { return }
|
||||
domainLabel.text = serverInfo.domain
|
||||
descriptionLabel.text = serverInfo.description
|
||||
let processor = RoundCornerImageProcessor(cornerRadius: 3)
|
||||
thumbImageView.kf.indicatorType = .activity
|
||||
thumbImageView.kf.setImage(with: URL(string: serverInfo.proxiedThumbnail ?? "")!, placeholder: UIImage.placeholder(color: Asset.Colors.lightBackground.color), options: [
|
||||
.processor(processor),
|
||||
.scaleFactor(UIScreen.main.scale),
|
||||
.transition(.fade(1))
|
||||
])
|
||||
langValueLabel.text = serverInfo.language.uppercased()
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,103 @@
|
|||
//
|
||||
// PickServerSearchCell.swift
|
||||
// Mastodon
|
||||
//
|
||||
// Created by BradGao on 2021/2/24.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
|
||||
protocol PickServerSearchCellDelegate: class {
|
||||
func pickServerSearchCell(didChange searchText: String?)
|
||||
}
|
||||
|
||||
class PickServerSearchCell: UITableViewCell {
|
||||
|
||||
weak var delegate: PickServerSearchCellDelegate?
|
||||
|
||||
private var bgView: UIView = {
|
||||
let view = UIView()
|
||||
view.backgroundColor = Asset.Colors.lightWhite.color
|
||||
view.translatesAutoresizingMaskIntoConstraints = false
|
||||
view.layer.maskedCorners = [
|
||||
.layerMinXMinYCorner,
|
||||
.layerMaxXMinYCorner
|
||||
]
|
||||
view.layer.cornerCurve = .continuous
|
||||
view.layer.cornerRadius = 10
|
||||
return view
|
||||
}()
|
||||
|
||||
private var textFieldBgView: UIView = {
|
||||
let view = UIView()
|
||||
view.backgroundColor = Asset.Colors.lightBackground.color.withAlphaComponent(0.6)
|
||||
view.translatesAutoresizingMaskIntoConstraints = false
|
||||
view.layer.masksToBounds = true
|
||||
view.layer.cornerRadius = 6
|
||||
view.layer.cornerCurve = .continuous
|
||||
return view
|
||||
}()
|
||||
|
||||
private var searchTextField: UITextField = {
|
||||
let textField = UITextField()
|
||||
textField.translatesAutoresizingMaskIntoConstraints = false
|
||||
textField.font = .preferredFont(forTextStyle: .headline)
|
||||
textField.tintColor = Asset.Colors.lightDarkGray.color
|
||||
textField.textColor = Asset.Colors.lightDarkGray.color
|
||||
textField.adjustsFontForContentSizeCategory = true
|
||||
textField.attributedPlaceholder =
|
||||
NSAttributedString(string: L10n.Scene.ServerPicker.Input.placeholder,
|
||||
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
|
||||
}()
|
||||
|
||||
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
|
||||
super.init(style: style, reuseIdentifier: reuseIdentifier)
|
||||
_init()
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
super.init(coder: coder)
|
||||
_init()
|
||||
}
|
||||
}
|
||||
|
||||
extension PickServerSearchCell {
|
||||
private func _init() {
|
||||
self.selectionStyle = .none
|
||||
backgroundColor = .clear
|
||||
|
||||
searchTextField.addTarget(self, action: #selector(textFieldDidChange(_:)), for: .editingChanged)
|
||||
|
||||
contentView.addSubview(bgView)
|
||||
contentView.addSubview(textFieldBgView)
|
||||
contentView.addSubview(searchTextField)
|
||||
|
||||
NSLayoutConstraint.activate([
|
||||
bgView.leadingAnchor.constraint(equalTo: contentView.readableContentGuide.leadingAnchor),
|
||||
bgView.topAnchor.constraint(equalTo: contentView.topAnchor),
|
||||
bgView.trailingAnchor.constraint(equalTo: contentView.readableContentGuide.trailingAnchor),
|
||||
bgView.bottomAnchor.constraint(equalTo: contentView.bottomAnchor),
|
||||
|
||||
textFieldBgView.leadingAnchor.constraint(equalTo: bgView.leadingAnchor, constant: 14),
|
||||
textFieldBgView.topAnchor.constraint(equalTo: bgView.topAnchor, constant: 12),
|
||||
bgView.trailingAnchor.constraint(equalTo: textFieldBgView.trailingAnchor, constant: 14),
|
||||
bgView.bottomAnchor.constraint(equalTo: textFieldBgView.bottomAnchor, constant: 13),
|
||||
|
||||
searchTextField.leadingAnchor.constraint(equalTo: textFieldBgView.leadingAnchor, constant: 11),
|
||||
searchTextField.topAnchor.constraint(equalTo: textFieldBgView.topAnchor, constant: 4),
|
||||
textFieldBgView.trailingAnchor.constraint(equalTo: searchTextField.trailingAnchor, constant: 11),
|
||||
textFieldBgView.bottomAnchor.constraint(equalTo: searchTextField.bottomAnchor, constant: 4),
|
||||
])
|
||||
}
|
||||
}
|
||||
|
||||
extension PickServerSearchCell {
|
||||
@objc func textFieldDidChange(_ textField: UITextField) {
|
||||
delegate?.pickServerSearchCell(didChange: textField.text)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,48 @@
|
|||
//
|
||||
// PickServerTitleCell.swift
|
||||
// Mastodon
|
||||
//
|
||||
// Created by BradGao on 2021/2/23.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
|
||||
final class PickServerTitleCell: UITableViewCell {
|
||||
|
||||
let titleLabel: UILabel = {
|
||||
let label = UILabel()
|
||||
label.font = UIFontMetrics(forTextStyle: .largeTitle).scaledFont(for: UIFont.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
|
||||
}()
|
||||
|
||||
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
|
||||
super.init(style: style, reuseIdentifier: reuseIdentifier)
|
||||
_init()
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
super.init(coder: coder)
|
||||
_init()
|
||||
}
|
||||
}
|
||||
|
||||
extension PickServerTitleCell {
|
||||
|
||||
private func _init() {
|
||||
self.selectionStyle = .none
|
||||
backgroundColor = .clear
|
||||
|
||||
contentView.addSubview(titleLabel)
|
||||
NSLayoutConstraint.activate([
|
||||
titleLabel.leadingAnchor.constraint(equalTo: contentView.readableContentGuide.leadingAnchor),
|
||||
contentView.readableContentGuide.trailingAnchor.constraint(equalTo: titleLabel.trailingAnchor),
|
||||
titleLabel.topAnchor.constraint(equalTo: contentView.topAnchor),
|
||||
titleLabel.bottomAnchor.constraint(equalTo: contentView.bottomAnchor),
|
||||
])
|
||||
}
|
||||
}
|
|
@ -0,0 +1,99 @@
|
|||
//
|
||||
// PickServerCategoryView.swift
|
||||
// Mastodon
|
||||
//
|
||||
// Created by BradGao on 2021/2/23.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
import MastodonSDK
|
||||
|
||||
class PickServerCategoryView: UIView {
|
||||
var category: PickServerViewModel.Category? {
|
||||
didSet {
|
||||
updateCategory()
|
||||
}
|
||||
}
|
||||
var selected: Bool = false {
|
||||
didSet {
|
||||
updateSelectStatus()
|
||||
}
|
||||
}
|
||||
|
||||
var bgShadowView: UIView = {
|
||||
let view = UIView()
|
||||
view.translatesAutoresizingMaskIntoConstraints = false
|
||||
return view
|
||||
}()
|
||||
|
||||
var bgView: UIView = {
|
||||
let view = UIView()
|
||||
view.translatesAutoresizingMaskIntoConstraints = false
|
||||
view.layer.masksToBounds = true
|
||||
view.layer.cornerRadius = 30
|
||||
return view
|
||||
}()
|
||||
|
||||
var titleLabel: UILabel = {
|
||||
let label = UILabel()
|
||||
label.textAlignment = .center
|
||||
label.translatesAutoresizingMaskIntoConstraints = false
|
||||
return label
|
||||
}()
|
||||
|
||||
init() {
|
||||
super.init(frame: .zero)
|
||||
configure()
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
super.init(coder: coder)
|
||||
configure()
|
||||
}
|
||||
}
|
||||
|
||||
extension PickServerCategoryView {
|
||||
private func configure() {
|
||||
addSubview(bgView)
|
||||
addSubview(titleLabel)
|
||||
|
||||
bgView.backgroundColor = Asset.Colors.lightWhite.color
|
||||
|
||||
NSLayoutConstraint.activate([
|
||||
bgView.leadingAnchor.constraint(equalTo: self.leadingAnchor),
|
||||
bgView.trailingAnchor.constraint(equalTo: self.trailingAnchor),
|
||||
bgView.topAnchor.constraint(equalTo: self.topAnchor),
|
||||
bgView.bottomAnchor.constraint(equalTo: self.bottomAnchor),
|
||||
|
||||
titleLabel.centerXAnchor.constraint(equalTo: self.centerXAnchor),
|
||||
titleLabel.centerYAnchor.constraint(equalTo: self.centerYAnchor),
|
||||
])
|
||||
}
|
||||
|
||||
private func updateCategory() {
|
||||
guard let category = category else { return }
|
||||
titleLabel.text = category.title
|
||||
switch category {
|
||||
case .all:
|
||||
titleLabel.font = UIFont.systemFont(ofSize: 17)
|
||||
case .some:
|
||||
titleLabel.font = UIFont.systemFont(ofSize: 28)
|
||||
}
|
||||
}
|
||||
|
||||
private func updateSelectStatus() {
|
||||
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 {
|
||||
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 {
|
||||
titleLabel.textColor = Asset.Colors.lightBrandBlue.color
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -2,12 +2,24 @@
|
|||
// PrimaryActionButton.swift
|
||||
// Mastodon
|
||||
//
|
||||
// Created by 高原 on 2021/2/20.
|
||||
// Created by BradGao on 2021/2/20.
|
||||
//
|
||||
|
||||
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,13 +29,39 @@ 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 {
|
||||
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)
|
||||
}
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
// WelcomeViewController.swift
|
||||
// Mastodon
|
||||
//
|
||||
// Created by 高原 on 2021/2/20.
|
||||
// Created by BradGao on 2021/2/20.
|
||||
//
|
||||
|
||||
import os.log
|
||||
|
@ -13,15 +13,6 @@ final class WelcomeViewController: UIViewController, NeedsDependency {
|
|||
weak var context: AppContext! { willSet { precondition(!isViewLoaded) } }
|
||||
weak var coordinator: SceneCoordinator! { willSet { precondition(!isViewLoaded) } }
|
||||
|
||||
#if DEBUG
|
||||
lazy var authenticationViewController: AuthenticationViewController = {
|
||||
let authenticationViewController = AuthenticationViewController()
|
||||
authenticationViewController.context = context
|
||||
authenticationViewController.coordinator = coordinator
|
||||
return authenticationViewController
|
||||
}()
|
||||
#endif
|
||||
|
||||
let logoImageView: UIImageView = {
|
||||
let imageView = UIImageView(image: Asset.welcomeLogo.image)
|
||||
imageView.translatesAutoresizingMaskIntoConstraints = false
|
||||
|
@ -65,6 +56,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([
|
||||
|
@ -93,8 +88,8 @@ extension WelcomeViewController {
|
|||
signInButton.topAnchor.constraint(equalTo: signUpButton.bottomAnchor, constant: 5)
|
||||
])
|
||||
|
||||
signInButton.addTarget(self, action: #selector(WelcomeViewController.signInButtonPressed(_:)), for: .touchUpInside)
|
||||
signUpButton.addTarget(self, action: #selector(WelcomeViewController.signUpButtonPressed(_:)), for: .touchUpInside)
|
||||
signUpButton.addTarget(self, action: #selector(signUpButtonDidClicked(_:)), for: .touchUpInside)
|
||||
signInButton.addTarget(self, action: #selector(signInButtonDidClicked(_:)), for: .touchUpInside)
|
||||
}
|
||||
|
||||
override func viewWillAppear(_ animated: Bool) {
|
||||
|
@ -105,20 +100,13 @@ extension WelcomeViewController {
|
|||
}
|
||||
|
||||
extension WelcomeViewController {
|
||||
|
||||
@objc private func signInButtonPressed(_ sender: UIButton) {
|
||||
os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s", ((#file as NSString).lastPathComponent), #line, #function)
|
||||
|
||||
#if DEBUG
|
||||
authenticationViewController.viewModel = AuthenticationViewModel(context: context, coordinator: coordinator, isAuthenticationExist: true)
|
||||
authenticationViewController.viewModel.domain.value = "pawoo.net"
|
||||
let _ = authenticationViewController.view // trigger view load
|
||||
authenticationViewController.signInButton.sendActions(for: .touchUpInside)
|
||||
#endif
|
||||
@objc
|
||||
private func signUpButtonDidClicked(_ sender: UIButton) {
|
||||
coordinator.present(scene: .pickServer(viewMode: PickServerViewModel(context: context, mode: .signUp)), from: self, transition: .show)
|
||||
}
|
||||
|
||||
@objc private func signUpButtonPressed(_ sender: UIButton) {
|
||||
os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s", ((#file as NSString).lastPathComponent), #line, #function)
|
||||
@objc
|
||||
private func signInButtonDidClicked(_ sender: UIButton) {
|
||||
coordinator.present(scene: .pickServer(viewMode: PickServerViewModel(context: context, mode: .signIn)), from: self, transition: .show)
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
@ -37,6 +37,25 @@ extension Mastodon.Entity {
|
|||
case language
|
||||
case category
|
||||
}
|
||||
|
||||
public init(instance: Instance) {
|
||||
self.domain = instance.uri
|
||||
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
|
||||
self.categories = []
|
||||
self.category = "Unknown"
|
||||
self.proxiedThumbnail = instance.thumbnail
|
||||
self.totalUsers = instance.statistics?.userCount ?? 0
|
||||
self.lastWeekUsers = 0
|
||||
self.approvalRequired = instance.approvalRequired ?? false
|
||||
}
|
||||
|
||||
public static func == (lhs: Self, rhs: Self) -> Bool {
|
||||
return lhs.domain.caseInsensitiveCompare(rhs.domain) == .orderedSame
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue