Merge pull request #551 from mastodon/540-better-onboarding
Better Login
This commit is contained in:
commit
e208aedb7a
|
@ -74,8 +74,8 @@
|
|||
"take_photo": "Take Photo",
|
||||
"save_photo": "Save Photo",
|
||||
"copy_photo": "Copy Photo",
|
||||
"sign_in": "Sign In",
|
||||
"sign_up": "Sign Up",
|
||||
"sign_in": "Log in",
|
||||
"sign_up": "Create account",
|
||||
"see_more": "See More",
|
||||
"preview": "Preview",
|
||||
"share": "Share",
|
||||
|
@ -218,10 +218,16 @@
|
|||
"get_started": "Get Started",
|
||||
"log_in": "Log In"
|
||||
},
|
||||
"login": {
|
||||
"title": "Welcome back",
|
||||
"subtitle": "Log you in on the server you created your account on.",
|
||||
"server_search_field": {
|
||||
"placeholder": "Enter URL or search for your server"
|
||||
}
|
||||
}
|
||||
"server_picker": {
|
||||
"title": "Mastodon is made of users in different servers.",
|
||||
"subtitle": "Pick a server based on your interests, region, or a general purpose one.",
|
||||
"subtitle_extend": "Pick a server based on your interests, region, or a general purpose one. Each server is operated by an entirely independent organization or individual.",
|
||||
"subtitle": "Pick a server based on your region, interests, or a general purpose one. You can still chat with anyone on Mastodon, regardless of your servers.",
|
||||
"button": {
|
||||
"category": {
|
||||
"all": "All",
|
||||
|
@ -248,8 +254,7 @@
|
|||
"category": "CATEGORY"
|
||||
},
|
||||
"input": {
|
||||
"placeholder": "Search servers",
|
||||
"search_servers_or_enter_url": "Search servers or enter URL"
|
||||
"search_servers_or_enter_url": "Search communities or enter URL"
|
||||
},
|
||||
"empty_state": {
|
||||
"finding_servers": "Finding available servers...",
|
||||
|
@ -719,4 +724,4 @@
|
|||
"title": "Bookmarks"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -74,8 +74,8 @@
|
|||
"take_photo": "Take Photo",
|
||||
"save_photo": "Save Photo",
|
||||
"copy_photo": "Copy Photo",
|
||||
"sign_in": "Sign In",
|
||||
"sign_up": "Sign Up",
|
||||
"sign_in": "Log in",
|
||||
"sign_up": "Create account",
|
||||
"see_more": "See More",
|
||||
"preview": "Preview",
|
||||
"share": "Share",
|
||||
|
@ -218,10 +218,16 @@
|
|||
"get_started": "Get Started",
|
||||
"log_in": "Log In"
|
||||
},
|
||||
"login": {
|
||||
"title": "Welcome back",
|
||||
"subtitle": "Log you in on the server you created your account on.",
|
||||
"server_search_field": {
|
||||
"placeholder": "Enter URL or search for your server"
|
||||
}
|
||||
},
|
||||
"server_picker": {
|
||||
"title": "Mastodon is made of users in different servers.",
|
||||
"subtitle": "Pick a server based on your interests, region, or a general purpose one.",
|
||||
"subtitle_extend": "Pick a server based on your interests, region, or a general purpose one. Each server is operated by an entirely independent organization or individual.",
|
||||
"subtitle": "Pick a server based on your region, interests, or a general purpose one. You can still chat with anyone on Mastodon, regardless of your servers.",
|
||||
"button": {
|
||||
"category": {
|
||||
"all": "All",
|
||||
|
@ -248,8 +254,7 @@
|
|||
"category": "CATEGORY"
|
||||
},
|
||||
"input": {
|
||||
"placeholder": "Search servers",
|
||||
"search_servers_or_enter_url": "Search servers or enter URL"
|
||||
"search_servers_or_enter_url": "Search communities or enter URL"
|
||||
},
|
||||
"empty_state": {
|
||||
"finding_servers": "Finding available servers...",
|
||||
|
@ -719,4 +724,4 @@
|
|||
"title": "Bookmarks"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -88,6 +88,11 @@
|
|||
62FD27D52893708A00B205C5 /* BookmarkViewModel+Diffable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62FD27D42893708A00B205C5 /* BookmarkViewModel+Diffable.swift */; };
|
||||
87FFDA5D898A5C42ADCB35E7 /* Pods_Mastodon.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = A4ABE34829701A4496C5BB64 /* Pods_Mastodon.framework */; };
|
||||
C24C97032922F30500BAE8CB /* RefreshControl.swift in Sources */ = {isa = PBXBuildFile; fileRef = C24C97022922F30500BAE8CB /* RefreshControl.swift */; };
|
||||
D87BFC8B291D5C6B00FEE264 /* MastodonLoginView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D87BFC8A291D5C6B00FEE264 /* MastodonLoginView.swift */; };
|
||||
D87BFC8D291EB81200FEE264 /* MastodonLoginViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = D87BFC8C291EB81200FEE264 /* MastodonLoginViewModel.swift */; };
|
||||
D87BFC8F291EC26A00FEE264 /* MastodonLoginServerTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = D87BFC8E291EC26A00FEE264 /* MastodonLoginServerTableViewCell.swift */; };
|
||||
D8916DC029211BE500124085 /* ContentSizedTableView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D8916DBF29211BE500124085 /* ContentSizedTableView.swift */; };
|
||||
D8A6AB6C291C5136003AB663 /* MastodonLoginViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D8A6AB6B291C5136003AB663 /* MastodonLoginViewController.swift */; };
|
||||
DB0009A626AEE5DC009B9D2D /* Intents.intentdefinition in Sources */ = {isa = PBXBuildFile; fileRef = DB0009A926AEE5DC009B9D2D /* Intents.intentdefinition */; settings = {ATTRIBUTES = (codegen, ); }; };
|
||||
DB0009A726AEE5DC009B9D2D /* Intents.intentdefinition in Sources */ = {isa = PBXBuildFile; fileRef = DB0009A926AEE5DC009B9D2D /* Intents.intentdefinition */; };
|
||||
DB0140CF25C42AEE00F9F3CF /* OSLog.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB0140CE25C42AEE00F9F3CF /* OSLog.swift */; };
|
||||
|
@ -608,6 +613,11 @@
|
|||
C3789232A52F43529CA67E95 /* Pods-MastodonIntent.asdk - debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-MastodonIntent.asdk - debug.xcconfig"; path = "Target Support Files/Pods-MastodonIntent/Pods-MastodonIntent.asdk - debug.xcconfig"; sourceTree = "<group>"; };
|
||||
CD92E0F10BDE4FE7C4B999F2 /* Pods_MastodonTests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_MastodonTests.framework; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
D7D7CF93E262178800077512 /* Pods-Mastodon-AppShared.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Mastodon-AppShared.debug.xcconfig"; path = "Target Support Files/Pods-Mastodon-AppShared/Pods-Mastodon-AppShared.debug.xcconfig"; sourceTree = "<group>"; };
|
||||
D87BFC8A291D5C6B00FEE264 /* MastodonLoginView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MastodonLoginView.swift; sourceTree = "<group>"; };
|
||||
D87BFC8C291EB81200FEE264 /* MastodonLoginViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MastodonLoginViewModel.swift; sourceTree = "<group>"; };
|
||||
D87BFC8E291EC26A00FEE264 /* MastodonLoginServerTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MastodonLoginServerTableViewCell.swift; sourceTree = "<group>"; };
|
||||
D8916DBF29211BE500124085 /* ContentSizedTableView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentSizedTableView.swift; sourceTree = "<group>"; };
|
||||
D8A6AB6B291C5136003AB663 /* MastodonLoginViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MastodonLoginViewController.swift; sourceTree = "<group>"; };
|
||||
DB0009A826AEE5DC009B9D2D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.intentdefinition; name = Base; path = Base.lproj/Intents.intentdefinition; sourceTree = "<group>"; };
|
||||
DB0009AD26AEE5E4009B9D2D /* ar */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ar; path = ar.lproj/Intents.strings; sourceTree = "<group>"; };
|
||||
DB0140CE25C42AEE00F9F3CF /* OSLog.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OSLog.swift; sourceTree = "<group>"; };
|
||||
|
@ -632,7 +642,6 @@
|
|||
DB0618022785A7100030EE79 /* RegisterSection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RegisterSection.swift; sourceTree = "<group>"; };
|
||||
DB0618042785A73D0030EE79 /* RegisterItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RegisterItem.swift; sourceTree = "<group>"; };
|
||||
DB0618062785A8880030EE79 /* MastodonRegisterViewModel+Diffable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "MastodonRegisterViewModel+Diffable.swift"; sourceTree = "<group>"; };
|
||||
DB0618092785B2AB0030EE79 /* MastodonRegisterAvatarTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MastodonRegisterAvatarTableViewCell.swift; sourceTree = "<group>"; };
|
||||
DB0A322D280EE9FD001729D2 /* DiscoveryIntroBannerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DiscoveryIntroBannerView.swift; sourceTree = "<group>"; };
|
||||
DB0C947626A7FE840088FB11 /* NotificationAvatarButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationAvatarButton.swift; sourceTree = "<group>"; };
|
||||
DB0EF72A26FDB1D200347686 /* SidebarListCollectionViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SidebarListCollectionViewCell.swift; sourceTree = "<group>"; };
|
||||
|
@ -855,8 +864,6 @@
|
|||
DB7A9F922818F33C0016AF98 /* MastodonServerRulesViewController+Debug.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "MastodonServerRulesViewController+Debug.swift"; sourceTree = "<group>"; };
|
||||
DB7F48442620241000796008 /* ProfileHeaderViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfileHeaderViewModel.swift; sourceTree = "<group>"; };
|
||||
DB8190C52601FF0400020C08 /* AttachmentContainerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AttachmentContainerView.swift; sourceTree = "<group>"; };
|
||||
DB8481142788121200BBEABA /* MastodonRegisterTextFieldTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MastodonRegisterTextFieldTableViewCell.swift; sourceTree = "<group>"; };
|
||||
DB84811627883C2600BBEABA /* MastodonRegisterPasswordHintTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MastodonRegisterPasswordHintTableViewCell.swift; sourceTree = "<group>"; };
|
||||
DB848E32282B62A800A302CC /* ReportResultView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReportResultView.swift; sourceTree = "<group>"; };
|
||||
DB852D1826FAEB6B00FC9D81 /* SidebarViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SidebarViewController.swift; sourceTree = "<group>"; };
|
||||
DB852D1B26FB021500FC9D81 /* RootSplitViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RootSplitViewController.swift; sourceTree = "<group>"; };
|
||||
|
@ -1497,9 +1504,22 @@
|
|||
path = Bookmark;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
D8A6AB68291C50F3003AB663 /* Login */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
D8A6AB6B291C5136003AB663 /* MastodonLoginViewController.swift */,
|
||||
D87BFC8A291D5C6B00FEE264 /* MastodonLoginView.swift */,
|
||||
D87BFC8C291EB81200FEE264 /* MastodonLoginViewModel.swift */,
|
||||
D87BFC8E291EC26A00FEE264 /* MastodonLoginServerTableViewCell.swift */,
|
||||
D8916DBF29211BE500124085 /* ContentSizedTableView.swift */,
|
||||
);
|
||||
path = Login;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
DB01409B25C40BB600F9F3CF /* Onboarding */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
D8A6AB68291C50F3003AB663 /* Login */,
|
||||
DB68A03825E900CC00CFDF14 /* Share */,
|
||||
0FAA0FDD25E0B5700017CCDE /* Welcome */,
|
||||
0FAA102525E1125D0017CCDE /* PickServer */,
|
||||
|
@ -1564,16 +1584,6 @@
|
|||
path = Cell;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
DB06180B2785B2AF0030EE79 /* Cell */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
DB0618092785B2AB0030EE79 /* MastodonRegisterAvatarTableViewCell.swift */,
|
||||
DB8481142788121200BBEABA /* MastodonRegisterTextFieldTableViewCell.swift */,
|
||||
DB84811627883C2600BBEABA /* MastodonRegisterPasswordHintTableViewCell.swift */,
|
||||
);
|
||||
path = Cell;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
DB0A322F280EEA00001729D2 /* View */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
|
@ -2540,7 +2550,6 @@
|
|||
DBE0821A25CD382900FD6BBD /* Register */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
DB06180B2785B2AF0030EE79 /* Cell */,
|
||||
DBE0821425CD382600FD6BBD /* MastodonRegisterViewController.swift */,
|
||||
2D939AE725EE1CF80076FA61 /* MastodonRegisterViewController+Avatar.swift */,
|
||||
DBE0822325CD3F1E00FD6BBD /* MastodonRegisterViewModel.swift */,
|
||||
|
@ -3185,6 +3194,7 @@
|
|||
DB5B54A32833BD1A00DEF8B2 /* UserListViewModel.swift in Sources */,
|
||||
DBF156DF2701B17600EC00B7 /* SidebarAddAccountCollectionViewCell.swift in Sources */,
|
||||
DB0617F1278413D00030EE79 /* PickServerServerSectionTableHeaderView.swift in Sources */,
|
||||
D87BFC8F291EC26A00FEE264 /* MastodonLoginServerTableViewCell.swift in Sources */,
|
||||
DB0FCB7E27958957006C02E2 /* StatusThreadRootTableViewCell+ViewModel.swift in Sources */,
|
||||
DB789A0B25F9F2950071ACA0 /* ComposeViewController.swift in Sources */,
|
||||
DB938F0926240F3C00E5B6C1 /* RemoteThreadViewModel.swift in Sources */,
|
||||
|
@ -3304,9 +3314,11 @@
|
|||
DB63F769279A5EBB00455B82 /* NotificationTimelineViewModel+Diffable.swift in Sources */,
|
||||
DBFEEC9B279BDDD9004F81DD /* ProfileAboutViewModel+Diffable.swift in Sources */,
|
||||
DBB525562611EDCA002F1F29 /* UserTimelineViewModel.swift in Sources */,
|
||||
D8916DC029211BE500124085 /* ContentSizedTableView.swift in Sources */,
|
||||
DB0618012785732C0030EE79 /* ServerRulesTableViewCell.swift in Sources */,
|
||||
DB98EB5C27B10A730082E365 /* ReportSupplementaryViewModel.swift in Sources */,
|
||||
DB0617EF277F12720030EE79 /* NavigationActionView.swift in Sources */,
|
||||
D87BFC8D291EB81200FEE264 /* MastodonLoginViewModel.swift in Sources */,
|
||||
DB1FD43625F26899004CFCFC /* MastodonPickServerViewModel+LoadIndexedServerState.swift in Sources */,
|
||||
2D939AE825EE1CF80076FA61 /* MastodonRegisterViewController+Avatar.swift in Sources */,
|
||||
DB1D186C25EF5BA7003F1F23 /* PollTableView.swift in Sources */,
|
||||
|
@ -3375,6 +3387,7 @@
|
|||
DBCC3B8F26148F7B0045B23D /* CachedProfileViewModel.swift in Sources */,
|
||||
DB4F097526A037F500D62E92 /* SearchHistoryViewModel.swift in Sources */,
|
||||
DB3EA8E9281B7A3700598866 /* DiscoveryCommunityViewModel.swift in Sources */,
|
||||
D87BFC8B291D5C6B00FEE264 /* MastodonLoginView.swift in Sources */,
|
||||
DB6180F826391D660018D199 /* MediaPreviewingViewController.swift in Sources */,
|
||||
DBEFCD71282A12B200C0ABEA /* ReportReasonViewController.swift in Sources */,
|
||||
DB0140CF25C42AEE00F9F3CF /* OSLog.swift in Sources */,
|
||||
|
@ -3436,6 +3449,7 @@
|
|||
DB697DDB278F4DE3004EF2F7 /* DataSourceProvider+StatusTableViewCellDelegate.swift in Sources */,
|
||||
DB87D4512609CF1E00D12C0D /* ComposeStatusPollOptionAppendEntryCollectionViewCell.swift in Sources */,
|
||||
DBB45B5627B39FC9002DC5A7 /* MediaPreviewVideoViewController.swift in Sources */,
|
||||
D8A6AB6C291C5136003AB663 /* MastodonLoginViewController.swift in Sources */,
|
||||
DB0FCB8027968F70006C02E2 /* MastodonStatusThreadViewModel.swift in Sources */,
|
||||
DB67D08627312E67006A36CF /* WizardViewController.swift in Sources */,
|
||||
DB6746EB278ED8B0008A6B94 /* PollOptionView+Configuration.swift in Sources */,
|
||||
|
|
|
@ -149,6 +149,7 @@ extension SceneCoordinator {
|
|||
case mastodonConfirmEmail(viewModel: MastodonConfirmEmailViewModel)
|
||||
case mastodonResendEmail(viewModel: MastodonResendEmailViewModel)
|
||||
case mastodonWebView(viewModel: WebViewModel)
|
||||
case mastodonLogin
|
||||
|
||||
// search
|
||||
case searchDetail(viewModel: SearchDetailViewModel)
|
||||
|
@ -199,6 +200,7 @@ extension SceneCoordinator {
|
|||
case .welcome,
|
||||
.mastodonPickServer,
|
||||
.mastodonRegister,
|
||||
.mastodonLogin,
|
||||
.mastodonServerRules,
|
||||
.mastodonConfirmEmail,
|
||||
.mastodonResendEmail:
|
||||
|
@ -403,6 +405,13 @@ private extension SceneCoordinator {
|
|||
let _viewController = MastodonConfirmEmailViewController()
|
||||
_viewController.viewModel = viewModel
|
||||
viewController = _viewController
|
||||
case .mastodonLogin:
|
||||
let loginViewController = MastodonLoginViewController(appContext: appContext,
|
||||
authenticationViewModel: AuthenticationViewModel(context: appContext, coordinator: self, isAuthenticationExist: false),
|
||||
sceneCoordinator: self)
|
||||
loginViewController.delegate = self
|
||||
|
||||
viewController = loginViewController
|
||||
case .mastodonResendEmail(let viewModel):
|
||||
let _viewController = MastodonResendEmailViewController()
|
||||
_viewController.viewModel = viewModel
|
||||
|
@ -529,5 +538,16 @@ private extension SceneCoordinator {
|
|||
needs?.context = appContext
|
||||
needs?.coordinator = self
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
//MARK: - MastodonLoginViewControllerDelegate
|
||||
|
||||
extension SceneCoordinator: MastodonLoginViewControllerDelegate {
|
||||
func backButtonPressed(_ viewController: MastodonLoginViewController) {
|
||||
viewController.navigationController?.popViewController(animated: true)
|
||||
}
|
||||
|
||||
func nextButtonPressed(_ viewController: MastodonLoginViewController) {
|
||||
viewController.login()
|
||||
}
|
||||
}
|
||||
|
|
|
@ -21,7 +21,6 @@ extension CategoryPickerSection {
|
|||
UICollectionViewDiffableDataSource(collectionView: collectionView) { [weak dependency] collectionView, indexPath, item -> UICollectionViewCell? in
|
||||
guard let _ = dependency else { return nil }
|
||||
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: String(describing: PickServerCategoryCollectionViewCell.self), for: indexPath) as! PickServerCategoryCollectionViewCell
|
||||
cell.categoryView.emojiLabel.text = item.emoji
|
||||
cell.categoryView.titleLabel.text = item.title
|
||||
cell.observe(\.isSelected, options: [.initial, .new]) { cell, _ in
|
||||
cell.categoryView.highlightedIndicatorView.alpha = cell.isSelected ? 1 : 0
|
||||
|
|
|
@ -18,16 +18,14 @@ enum PickServerSection: Equatable, Hashable {
|
|||
extension PickServerSection {
|
||||
static func tableViewDiffableDataSource(
|
||||
for tableView: UITableView,
|
||||
dependency: NeedsDependency,
|
||||
pickServerCellDelegate: PickServerCellDelegate
|
||||
dependency: NeedsDependency
|
||||
) -> UITableViewDiffableDataSource<PickServerSection, PickServerItem> {
|
||||
tableView.register(OnboardingHeadlineTableViewCell.self, forCellReuseIdentifier: String(describing: OnboardingHeadlineTableViewCell.self))
|
||||
tableView.register(PickServerCell.self, forCellReuseIdentifier: String(describing: PickServerCell.self))
|
||||
tableView.register(PickServerLoaderTableViewCell.self, forCellReuseIdentifier: String(describing: PickServerLoaderTableViewCell.self))
|
||||
|
||||
return UITableViewDiffableDataSource(tableView: tableView) { [
|
||||
weak dependency,
|
||||
weak pickServerCellDelegate
|
||||
weak dependency
|
||||
] tableView, indexPath, item -> UITableViewCell? in
|
||||
guard let _ = dependency else { return nil }
|
||||
switch item {
|
||||
|
@ -37,7 +35,6 @@ extension PickServerSection {
|
|||
case .server(let server, let attribute):
|
||||
let cell = tableView.dequeueReusableCell(withIdentifier: String(describing: PickServerCell.self), for: indexPath) as! PickServerCell
|
||||
PickServerSection.configure(cell: cell, server: server, attribute: attribute)
|
||||
cell.delegate = pickServerCellDelegate
|
||||
return cell
|
||||
case .loader(let attribute):
|
||||
let cell = tableView.dequeueReusableCell(withIdentifier: String(describing: PickServerLoaderTableViewCell.self), for: indexPath) as! PickServerLoaderTableViewCell
|
||||
|
|
|
@ -0,0 +1,22 @@
|
|||
//
|
||||
// MastodonLoginTableView.swift
|
||||
// Mastodon
|
||||
//
|
||||
// Created by Nathan Mattes on 13.11.22.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
|
||||
// Source: https://stackoverflow.com/a/48623673
|
||||
final class ContentSizedTableView: UITableView {
|
||||
override var contentSize:CGSize {
|
||||
didSet {
|
||||
invalidateIntrinsicContentSize()
|
||||
}
|
||||
}
|
||||
|
||||
override var intrinsicContentSize: CGSize {
|
||||
layoutIfNeeded()
|
||||
return CGSize(width: UIView.noIntrinsicMetric, height: contentSize.height)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,12 @@
|
|||
//
|
||||
// MastodonLoginServerTableViewCell.swift
|
||||
// Mastodon
|
||||
//
|
||||
// Created by Nathan Mattes on 11.11.22.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
|
||||
class MastodonLoginServerTableViewCell: UITableViewCell {
|
||||
static let reuseIdentifier = "MastodonLoginServerTableViewCell"
|
||||
}
|
|
@ -0,0 +1,151 @@
|
|||
//
|
||||
// MastodonLoginView.swift
|
||||
// Mastodon
|
||||
//
|
||||
// Created by Nathan Mattes on 10.11.22.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
import MastodonAsset
|
||||
import MastodonLocalization
|
||||
|
||||
class MastodonLoginView: UIView {
|
||||
|
||||
// List with (filtered) domains
|
||||
|
||||
let titleLabel: UILabel
|
||||
let subtitleLabel: UILabel
|
||||
private let headerStackView: UIStackView
|
||||
|
||||
let searchTextField: UITextField
|
||||
private let searchTextFieldLeftView: UIView
|
||||
private let searchTextFieldMagnifyingGlass: UIImageView
|
||||
private let searchContainerLeftPaddingView: UIView
|
||||
|
||||
let tableView: UITableView
|
||||
let navigationActionView: NavigationActionView
|
||||
var bottomConstraint: NSLayoutConstraint?
|
||||
|
||||
override init(frame: CGRect) {
|
||||
|
||||
titleLabel = UILabel()
|
||||
titleLabel.font = MastodonLoginViewController.largeTitleFont
|
||||
titleLabel.textColor = MastodonLoginViewController.largeTitleTextColor
|
||||
titleLabel.text = L10n.Scene.Login.title
|
||||
titleLabel.numberOfLines = 0
|
||||
|
||||
subtitleLabel = UILabel()
|
||||
subtitleLabel.font = MastodonLoginViewController.subTitleFont
|
||||
subtitleLabel.textColor = MastodonLoginViewController.subTitleTextColor
|
||||
subtitleLabel.text = L10n.Scene.Login.subtitle
|
||||
subtitleLabel.numberOfLines = 0
|
||||
|
||||
headerStackView = UIStackView(arrangedSubviews: [titleLabel, subtitleLabel])
|
||||
headerStackView.axis = .vertical
|
||||
headerStackView.spacing = 16
|
||||
headerStackView.translatesAutoresizingMaskIntoConstraints = false
|
||||
|
||||
searchTextFieldMagnifyingGlass = UIImageView(image: UIImage(
|
||||
systemName: "magnifyingglass",
|
||||
withConfiguration: UIImage.SymbolConfiguration(pointSize: 15, weight: .regular)
|
||||
))
|
||||
searchTextFieldMagnifyingGlass.tintColor = Asset.Colors.Label.secondary.color.withAlphaComponent(0.6)
|
||||
searchTextFieldMagnifyingGlass.translatesAutoresizingMaskIntoConstraints = false
|
||||
|
||||
searchContainerLeftPaddingView = UIView()
|
||||
searchContainerLeftPaddingView.translatesAutoresizingMaskIntoConstraints = false
|
||||
|
||||
searchTextFieldLeftView = UIView()
|
||||
searchTextFieldLeftView.addSubview(searchTextFieldMagnifyingGlass)
|
||||
searchTextFieldLeftView.addSubview(searchContainerLeftPaddingView)
|
||||
|
||||
searchTextField = UITextField()
|
||||
searchTextField.translatesAutoresizingMaskIntoConstraints = false
|
||||
searchTextField.backgroundColor = Asset.Scene.Onboarding.searchBarBackground.color
|
||||
searchTextField.placeholder = L10n.Scene.Login.ServerSearchField.placeholder
|
||||
searchTextField.leftView = searchTextFieldLeftView
|
||||
searchTextField.leftViewMode = .always
|
||||
searchTextField.layer.cornerRadius = 10
|
||||
searchTextField.keyboardType = .URL
|
||||
searchTextField.autocorrectionType = .no
|
||||
searchTextField.autocapitalizationType = .none
|
||||
|
||||
tableView = ContentSizedTableView()
|
||||
tableView.translatesAutoresizingMaskIntoConstraints = false
|
||||
tableView.backgroundColor = Asset.Scene.Onboarding.textFieldBackground.color
|
||||
tableView.keyboardDismissMode = .onDrag
|
||||
tableView.layer.cornerRadius = 10
|
||||
|
||||
navigationActionView = NavigationActionView()
|
||||
navigationActionView.translatesAutoresizingMaskIntoConstraints = false
|
||||
|
||||
super.init(frame: frame)
|
||||
|
||||
addSubview(headerStackView)
|
||||
addSubview(searchTextField)
|
||||
addSubview(tableView)
|
||||
addSubview(navigationActionView)
|
||||
backgroundColor = Asset.Scene.Onboarding.background.color
|
||||
|
||||
setupConstraints()
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
private func setupConstraints() {
|
||||
|
||||
let bottomConstraint = safeAreaLayoutGuide.bottomAnchor.constraint(equalTo: navigationActionView.bottomAnchor)
|
||||
|
||||
let constraints = [
|
||||
|
||||
headerStackView.topAnchor.constraint(equalTo: safeAreaLayoutGuide.topAnchor),
|
||||
headerStackView.leadingAnchor.constraint(equalTo: readableContentGuide.leadingAnchor),
|
||||
headerStackView.trailingAnchor.constraint(equalTo: readableContentGuide.trailingAnchor),
|
||||
|
||||
searchTextField.topAnchor.constraint(equalTo: headerStackView.bottomAnchor, constant: 32),
|
||||
searchTextField.leadingAnchor.constraint(equalTo: leadingAnchor, constant: 16),
|
||||
searchTextField.heightAnchor.constraint(equalToConstant: 55),
|
||||
trailingAnchor.constraint(equalTo: searchTextField.trailingAnchor, constant: 16),
|
||||
|
||||
searchTextFieldMagnifyingGlass.topAnchor.constraint(equalTo: searchTextFieldLeftView.topAnchor),
|
||||
searchTextFieldMagnifyingGlass.leadingAnchor.constraint(equalTo: searchTextFieldLeftView.leadingAnchor, constant: 8),
|
||||
searchTextFieldMagnifyingGlass.bottomAnchor.constraint(equalTo: searchTextFieldLeftView.bottomAnchor),
|
||||
|
||||
searchContainerLeftPaddingView.topAnchor.constraint(equalTo: searchTextFieldLeftView.topAnchor),
|
||||
searchContainerLeftPaddingView.leadingAnchor.constraint(equalTo: searchTextFieldMagnifyingGlass.trailingAnchor),
|
||||
searchContainerLeftPaddingView.trailingAnchor.constraint(equalTo: searchTextFieldLeftView.trailingAnchor),
|
||||
searchContainerLeftPaddingView.bottomAnchor.constraint(equalTo: searchTextFieldLeftView.bottomAnchor),
|
||||
searchContainerLeftPaddingView.widthAnchor.constraint(equalToConstant: 4).priority(.defaultHigh),
|
||||
|
||||
tableView.topAnchor.constraint(equalTo: searchTextField.bottomAnchor, constant: 2),
|
||||
tableView.leadingAnchor.constraint(equalTo: leadingAnchor, constant: 16),
|
||||
trailingAnchor.constraint(equalTo: tableView.trailingAnchor, constant: 16),
|
||||
tableView.bottomAnchor.constraint(lessThanOrEqualTo: navigationActionView.topAnchor),
|
||||
|
||||
navigationActionView.leadingAnchor.constraint(equalTo: leadingAnchor),
|
||||
navigationActionView.trailingAnchor.constraint(equalTo: trailingAnchor),
|
||||
bottomConstraint,
|
||||
]
|
||||
|
||||
self.bottomConstraint = bottomConstraint
|
||||
NSLayoutConstraint.activate(constraints)
|
||||
}
|
||||
|
||||
func updateCorners(numberOfResults: Int = 0) {
|
||||
|
||||
tableView.isHidden = (numberOfResults == 0)
|
||||
tableView.layer.maskedCorners = [.layerMinXMaxYCorner, .layerMaxXMaxYCorner]
|
||||
|
||||
let maskedCorners: CACornerMask
|
||||
|
||||
if numberOfResults == 0 {
|
||||
maskedCorners = [.layerMinXMinYCorner, .layerMinXMaxYCorner, .layerMaxXMinYCorner, .layerMaxXMaxYCorner]
|
||||
} else {
|
||||
maskedCorners = [.layerMinXMinYCorner, .layerMaxXMinYCorner]
|
||||
}
|
||||
|
||||
searchTextField.layer.maskedCorners = maskedCorners
|
||||
}
|
||||
}
|
|
@ -0,0 +1,288 @@
|
|||
//
|
||||
// MastodonLoginViewController.swift
|
||||
// Mastodon
|
||||
//
|
||||
// Created by Nathan Mattes on 09.11.22.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
import MastodonSDK
|
||||
import MastodonCore
|
||||
import MastodonAsset
|
||||
import Combine
|
||||
import AuthenticationServices
|
||||
|
||||
protocol MastodonLoginViewControllerDelegate: AnyObject {
|
||||
func backButtonPressed(_ viewController: MastodonLoginViewController)
|
||||
func nextButtonPressed(_ viewController: MastodonLoginViewController)
|
||||
}
|
||||
|
||||
enum MastodonLoginViewSection: Hashable {
|
||||
case servers
|
||||
}
|
||||
|
||||
class MastodonLoginViewController: UIViewController, NeedsDependency {
|
||||
|
||||
weak var delegate: MastodonLoginViewControllerDelegate?
|
||||
var dataSource: UITableViewDiffableDataSource<MastodonLoginViewSection, Mastodon.Entity.Server>?
|
||||
let viewModel: MastodonLoginViewModel
|
||||
let authenticationViewModel: AuthenticationViewModel
|
||||
var mastodonAuthenticationController: MastodonAuthenticationController?
|
||||
|
||||
weak var context: AppContext!
|
||||
weak var coordinator: SceneCoordinator!
|
||||
|
||||
var disposeBag = Set<AnyCancellable>()
|
||||
|
||||
var contentView: MastodonLoginView {
|
||||
view as! MastodonLoginView
|
||||
}
|
||||
|
||||
init(appContext: AppContext, authenticationViewModel: AuthenticationViewModel, sceneCoordinator: SceneCoordinator) {
|
||||
|
||||
viewModel = MastodonLoginViewModel(appContext: appContext)
|
||||
self.authenticationViewModel = authenticationViewModel
|
||||
self.context = appContext
|
||||
self.coordinator = sceneCoordinator
|
||||
|
||||
super.init(nibName: nil, bundle: nil)
|
||||
viewModel.delegate = self
|
||||
|
||||
navigationItem.hidesBackButton = true
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") }
|
||||
|
||||
override func loadView() {
|
||||
let loginView = MastodonLoginView()
|
||||
|
||||
loginView.navigationActionView.nextButton.addTarget(self, action: #selector(MastodonLoginViewController.nextButtonPressed(_:)), for: .touchUpInside)
|
||||
loginView.navigationActionView.backButton.addTarget(self, action: #selector(MastodonLoginViewController.backButtonPressed(_:)), for: .touchUpInside)
|
||||
loginView.searchTextField.addTarget(self, action: #selector(MastodonLoginViewController.textfieldDidChange(_:)), for: .editingChanged)
|
||||
loginView.tableView.delegate = self
|
||||
loginView.tableView.register(MastodonLoginServerTableViewCell.self, forCellReuseIdentifier: MastodonLoginServerTableViewCell.reuseIdentifier)
|
||||
loginView.navigationActionView.nextButton.isEnabled = false
|
||||
|
||||
view = loginView
|
||||
}
|
||||
|
||||
override func viewDidLoad() {
|
||||
super.viewDidLoad()
|
||||
|
||||
NotificationCenter.default.addObserver(self, selector: #selector(keyboardWillShowNotification(_:)), name: UIResponder.keyboardWillShowNotification, object: nil)
|
||||
NotificationCenter.default.addObserver(self, selector: #selector(keyboardWillHideNotification(_:)), name: UIResponder.keyboardWillHideNotification, object: nil)
|
||||
|
||||
let dataSource = UITableViewDiffableDataSource<MastodonLoginViewSection, Mastodon.Entity.Server>(tableView: contentView.tableView) { [weak self] tableView, indexPath, itemIdentifier in
|
||||
guard let cell = tableView.dequeueReusableCell(withIdentifier: MastodonLoginServerTableViewCell.reuseIdentifier, for: indexPath) as? MastodonLoginServerTableViewCell,
|
||||
let self = self else {
|
||||
fatalError("Wrong cell")
|
||||
}
|
||||
|
||||
let server = self.viewModel.filteredServers[indexPath.row]
|
||||
var configuration = cell.defaultContentConfiguration()
|
||||
configuration.text = server.domain
|
||||
|
||||
cell.contentConfiguration = configuration
|
||||
cell.accessoryType = .disclosureIndicator
|
||||
|
||||
cell.backgroundColor = Asset.Scene.Onboarding.textFieldBackground.color
|
||||
|
||||
return cell
|
||||
}
|
||||
|
||||
contentView.tableView.dataSource = dataSource
|
||||
self.dataSource = dataSource
|
||||
|
||||
contentView.updateCorners()
|
||||
|
||||
defer { setupNavigationBarBackgroundView() }
|
||||
setupOnboardingAppearance()
|
||||
}
|
||||
|
||||
override func viewWillAppear(_ animated: Bool) {
|
||||
super.viewWillAppear(animated)
|
||||
|
||||
viewModel.updateServers()
|
||||
}
|
||||
|
||||
override func viewDidAppear(_ animated: Bool) {
|
||||
super.viewDidAppear(animated)
|
||||
|
||||
contentView.searchTextField.becomeFirstResponder()
|
||||
}
|
||||
|
||||
//MARK: - Actions
|
||||
|
||||
@objc func backButtonPressed(_ sender: Any) {
|
||||
contentView.searchTextField.resignFirstResponder()
|
||||
delegate?.backButtonPressed(self)
|
||||
}
|
||||
|
||||
@objc func nextButtonPressed(_ sender: Any) {
|
||||
contentView.searchTextField.resignFirstResponder()
|
||||
delegate?.nextButtonPressed(self)
|
||||
}
|
||||
|
||||
@objc func login() {
|
||||
guard let server = viewModel.selectedServer else { return }
|
||||
|
||||
authenticationViewModel
|
||||
.authenticated
|
||||
.asyncMap { domain, user -> Result<Bool, Error> in
|
||||
do {
|
||||
let result = try await self.context.authenticationService.activeMastodonUser(domain: domain, userID: user.id)
|
||||
return .success(result)
|
||||
} catch {
|
||||
return .failure(error)
|
||||
}
|
||||
}
|
||||
.receive(on: DispatchQueue.main)
|
||||
.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)
|
||||
|
||||
authenticationViewModel.isAuthenticating.send(true)
|
||||
context.apiService.createApplication(domain: server.domain)
|
||||
.tryMap { response -> AuthenticationViewModel.AuthenticateInfo in
|
||||
let application = response.value
|
||||
guard let info = AuthenticationViewModel.AuthenticateInfo(
|
||||
domain: server.domain,
|
||||
application: application,
|
||||
redirectURI: response.value.redirectURI ?? APIService.oauthCallbackURL
|
||||
) else {
|
||||
throw APIService.APIError.explicit(.badResponse)
|
||||
}
|
||||
return info
|
||||
}
|
||||
.receive(on: DispatchQueue.main)
|
||||
.sink { [weak self] completion in
|
||||
guard let self = self else { return }
|
||||
self.authenticationViewModel.isAuthenticating.send(false)
|
||||
|
||||
switch completion {
|
||||
case .failure(let error):
|
||||
let alert = UIAlertController.standardAlert(of: error)
|
||||
self.present(alert, animated: true)
|
||||
case .finished:
|
||||
// do nothing. There's a subscriber above resulting in `coordinator.setup()`
|
||||
break
|
||||
}
|
||||
} receiveValue: { [weak self] info in
|
||||
guard let self else { return }
|
||||
let authenticationController = MastodonAuthenticationController(
|
||||
context: self.context,
|
||||
authenticateURL: info.authorizeURL
|
||||
)
|
||||
|
||||
self.mastodonAuthenticationController = authenticationController
|
||||
authenticationController.authenticationSession?.presentationContextProvider = self
|
||||
authenticationController.authenticationSession?.start()
|
||||
|
||||
self.authenticationViewModel.authenticate(
|
||||
info: info,
|
||||
pinCodePublisher: authenticationController.pinCodePublisher
|
||||
)
|
||||
}
|
||||
.store(in: &disposeBag)
|
||||
}
|
||||
|
||||
@objc func textfieldDidChange(_ textField: UITextField) {
|
||||
viewModel.filterServers(withText: textField.text)
|
||||
|
||||
|
||||
if let text = textField.text,
|
||||
let domain = AuthenticationViewModel.parseDomain(from: text) {
|
||||
|
||||
viewModel.selectedServer = .init(domain: domain, instance: .init(domain: domain))
|
||||
contentView.navigationActionView.nextButton.isEnabled = true
|
||||
} else {
|
||||
viewModel.selectedServer = nil
|
||||
contentView.navigationActionView.nextButton.isEnabled = false
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Notifications
|
||||
@objc func keyboardWillShowNotification(_ notification: Notification) {
|
||||
|
||||
guard let userInfo = notification.userInfo,
|
||||
let keyboardFrameValue = userInfo[UIResponder.keyboardFrameEndUserInfoKey] as? NSValue,
|
||||
let duration = userInfo[UIResponder.keyboardAnimationDurationUserInfoKey] as? NSNumber
|
||||
else { return }
|
||||
|
||||
// inspired by https://stackoverflow.com/a/30245044
|
||||
let keyboardFrame = keyboardFrameValue.cgRectValue
|
||||
|
||||
let keyboardOrigin = view.convert(keyboardFrame.origin, from: nil)
|
||||
let intersectionY = CGRectGetMaxY(view.frame) - keyboardOrigin.y;
|
||||
|
||||
if intersectionY >= 0 {
|
||||
contentView.bottomConstraint?.constant = intersectionY - view.safeAreaInsets.bottom
|
||||
}
|
||||
|
||||
UIView.animate(withDuration: duration.doubleValue, delay: 0, options: .curveEaseInOut) {
|
||||
self.view.layoutIfNeeded()
|
||||
}
|
||||
}
|
||||
|
||||
@objc func keyboardWillHideNotification(_ notification: Notification) {
|
||||
|
||||
guard let userInfo = notification.userInfo,
|
||||
let duration = userInfo[UIResponder.keyboardAnimationDurationUserInfoKey] as? NSNumber
|
||||
else { return }
|
||||
|
||||
contentView.bottomConstraint?.constant = 0
|
||||
|
||||
UIView.animate(withDuration: duration.doubleValue, delay: 0, options: .curveEaseInOut) {
|
||||
self.view.layoutIfNeeded()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - OnboardingViewControllerAppearance
|
||||
extension MastodonLoginViewController: OnboardingViewControllerAppearance { }
|
||||
|
||||
// MARK: - UITableViewDelegate
|
||||
extension MastodonLoginViewController: UITableViewDelegate {
|
||||
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
|
||||
let server = viewModel.filteredServers[indexPath.row]
|
||||
viewModel.selectedServer = server
|
||||
|
||||
contentView.searchTextField.text = server.domain
|
||||
viewModel.filterServers(withText: " ")
|
||||
|
||||
contentView.navigationActionView.nextButton.isEnabled = true
|
||||
tableView.deselectRow(at: indexPath, animated: true)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - MastodonLoginViewModelDelegate
|
||||
extension MastodonLoginViewController: MastodonLoginViewModelDelegate {
|
||||
func serversUpdated(_ viewModel: MastodonLoginViewModel) {
|
||||
var snapshot = NSDiffableDataSourceSnapshot<MastodonLoginViewSection, Mastodon.Entity.Server>()
|
||||
|
||||
snapshot.appendSections([MastodonLoginViewSection.servers])
|
||||
snapshot.appendItems(viewModel.filteredServers)
|
||||
|
||||
dataSource?.applySnapshot(snapshot, animated: false)
|
||||
|
||||
OperationQueue.main.addOperation {
|
||||
let numberOfResults = viewModel.filteredServers.count
|
||||
self.contentView.updateCorners(numberOfResults: numberOfResults)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - ASWebAuthenticationPresentationContextProviding
|
||||
extension MastodonLoginViewController: ASWebAuthenticationPresentationContextProviding {
|
||||
func presentationAnchor(for session: ASWebAuthenticationSession) -> ASPresentationAnchor {
|
||||
return view.window!
|
||||
}
|
||||
}
|
|
@ -0,0 +1,57 @@
|
|||
//
|
||||
// MastodonLoginViewModel.swift
|
||||
// Mastodon
|
||||
//
|
||||
// Created by Nathan Mattes on 11.11.22.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import MastodonSDK
|
||||
import MastodonCore
|
||||
import Combine
|
||||
|
||||
protocol MastodonLoginViewModelDelegate: AnyObject {
|
||||
func serversUpdated(_ viewModel: MastodonLoginViewModel)
|
||||
}
|
||||
|
||||
class MastodonLoginViewModel {
|
||||
|
||||
private var serverList: [Mastodon.Entity.Server] = []
|
||||
var selectedServer: Mastodon.Entity.Server?
|
||||
var filteredServers: [Mastodon.Entity.Server] = []
|
||||
|
||||
weak var appContext: AppContext?
|
||||
weak var delegate: MastodonLoginViewModelDelegate?
|
||||
var disposeBag = Set<AnyCancellable>()
|
||||
|
||||
init(appContext: AppContext) {
|
||||
self.appContext = appContext
|
||||
}
|
||||
|
||||
func updateServers() {
|
||||
appContext?.apiService.servers().sink(receiveCompletion: { [weak self] completion in
|
||||
switch completion {
|
||||
case .finished:
|
||||
guard let self = self else { return }
|
||||
|
||||
self.delegate?.serversUpdated(self)
|
||||
case .failure(let error):
|
||||
print(error)
|
||||
}
|
||||
}, receiveValue: { content in
|
||||
let servers = content.value
|
||||
self.serverList = servers
|
||||
}).store(in: &disposeBag)
|
||||
}
|
||||
|
||||
func filterServers(withText query: String?) {
|
||||
guard let query else {
|
||||
filteredServers = serverList
|
||||
delegate?.serversUpdated(self)
|
||||
return
|
||||
}
|
||||
|
||||
filteredServers = serverList.filter { $0.domain.lowercased().contains(query) }.sorted {$0.totalUsers > $1.totalUsers }
|
||||
delegate?.serversUpdated(self)
|
||||
}
|
||||
}
|
|
@ -143,8 +143,7 @@ extension MastodonPickServerViewController {
|
|||
viewModel.setupDiffableDataSource(
|
||||
for: tableView,
|
||||
dependency: self,
|
||||
pickServerServerSectionTableHeaderViewDelegate: self,
|
||||
pickServerCellDelegate: self
|
||||
pickServerServerSectionTableHeaderViewDelegate: self
|
||||
)
|
||||
|
||||
KeyboardResponderService
|
||||
|
@ -172,7 +171,7 @@ extension MastodonPickServerViewController {
|
|||
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(
|
||||
_ = self.coordinator.present(
|
||||
scene: .alertController(alertController: alertController),
|
||||
from: nil,
|
||||
transition: .alertController(animated: true, completion: nil)
|
||||
|
@ -271,59 +270,6 @@ extension MastodonPickServerViewController {
|
|||
}
|
||||
|
||||
@objc private func nextButtonDidPressed(_ sender: UIButton) {
|
||||
switch viewModel.mode {
|
||||
case .signIn: doSignIn()
|
||||
case .signUp: doSignUp()
|
||||
}
|
||||
}
|
||||
|
||||
private func doSignIn() {
|
||||
guard let server = viewModel.selectedServer.value else { return }
|
||||
authenticationViewModel.isAuthenticating.send(true)
|
||||
context.apiService.createApplication(domain: server.domain)
|
||||
.tryMap { response -> AuthenticationViewModel.AuthenticateInfo in
|
||||
let application = response.value
|
||||
guard let info = AuthenticationViewModel.AuthenticateInfo(
|
||||
domain: server.domain,
|
||||
application: application,
|
||||
redirectURI: response.value.redirectURI ?? APIService.oauthCallbackURL
|
||||
) else {
|
||||
throw APIService.APIError.explicit(.badResponse)
|
||||
}
|
||||
return info
|
||||
}
|
||||
.receive(on: DispatchQueue.main)
|
||||
.sink { [weak self] completion in
|
||||
guard let self = self else { return }
|
||||
self.authenticationViewModel.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 authenticationController = MastodonAuthenticationController(
|
||||
context: self.context,
|
||||
authenticateURL: info.authorizeURL
|
||||
)
|
||||
|
||||
self.mastodonAuthenticationController = authenticationController
|
||||
authenticationController.authenticationSession?.presentationContextProvider = self
|
||||
authenticationController.authenticationSession?.start()
|
||||
|
||||
self.authenticationViewModel.authenticate(
|
||||
info: info,
|
||||
pinCodePublisher: authenticationController.pinCodePublisher
|
||||
)
|
||||
}
|
||||
.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 }
|
||||
authenticationViewModel.isAuthenticating.send(true)
|
||||
|
@ -394,7 +340,7 @@ extension MastodonPickServerViewController {
|
|||
instance: response.instance.value,
|
||||
applicationToken: response.applicationToken.value
|
||||
)
|
||||
self.coordinator.present(scene: .mastodonServerRules(viewModel: mastodonServerRulesViewModel), from: self, transition: .show)
|
||||
_ = self.coordinator.present(scene: .mastodonServerRules(viewModel: mastodonServerRulesViewModel), from: self, transition: .show)
|
||||
} else {
|
||||
let mastodonRegisterViewModel = MastodonRegisterViewModel(
|
||||
context: self.context,
|
||||
|
@ -403,7 +349,7 @@ extension MastodonPickServerViewController {
|
|||
instance: response.instance.value,
|
||||
applicationToken: response.applicationToken.value
|
||||
)
|
||||
self.coordinator.present(scene: .mastodonRegister(viewModel: mastodonRegisterViewModel), from: nil, transition: .show)
|
||||
_ = self.coordinator.present(scene: .mastodonRegister(viewModel: mastodonRegisterViewModel), from: nil, transition: .show)
|
||||
}
|
||||
}
|
||||
.store(in: &disposeBag)
|
||||
|
@ -503,17 +449,5 @@ extension MastodonPickServerViewController: PickServerServerSectionTableHeaderVi
|
|||
}
|
||||
}
|
||||
|
||||
// MARK: - PickServerCellDelegate
|
||||
extension MastodonPickServerViewController: PickServerCellDelegate {
|
||||
|
||||
}
|
||||
|
||||
// MARK: - OnboardingViewControllerAppearance
|
||||
extension MastodonPickServerViewController: OnboardingViewControllerAppearance { }
|
||||
|
||||
// MARK: - ASWebAuthenticationPresentationContextProviding
|
||||
extension MastodonPickServerViewController: ASWebAuthenticationPresentationContextProviding {
|
||||
func presentationAnchor(for session: ASWebAuthenticationSession) -> ASPresentationAnchor {
|
||||
return view.window!
|
||||
}
|
||||
}
|
||||
|
|
|
@ -13,8 +13,7 @@ extension MastodonPickServerViewModel {
|
|||
func setupDiffableDataSource(
|
||||
for tableView: UITableView,
|
||||
dependency: NeedsDependency,
|
||||
pickServerServerSectionTableHeaderViewDelegate: PickServerServerSectionTableHeaderViewDelegate,
|
||||
pickServerCellDelegate: PickServerCellDelegate
|
||||
pickServerServerSectionTableHeaderViewDelegate: PickServerServerSectionTableHeaderViewDelegate
|
||||
) {
|
||||
// set section header
|
||||
serverSectionHeaderView.diffableDataSource = CategoryPickerSection.collectionViewDiffableDataSource(
|
||||
|
@ -34,8 +33,7 @@ extension MastodonPickServerViewModel {
|
|||
// set tableView
|
||||
diffableDataSource = PickServerSection.tableViewDiffableDataSource(
|
||||
for: tableView,
|
||||
dependency: dependency,
|
||||
pickServerCellDelegate: pickServerCellDelegate
|
||||
dependency: dependency
|
||||
)
|
||||
|
||||
var snapshot = NSDiffableDataSourceSnapshot<PickServerSection, PickServerItem>()
|
||||
|
|
|
@ -17,12 +17,7 @@ import MastodonCore
|
|||
import MastodonUI
|
||||
|
||||
class MastodonPickServerViewModel: NSObject {
|
||||
|
||||
enum PickServerMode {
|
||||
case signUp
|
||||
case signIn
|
||||
}
|
||||
|
||||
|
||||
enum EmptyStateViewState {
|
||||
case none
|
||||
case loading
|
||||
|
@ -34,7 +29,6 @@ class MastodonPickServerViewModel: NSObject {
|
|||
let serverSectionHeaderView = PickServerServerSectionTableHeaderView()
|
||||
|
||||
// input
|
||||
let mode: PickServerMode
|
||||
let context: AppContext
|
||||
var categoryPickerItems: [CategoryPickerItem] = {
|
||||
var items: [CategoryPickerItem] = []
|
||||
|
@ -72,9 +66,8 @@ class MastodonPickServerViewModel: NSObject {
|
|||
let loadingIndexedServersError = CurrentValueSubject<Error?, Never>(nil)
|
||||
let emptyStateViewState = CurrentValueSubject<EmptyStateViewState, Never>(.none)
|
||||
|
||||
init(context: AppContext, mode: PickServerMode) {
|
||||
init(context: AppContext) {
|
||||
self.context = context
|
||||
self.mode = mode
|
||||
super.init()
|
||||
|
||||
configure()
|
||||
|
@ -115,9 +108,7 @@ extension MastodonPickServerViewModel {
|
|||
.map { indexedServers, selectCategoryItem, searchText -> [Mastodon.Entity.Server] in
|
||||
// ignore approval required servers when sign-up
|
||||
var indexedServers = indexedServers
|
||||
if self.mode == .signUp {
|
||||
indexedServers = indexedServers.filter { !$0.approvalRequired }
|
||||
}
|
||||
indexedServers = indexedServers.filter { !$0.approvalRequired }
|
||||
// Note:
|
||||
// sort by calculate last week users count
|
||||
// and make medium size (~800) server to top
|
||||
|
|
|
@ -14,14 +14,8 @@ import Kanna
|
|||
import MastodonAsset
|
||||
import MastodonLocalization
|
||||
|
||||
protocol PickServerCellDelegate: AnyObject {
|
||||
// func pickServerCell(_ cell: PickServerCell, expandButtonPressed button: UIButton)
|
||||
}
|
||||
|
||||
class PickServerCell: UITableViewCell {
|
||||
|
||||
weak var delegate: PickServerCellDelegate?
|
||||
|
||||
var disposeBag = Set<AnyCancellable>()
|
||||
|
||||
let containerView: UIStackView = {
|
||||
|
@ -88,7 +82,7 @@ class PickServerCell: UITableViewCell {
|
|||
label.adjustsFontForContentSizeCategory = true
|
||||
return label
|
||||
}()
|
||||
|
||||
|
||||
private var collapseConstraints: [NSLayoutConstraint] = []
|
||||
private var expandConstraints: [NSLayoutConstraint] = []
|
||||
|
||||
|
|
|
@ -19,13 +19,6 @@ class PickServerCategoryView: UIView {
|
|||
return view
|
||||
}()
|
||||
|
||||
let emojiLabel: UILabel = {
|
||||
let label = UILabel()
|
||||
label.textAlignment = .center
|
||||
label.font = .systemFont(ofSize: 34, weight: .regular)
|
||||
return label
|
||||
}()
|
||||
|
||||
let titleLabel: UILabel = {
|
||||
let label = UILabel()
|
||||
label.textAlignment = .center
|
||||
|
@ -50,6 +43,7 @@ extension PickServerCategoryView {
|
|||
private func configure() {
|
||||
let container = UIStackView()
|
||||
container.axis = .vertical
|
||||
container.spacing = 2
|
||||
container.distribution = .fillProportionally
|
||||
|
||||
container.translatesAutoresizingMaskIntoConstraints = false
|
||||
|
@ -61,12 +55,11 @@ extension PickServerCategoryView {
|
|||
container.bottomAnchor.constraint(equalTo: bottomAnchor),
|
||||
])
|
||||
|
||||
container.addArrangedSubview(emojiLabel)
|
||||
container.addArrangedSubview(titleLabel)
|
||||
highlightedIndicatorView.translatesAutoresizingMaskIntoConstraints = false
|
||||
container.addArrangedSubview(highlightedIndicatorView)
|
||||
NSLayoutConstraint.activate([
|
||||
highlightedIndicatorView.heightAnchor.constraint(equalToConstant: 3).priority(.required - 1),
|
||||
highlightedIndicatorView.heightAnchor.constraint(equalToConstant: 3)//.priority(.required - 1),
|
||||
])
|
||||
titleLabel.setContentHuggingPriority(.required - 1, for: .vertical)
|
||||
}
|
||||
|
|
|
@ -19,7 +19,7 @@ protocol PickServerServerSectionTableHeaderViewDelegate: AnyObject {
|
|||
|
||||
final class PickServerServerSectionTableHeaderView: UIView {
|
||||
|
||||
static let collectionViewHeight: CGFloat = 88
|
||||
static let collectionViewHeight: CGFloat = 30
|
||||
static let searchTextFieldHeight: CGFloat = 38
|
||||
static let spacing: CGFloat = 11
|
||||
|
||||
|
@ -177,7 +177,6 @@ extension PickServerServerSectionTableHeaderView {
|
|||
extension PickServerServerSectionTableHeaderView: UICollectionViewDelegate {
|
||||
|
||||
func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
|
||||
os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s: indexPath: %s", ((#file as NSString).lastPathComponent), #line, #function, indexPath.debugDescription)
|
||||
collectionView.selectItem(at: indexPath, animated: true, scrollPosition: .centeredHorizontally)
|
||||
delegate?.pickServerServerSectionTableHeaderView(self, collectionView: collectionView, didSelectItemAt: indexPath)
|
||||
}
|
||||
|
@ -205,5 +204,5 @@ extension PickServerServerSectionTableHeaderView: UITextFieldDelegate {
|
|||
textField.resignFirstResponder()
|
||||
return false
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
|
|
@ -1,116 +0,0 @@
|
|||
//
|
||||
// MastodonRegisterAvatarTableViewCell.swift
|
||||
// Mastodon
|
||||
//
|
||||
// Created by MainasuK on 2022-1-5.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
import Combine
|
||||
import MastodonAsset
|
||||
import MastodonLocalization
|
||||
|
||||
final class MastodonRegisterAvatarTableViewCell: UITableViewCell {
|
||||
|
||||
static let containerSize = CGSize(width: 88, height: 88)
|
||||
|
||||
var disposeBag = Set<AnyCancellable>()
|
||||
|
||||
let containerView: UIView = {
|
||||
let view = UIView()
|
||||
view.backgroundColor = .clear
|
||||
view.layer.masksToBounds = true
|
||||
view.layer.cornerCurve = .continuous
|
||||
view.layer.cornerRadius = 22
|
||||
return view
|
||||
}()
|
||||
|
||||
let avatarButton: HighlightDimmableButton = {
|
||||
let button = HighlightDimmableButton()
|
||||
button.backgroundColor = Asset.Theme.Mastodon.secondaryGroupedSystemBackground.color
|
||||
button.setImage(Asset.Scene.Onboarding.avatarPlaceholder.image, for: .normal)
|
||||
return button
|
||||
}()
|
||||
|
||||
let editBannerView: UIView = {
|
||||
let bannerView = UIView()
|
||||
bannerView.backgroundColor = UIColor.black.withAlphaComponent(0.5)
|
||||
bannerView.isUserInteractionEnabled = false
|
||||
|
||||
let label: UILabel = {
|
||||
let label = UILabel()
|
||||
label.textColor = .white
|
||||
label.text = L10n.Common.Controls.Actions.edit
|
||||
label.font = .systemFont(ofSize: 13, weight: .semibold)
|
||||
label.textAlignment = .center
|
||||
label.minimumScaleFactor = 0.5
|
||||
label.adjustsFontSizeToFitWidth = true
|
||||
return label
|
||||
}()
|
||||
|
||||
label.translatesAutoresizingMaskIntoConstraints = false
|
||||
bannerView.addSubview(label)
|
||||
NSLayoutConstraint.activate([
|
||||
label.topAnchor.constraint(equalTo: bannerView.topAnchor),
|
||||
label.leadingAnchor.constraint(equalTo: bannerView.leadingAnchor),
|
||||
label.trailingAnchor.constraint(equalTo: bannerView.trailingAnchor),
|
||||
label.bottomAnchor.constraint(equalTo: bannerView.bottomAnchor),
|
||||
])
|
||||
|
||||
return bannerView
|
||||
}()
|
||||
|
||||
override func prepareForReuse() {
|
||||
super.prepareForReuse()
|
||||
|
||||
disposeBag.removeAll()
|
||||
}
|
||||
|
||||
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
|
||||
super.init(style: style, reuseIdentifier: reuseIdentifier)
|
||||
_init()
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
super.init(coder: coder)
|
||||
_init()
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
extension MastodonRegisterAvatarTableViewCell {
|
||||
|
||||
private func _init() {
|
||||
selectionStyle = .none
|
||||
backgroundColor = .clear
|
||||
|
||||
containerView.translatesAutoresizingMaskIntoConstraints = false
|
||||
contentView.addSubview(containerView)
|
||||
NSLayoutConstraint.activate([
|
||||
containerView.topAnchor.constraint(equalTo: contentView.topAnchor, constant: 22),
|
||||
containerView.centerXAnchor.constraint(equalTo: contentView.centerXAnchor),
|
||||
contentView.bottomAnchor.constraint(equalTo: containerView.bottomAnchor, constant: 8),
|
||||
containerView.widthAnchor.constraint(equalToConstant: MastodonRegisterAvatarTableViewCell.containerSize.width).priority(.required - 1),
|
||||
containerView.heightAnchor.constraint(equalToConstant: MastodonRegisterAvatarTableViewCell.containerSize.height).priority(.required - 1),
|
||||
])
|
||||
|
||||
avatarButton.translatesAutoresizingMaskIntoConstraints = false
|
||||
containerView.addSubview(avatarButton)
|
||||
NSLayoutConstraint.activate([
|
||||
avatarButton.topAnchor.constraint(equalTo: containerView.topAnchor),
|
||||
avatarButton.leadingAnchor.constraint(equalTo: containerView.leadingAnchor),
|
||||
avatarButton.trailingAnchor.constraint(equalTo: containerView.trailingAnchor),
|
||||
avatarButton.bottomAnchor.constraint(equalTo: containerView.bottomAnchor),
|
||||
])
|
||||
|
||||
editBannerView.translatesAutoresizingMaskIntoConstraints = false
|
||||
containerView.addSubview(editBannerView)
|
||||
NSLayoutConstraint.activate([
|
||||
editBannerView.leadingAnchor.constraint(equalTo: containerView.leadingAnchor),
|
||||
editBannerView.trailingAnchor.constraint(equalTo: containerView.trailingAnchor),
|
||||
editBannerView.bottomAnchor.constraint(equalTo: containerView.bottomAnchor),
|
||||
editBannerView.heightAnchor.constraint(equalToConstant: 22),
|
||||
])
|
||||
}
|
||||
|
||||
}
|
|
@ -1,50 +0,0 @@
|
|||
//
|
||||
// MastodonRegisterPasswordHintTableViewCell.swift
|
||||
// Mastodon
|
||||
//
|
||||
// Created by MainasuK on 2022-1-7.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
import MastodonAsset
|
||||
import MastodonLocalization
|
||||
|
||||
final class MastodonRegisterPasswordHintTableViewCell: UITableViewCell {
|
||||
|
||||
let passwordRuleLabel: UILabel = {
|
||||
let label = UILabel()
|
||||
label.font = .preferredFont(forTextStyle: .footnote)
|
||||
label.textColor = Asset.Colors.Label.secondary.color
|
||||
label.text = L10n.Scene.Register.Input.Password.hint
|
||||
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 MastodonRegisterPasswordHintTableViewCell {
|
||||
|
||||
private func _init() {
|
||||
selectionStyle = .none
|
||||
backgroundColor = .clear
|
||||
|
||||
passwordRuleLabel.translatesAutoresizingMaskIntoConstraints = false
|
||||
contentView.addSubview(passwordRuleLabel)
|
||||
NSLayoutConstraint.activate([
|
||||
passwordRuleLabel.topAnchor.constraint(equalTo: contentView.topAnchor),
|
||||
passwordRuleLabel.leadingAnchor.constraint(equalTo: contentView.readableContentGuide.leadingAnchor),
|
||||
passwordRuleLabel.trailingAnchor.constraint(equalTo: contentView.readableContentGuide.trailingAnchor),
|
||||
passwordRuleLabel.bottomAnchor.constraint(equalTo: contentView.bottomAnchor),
|
||||
])
|
||||
}
|
||||
|
||||
}
|
|
@ -1,142 +0,0 @@
|
|||
//
|
||||
// MastodonRegisterTextFieldTableViewCell.swift
|
||||
// Mastodon
|
||||
//
|
||||
// Created by MainasuK on 2022-1-7.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
import Combine
|
||||
import MastodonUI
|
||||
import MastodonAsset
|
||||
import MastodonLocalization
|
||||
|
||||
final class MastodonRegisterTextFieldTableViewCell: UITableViewCell {
|
||||
|
||||
static let textFieldHeight: CGFloat = 50
|
||||
static let textFieldLabelFont = UIFontMetrics(forTextStyle: .headline).scaledFont(for: .systemFont(ofSize: 17, weight: .semibold), maximumPointSize: 22)
|
||||
|
||||
var disposeBag = Set<AnyCancellable>()
|
||||
|
||||
let textFieldShadowContainer = ShadowBackgroundContainer()
|
||||
let textField: UITextField = {
|
||||
let textField = UITextField()
|
||||
textField.font = MastodonRegisterTextFieldTableViewCell.textFieldLabelFont
|
||||
textField.backgroundColor = Asset.Scene.Onboarding.textFieldBackground.color
|
||||
textField.layer.masksToBounds = true
|
||||
textField.layer.cornerRadius = 10
|
||||
textField.layer.cornerCurve = .continuous
|
||||
return textField
|
||||
}()
|
||||
|
||||
override func prepareForReuse() {
|
||||
super.prepareForReuse()
|
||||
|
||||
disposeBag.removeAll()
|
||||
textFieldShadowContainer.shadowColor = .black
|
||||
textFieldShadowContainer.shadowAlpha = 0.25
|
||||
resetTextField()
|
||||
}
|
||||
|
||||
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
|
||||
super.init(style: style, reuseIdentifier: reuseIdentifier)
|
||||
_init()
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
super.init(coder: coder)
|
||||
_init()
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
extension MastodonRegisterTextFieldTableViewCell {
|
||||
|
||||
private func _init() {
|
||||
selectionStyle = .none
|
||||
backgroundColor = .clear
|
||||
|
||||
textFieldShadowContainer.translatesAutoresizingMaskIntoConstraints = false
|
||||
contentView.addSubview(textFieldShadowContainer)
|
||||
NSLayoutConstraint.activate([
|
||||
textFieldShadowContainer.topAnchor.constraint(equalTo: contentView.topAnchor, constant: 6),
|
||||
textFieldShadowContainer.leadingAnchor.constraint(equalTo: contentView.readableContentGuide.leadingAnchor),
|
||||
textFieldShadowContainer.trailingAnchor.constraint(equalTo: contentView.readableContentGuide.trailingAnchor),
|
||||
contentView.bottomAnchor.constraint(equalTo: textFieldShadowContainer.bottomAnchor, constant: 6),
|
||||
])
|
||||
|
||||
textField.translatesAutoresizingMaskIntoConstraints = false
|
||||
textFieldShadowContainer.addSubview(textField)
|
||||
NSLayoutConstraint.activate([
|
||||
textField.topAnchor.constraint(equalTo: textFieldShadowContainer.topAnchor),
|
||||
textField.leadingAnchor.constraint(equalTo: textFieldShadowContainer.leadingAnchor),
|
||||
textField.trailingAnchor.constraint(equalTo: textFieldShadowContainer.trailingAnchor),
|
||||
textField.bottomAnchor.constraint(equalTo: textFieldShadowContainer.bottomAnchor),
|
||||
textField.heightAnchor.constraint(equalToConstant: MastodonRegisterTextFieldTableViewCell.textFieldHeight).priority(.required - 1),
|
||||
])
|
||||
|
||||
resetTextField()
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
extension MastodonRegisterTextFieldTableViewCell {
|
||||
func resetTextField() {
|
||||
textField.keyboardType = .default
|
||||
textField.autocorrectionType = .default
|
||||
textField.autocapitalizationType = .none
|
||||
textField.attributedPlaceholder = nil
|
||||
textField.isSecureTextEntry = false
|
||||
textField.textAlignment = .natural
|
||||
textField.semanticContentAttribute = .unspecified
|
||||
|
||||
let paddingRect = CGRect(x: 0, y: 0, width: 16, height: 10)
|
||||
textField.leftView = UIView(frame: paddingRect)
|
||||
textField.leftViewMode = .always
|
||||
textField.rightView = UIView(frame: paddingRect)
|
||||
textField.rightViewMode = .always
|
||||
}
|
||||
|
||||
func setupTextViewRightView(text: String) {
|
||||
textField.rightView = {
|
||||
let containerView = UIView()
|
||||
|
||||
let paddingView = UIView(frame: CGRect(x: 0, y: 0, width: 8, height: MastodonRegisterTextFieldTableViewCell.textFieldHeight))
|
||||
paddingView.translatesAutoresizingMaskIntoConstraints = false
|
||||
containerView.addSubview(paddingView)
|
||||
NSLayoutConstraint.activate([
|
||||
paddingView.topAnchor.constraint(equalTo: containerView.topAnchor),
|
||||
paddingView.leadingAnchor.constraint(equalTo: containerView.leadingAnchor),
|
||||
paddingView.bottomAnchor.constraint(equalTo: containerView.bottomAnchor),
|
||||
paddingView.widthAnchor.constraint(equalToConstant: 8).priority(.defaultHigh),
|
||||
])
|
||||
|
||||
let label = UILabel()
|
||||
label.font = MastodonRegisterTextFieldTableViewCell.textFieldLabelFont
|
||||
label.textColor = Asset.Colors.Label.primary.color
|
||||
label.text = text
|
||||
label.lineBreakMode = .byTruncatingMiddle
|
||||
|
||||
label.translatesAutoresizingMaskIntoConstraints = false
|
||||
containerView.addSubview(label)
|
||||
NSLayoutConstraint.activate([
|
||||
label.topAnchor.constraint(equalTo: containerView.topAnchor),
|
||||
label.leadingAnchor.constraint(equalTo: paddingView.trailingAnchor),
|
||||
containerView.trailingAnchor.constraint(equalTo: label.trailingAnchor, constant: 16),
|
||||
label.bottomAnchor.constraint(equalTo: containerView.bottomAnchor),
|
||||
label.widthAnchor.constraint(lessThanOrEqualToConstant: 180).priority(.required - 1),
|
||||
])
|
||||
return containerView
|
||||
}()
|
||||
}
|
||||
|
||||
func setupTextViewPlaceholder(text: String) {
|
||||
textField.attributedPlaceholder = NSAttributedString(
|
||||
string: text,
|
||||
attributes: [
|
||||
.foregroundColor: Asset.Colors.Label.secondary.color,
|
||||
.font: MastodonRegisterTextFieldTableViewCell.textFieldLabelFont
|
||||
]
|
||||
)
|
||||
}
|
||||
}
|
|
@ -168,21 +168,27 @@ struct MastodonRegisterView: View {
|
|||
|
||||
func body(content: Content) -> some View {
|
||||
ZStack {
|
||||
let shadowColor: Color = {
|
||||
let borderColor: Color = {
|
||||
switch validateState {
|
||||
case .empty: return .black.opacity(0.125)
|
||||
case .invalid: return Color(Asset.Colors.TextField.invalid.color)
|
||||
case .valid: return Color(Asset.Colors.TextField.valid.color)
|
||||
case .empty: return Color(Asset.Scene.Onboarding.textFieldBackground.color)
|
||||
case .invalid: return Color(Asset.Colors.TextField.invalid.color)
|
||||
case .valid: return Color(Asset.Scene.Onboarding.textFieldBackground.color)
|
||||
}
|
||||
}()
|
||||
|
||||
Color(Asset.Scene.Onboarding.textFieldBackground.color)
|
||||
.cornerRadius(10)
|
||||
.shadow(color: shadowColor, radius: 1, x: 0, y: 2)
|
||||
.animation(.easeInOut, value: validateState)
|
||||
.shadow(color: .black.opacity(0.125), radius: 1, x: 0, y: 2)
|
||||
|
||||
content
|
||||
.padding()
|
||||
.background(Color(Asset.Scene.Onboarding.textFieldBackground.color))
|
||||
.cornerRadius(10)
|
||||
.overlay(
|
||||
RoundedRectangle(cornerRadius: 10)
|
||||
.stroke(borderColor, lineWidth: 1)
|
||||
.animation(.easeInOut, value: validateState)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -145,7 +145,7 @@ extension MastodonRegisterViewController {
|
|||
let alertController = UIAlertController(for: error, title: "Sign Up Failure", preferredStyle: .alert)
|
||||
let okAction = UIAlertAction(title: L10n.Common.Controls.Actions.ok, style: .default, handler: nil)
|
||||
alertController.addAction(okAction)
|
||||
self.coordinator.present(
|
||||
_ = self.coordinator.present(
|
||||
scene: .alertController(alertController: alertController),
|
||||
from: nil,
|
||||
transition: .alertController(animated: true, completion: nil)
|
||||
|
@ -322,7 +322,7 @@ extension MastodonRegisterViewController {
|
|||
)
|
||||
}()
|
||||
let viewModel = MastodonConfirmEmailViewModel(context: self.context, email: email, authenticateInfo: self.viewModel.authenticateInfo, userToken: userToken, updateCredentialQuery: updateCredentialQuery)
|
||||
self.coordinator.present(scene: .mastodonConfirmEmail(viewModel: viewModel), from: self, transition: .show)
|
||||
_ = self.coordinator.present(scene: .mastodonConfirmEmail(viewModel: viewModel), from: self, transition: .show)
|
||||
}
|
||||
.store(in: &disposeBag)
|
||||
}
|
||||
|
|
|
@ -10,145 +10,6 @@ import Combine
|
|||
import MastodonAsset
|
||||
import MastodonLocalization
|
||||
|
||||
extension MastodonRegisterViewModel {
|
||||
func setupDiffableDataSource(
|
||||
tableView: UITableView
|
||||
) {
|
||||
tableView.register(OnboardingHeadlineTableViewCell.self, forCellReuseIdentifier: String(describing: OnboardingHeadlineTableViewCell.self))
|
||||
tableView.register(MastodonRegisterAvatarTableViewCell.self, forCellReuseIdentifier: String(describing: MastodonRegisterAvatarTableViewCell.self))
|
||||
tableView.register(MastodonRegisterTextFieldTableViewCell.self, forCellReuseIdentifier: String(describing: MastodonRegisterTextFieldTableViewCell.self))
|
||||
tableView.register(MastodonRegisterPasswordHintTableViewCell.self, forCellReuseIdentifier: String(describing: MastodonRegisterPasswordHintTableViewCell.self))
|
||||
|
||||
diffableDataSource = UITableViewDiffableDataSource(tableView: tableView) { tableView, indexPath, item in
|
||||
switch item {
|
||||
case .header(let domain):
|
||||
let cell = tableView.dequeueReusableCell(withIdentifier: String(describing: OnboardingHeadlineTableViewCell.self), for: indexPath) as! OnboardingHeadlineTableViewCell
|
||||
cell.titleLabel.text = L10n.Scene.Register.letsGetYouSetUpOnDomain(domain)
|
||||
cell.subTitleLabel.isHidden = true
|
||||
return cell
|
||||
case .avatar:
|
||||
let cell = tableView.dequeueReusableCell(withIdentifier: String(describing: MastodonRegisterAvatarTableViewCell.self), for: indexPath) as! MastodonRegisterAvatarTableViewCell
|
||||
self.configureAvatar(cell: cell)
|
||||
return cell
|
||||
case .name:
|
||||
let cell = tableView.dequeueReusableCell(withIdentifier: String(describing: MastodonRegisterTextFieldTableViewCell.self), for: indexPath) as! MastodonRegisterTextFieldTableViewCell
|
||||
cell.setupTextViewPlaceholder(text: L10n.Scene.Register.Input.DisplayName.placeholder)
|
||||
cell.textField.keyboardType = .default
|
||||
cell.textField.autocapitalizationType = .words
|
||||
cell.textField.text = self.name
|
||||
NotificationCenter.default.publisher(for: UITextField.textDidChangeNotification, object: cell.textField)
|
||||
.receive(on: DispatchQueue.main)
|
||||
.compactMap { notification in
|
||||
guard let textField = notification.object as? UITextField else {
|
||||
assertionFailure()
|
||||
return nil
|
||||
}
|
||||
return textField.text?.trimmingCharacters(in: .whitespacesAndNewlines) ?? ""
|
||||
}
|
||||
.assign(to: \.name, on: self)
|
||||
.store(in: &cell.disposeBag)
|
||||
return cell
|
||||
case .username:
|
||||
let cell = tableView.dequeueReusableCell(withIdentifier: String(describing: MastodonRegisterTextFieldTableViewCell.self), for: indexPath) as! MastodonRegisterTextFieldTableViewCell
|
||||
cell.setupTextViewRightView(text: "@" + self.domain)
|
||||
cell.setupTextViewPlaceholder(text: L10n.Scene.Register.Input.Username.placeholder)
|
||||
cell.textField.keyboardType = .alphabet
|
||||
cell.textField.autocorrectionType = .no
|
||||
cell.textField.text = self.username
|
||||
cell.textField.textAlignment = .left
|
||||
cell.textField.semanticContentAttribute = .forceLeftToRight
|
||||
NotificationCenter.default.publisher(for: UITextField.textDidChangeNotification, object: cell.textField)
|
||||
.receive(on: DispatchQueue.main)
|
||||
.compactMap { notification in
|
||||
guard let textField = notification.object as? UITextField else {
|
||||
assertionFailure()
|
||||
return nil
|
||||
}
|
||||
return textField.text?.trimmingCharacters(in: .whitespacesAndNewlines) ?? ""
|
||||
}
|
||||
.assign(to: \.username, on: self)
|
||||
.store(in: &cell.disposeBag)
|
||||
self.configureTextFieldCell(cell: cell, validateState: self.$usernameValidateState)
|
||||
return cell
|
||||
case .email:
|
||||
let cell = tableView.dequeueReusableCell(withIdentifier: String(describing: MastodonRegisterTextFieldTableViewCell.self), for: indexPath) as! MastodonRegisterTextFieldTableViewCell
|
||||
cell.setupTextViewPlaceholder(text: L10n.Scene.Register.Input.Email.placeholder)
|
||||
cell.textField.keyboardType = .emailAddress
|
||||
cell.textField.autocorrectionType = .no
|
||||
cell.textField.text = self.email
|
||||
NotificationCenter.default.publisher(for: UITextField.textDidChangeNotification, object: cell.textField)
|
||||
.receive(on: DispatchQueue.main)
|
||||
.compactMap { notification in
|
||||
guard let textField = notification.object as? UITextField else {
|
||||
assertionFailure()
|
||||
return nil
|
||||
}
|
||||
return textField.text?.trimmingCharacters(in: .whitespacesAndNewlines) ?? ""
|
||||
}
|
||||
.assign(to: \.email, on: self)
|
||||
.store(in: &cell.disposeBag)
|
||||
self.configureTextFieldCell(cell: cell, validateState: self.$emailValidateState)
|
||||
return cell
|
||||
case .password:
|
||||
let cell = tableView.dequeueReusableCell(withIdentifier: String(describing: MastodonRegisterTextFieldTableViewCell.self), for: indexPath) as! MastodonRegisterTextFieldTableViewCell
|
||||
cell.setupTextViewPlaceholder(text: L10n.Scene.Register.Input.Password.placeholder)
|
||||
cell.textField.keyboardType = .alphabet
|
||||
cell.textField.autocorrectionType = .no
|
||||
cell.textField.isSecureTextEntry = true
|
||||
cell.textField.text = self.password
|
||||
cell.textField.textAlignment = .left
|
||||
cell.textField.semanticContentAttribute = .forceLeftToRight
|
||||
NotificationCenter.default.publisher(for: UITextField.textDidChangeNotification, object: cell.textField)
|
||||
.receive(on: DispatchQueue.main)
|
||||
.compactMap { notification in
|
||||
guard let textField = notification.object as? UITextField else {
|
||||
assertionFailure()
|
||||
return nil
|
||||
}
|
||||
return textField.text?.trimmingCharacters(in: .whitespacesAndNewlines) ?? ""
|
||||
}
|
||||
.assign(to: \.password, on: self)
|
||||
.store(in: &cell.disposeBag)
|
||||
self.configureTextFieldCell(cell: cell, validateState: self.$passwordValidateState)
|
||||
return cell
|
||||
case .hint:
|
||||
let cell = tableView.dequeueReusableCell(withIdentifier: String(describing: MastodonRegisterPasswordHintTableViewCell.self), for: indexPath) as! MastodonRegisterPasswordHintTableViewCell
|
||||
return cell
|
||||
case .reason:
|
||||
let cell = tableView.dequeueReusableCell(withIdentifier: String(describing: MastodonRegisterTextFieldTableViewCell.self), for: indexPath) as! MastodonRegisterTextFieldTableViewCell
|
||||
cell.setupTextViewPlaceholder(text: L10n.Scene.Register.Input.Invite.registrationUserInviteRequest)
|
||||
cell.textField.keyboardType = .default
|
||||
cell.textField.text = self.reason
|
||||
NotificationCenter.default.publisher(for: UITextField.textDidChangeNotification, object: cell.textField)
|
||||
.receive(on: DispatchQueue.main)
|
||||
.compactMap { notification in
|
||||
guard let textField = notification.object as? UITextField else {
|
||||
assertionFailure()
|
||||
return nil
|
||||
}
|
||||
return textField.text?.trimmingCharacters(in: .whitespacesAndNewlines) ?? ""
|
||||
}
|
||||
.assign(to: \.reason, on: self)
|
||||
.store(in: &cell.disposeBag)
|
||||
self.configureTextFieldCell(cell: cell, validateState: self.$reasonValidateState)
|
||||
return cell
|
||||
default:
|
||||
assertionFailure()
|
||||
return UITableViewCell()
|
||||
}
|
||||
}
|
||||
|
||||
var snapshot = NSDiffableDataSourceSnapshot<RegisterSection, RegisterItem>()
|
||||
snapshot.appendSections([.main])
|
||||
snapshot.appendItems([.header(domain: domain)], toSection: .main)
|
||||
snapshot.appendItems([.avatar, .name, .username, .email, .password, .hint], toSection: .main)
|
||||
if approvalRequired {
|
||||
snapshot.appendItems([.reason], toSection: .main)
|
||||
}
|
||||
diffableDataSource?.applySnapshot(snapshot, animated: false, completion: nil)
|
||||
}
|
||||
}
|
||||
|
||||
extension MastodonRegisterViewModel {
|
||||
private func configureAvatar(cell: MastodonRegisterAvatarTableViewCell) {
|
||||
self.$avatarImage
|
||||
|
|
|
@ -127,7 +127,7 @@ extension MastodonServerRulesViewController {
|
|||
instance: viewModel.instance,
|
||||
applicationToken: viewModel.applicationToken
|
||||
)
|
||||
coordinator.present(scene: .mastodonRegister(viewModel: viewModel), from: self, transition: .show)
|
||||
_ = coordinator.present(scene: .mastodonRegister(viewModel: viewModel), from: self, transition: .show)
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -317,12 +317,12 @@ extension WelcomeViewController {
|
|||
extension WelcomeViewController {
|
||||
@objc
|
||||
private func signUpButtonDidClicked(_ sender: UIButton) {
|
||||
coordinator.present(scene: .mastodonPickServer(viewMode: MastodonPickServerViewModel(context: context, mode: .signUp)), from: self, transition: .show)
|
||||
_ = coordinator.present(scene: .mastodonPickServer(viewMode: MastodonPickServerViewModel(context: context)), from: self, transition: .show)
|
||||
}
|
||||
|
||||
@objc
|
||||
private func signInButtonDidClicked(_ sender: UIButton) {
|
||||
coordinator.present(scene: .mastodonPickServer(viewMode: MastodonPickServerViewModel(context: context, mode: .signIn)), from: self, transition: .show)
|
||||
_ = coordinator.present(scene: .mastodonLogin, from: self, transition: .show)
|
||||
}
|
||||
|
||||
@objc
|
||||
|
|
|
@ -65,11 +65,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate {
|
|||
|
||||
extension AppDelegate {
|
||||
func application(_ application: UIApplication, supportedInterfaceOrientationsFor window: UIWindow?) -> UIInterfaceOrientationMask {
|
||||
#if DEBUG
|
||||
return .all
|
||||
#else
|
||||
return UIDevice.current.userInterfaceIdiom == .phone ? .portrait : .all
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -32,7 +32,6 @@ let package = Package(
|
|||
.package(url: "https://github.com/Alamofire/AlamofireImage.git", from: "4.1.0"),
|
||||
.package(url: "https://github.com/apple/swift-collections.git", from: "1.0.3"),
|
||||
.package(url: "https://github.com/apple/swift-nio.git", from: "1.0.0"),
|
||||
.package(url: "https://github.com/cezheng/Fuzi.git", from: "3.1.3"),
|
||||
.package(url: "https://github.com/Flipboard/FLAnimatedImage.git", from: "1.0.0"),
|
||||
.package(url: "https://github.com/kean/Nuke-FLAnimatedImage-Plugin.git", from: "8.0.0"),
|
||||
.package(url: "https://github.com/kean/Nuke.git", from: "10.3.1"),
|
||||
|
|
|
@ -12,8 +12,8 @@ import MastodonSDK
|
|||
extension APIService {
|
||||
|
||||
public func servers(
|
||||
language: String?,
|
||||
category: String?
|
||||
language: String? = nil,
|
||||
category: String? = nil
|
||||
) -> AnyPublisher<Mastodon.Response.Content<[Mastodon.Entity.Server]>, Error> {
|
||||
let query = Mastodon.API.Onboarding.ServersQuery(language: language, category: category)
|
||||
return Mastodon.API.Onboarding.servers(session: session, query: query)
|
||||
|
|
|
@ -665,6 +665,16 @@ public enum L10n {
|
|||
}
|
||||
}
|
||||
}
|
||||
public enum Login {
|
||||
/// Log you in with the server where you created your account
|
||||
public static let subtitle = L10n.tr("Localizable", "Scene.Login.Subtitle", fallback: "Scene.Login.Subtitle")
|
||||
/// Welcome Back!
|
||||
public static let title = L10n.tr("Localizable", "Scene.Login.Title", fallback: "Welcome")
|
||||
public enum ServerSearchField {
|
||||
/// Search for your server
|
||||
public static let placeholder = L10n.tr("Localizable", "Scene.Login.ServerSearchField.Placeholder", fallback: "Scene.Login.ServerSearchField.Placeholder")
|
||||
}
|
||||
}
|
||||
public enum Notification {
|
||||
public enum FollowRequest {
|
||||
/// Accept
|
||||
|
|
|
@ -315,6 +315,9 @@ uploaded to Mastodon.";
|
|||
"Scene.Register.Input.Username.DuplicatePrompt" = "This username is taken.";
|
||||
"Scene.Register.Input.Username.Placeholder" = "username";
|
||||
"Scene.Register.LetsGetYouSetUpOnDomain" = "Let’s get you set up on %@";
|
||||
"Scene.Login.Title" = "Welcome Back!";
|
||||
"Scene.Login.Subtitle" = "Log you in with the server where you created your account";
|
||||
"Scene.Login.ServerSearchField.Placeholder" = "Search server or enter URL";
|
||||
"Scene.Register.Title" = "Let’s get you set up on %@";
|
||||
"Scene.Report.Content1" = "Are there any other posts you’d like to add to the report?";
|
||||
"Scene.Report.Content2" = "Is there anything the moderators should know about this report?";
|
||||
|
@ -454,4 +457,4 @@ uploaded to Mastodon.";
|
|||
back in your hands.";
|
||||
"Scene.Wizard.AccessibilityHint" = "Double tap to dismiss this wizard";
|
||||
"Scene.Wizard.MultipleAccountSwitchIntroDescription" = "Switch between multiple accounts by holding the profile button.";
|
||||
"Scene.Wizard.NewInMastodon" = "New in Mastodon";
|
||||
"Scene.Wizard.NewInMastodon" = "New in Mastodon";
|
||||
|
|
|
@ -9,7 +9,7 @@ import Foundation
|
|||
|
||||
extension Mastodon.Entity {
|
||||
|
||||
public struct Server: Codable, Equatable {
|
||||
public struct Server: Codable, Equatable, Hashable {
|
||||
public let domain: String
|
||||
public let version: String
|
||||
public let description: String
|
||||
|
|
Loading…
Reference in New Issue