fix: add missing error prompt for sign up scene

This commit is contained in:
CMK 2022-04-27 17:37:03 +08:00
parent 033c584eb4
commit 2ae3f21a99
12 changed files with 581 additions and 226 deletions

View File

@ -145,8 +145,6 @@
DB0618012785732C0030EE79 /* ServerRulesTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB0618002785732C0030EE79 /* ServerRulesTableViewCell.swift */; };
DB0618032785A7100030EE79 /* RegisterSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB0618022785A7100030EE79 /* RegisterSection.swift */; };
DB0618052785A73D0030EE79 /* RegisterItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB0618042785A73D0030EE79 /* RegisterItem.swift */; };
DB0618072785A8880030EE79 /* MastodonRegisterViewModel+Diffable.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB0618062785A8880030EE79 /* MastodonRegisterViewModel+Diffable.swift */; };
DB06180A2785B2AB0030EE79 /* MastodonRegisterAvatarTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB0618092785B2AB0030EE79 /* MastodonRegisterAvatarTableViewCell.swift */; };
DB084B5725CBC56C00F898ED /* Status.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB084B5625CBC56C00F898ED /* Status.swift */; };
DB0A322E280EE9FD001729D2 /* DiscoveryIntroBannerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB0A322D280EE9FD001729D2 /* DiscoveryIntroBannerView.swift */; };
DB0AC6FC25CD02E600D75117 /* APIService+Instance.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB0AC6FB25CD02E600D75117 /* APIService+Instance.swift */; };
@ -400,10 +398,10 @@
DB75BF1E263C1C1B00EDBF1F /* CustomScheduler.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB75BF1D263C1C1B00EDBF1F /* CustomScheduler.swift */; };
DB789A0B25F9F2950071ACA0 /* ComposeViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB789A0A25F9F2950071ACA0 /* ComposeViewController.swift */; };
DB789A1225F9F2CC0071ACA0 /* ComposeViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB789A1125F9F2CC0071ACA0 /* ComposeViewModel.swift */; };
DB7A9F912818EAF10016AF98 /* MastodonRegisterView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB7A9F902818EAF10016AF98 /* MastodonRegisterView.swift */; };
DB7A9F932818F33C0016AF98 /* MastodonServerRulesViewController+Debug.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB7A9F922818F33C0016AF98 /* MastodonServerRulesViewController+Debug.swift */; };
DB7F48452620241000796008 /* ProfileHeaderViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB7F48442620241000796008 /* ProfileHeaderViewModel.swift */; };
DB8190C62601FF0400020C08 /* AttachmentContainerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB8190C52601FF0400020C08 /* AttachmentContainerView.swift */; };
DB8481152788121200BBEABA /* MastodonRegisterTextFieldTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB8481142788121200BBEABA /* MastodonRegisterTextFieldTableViewCell.swift */; };
DB84811727883C2600BBEABA /* MastodonRegisterPasswordHintTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB84811627883C2600BBEABA /* MastodonRegisterPasswordHintTableViewCell.swift */; };
DB852D1926FAEB6B00FC9D81 /* SidebarViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB852D1826FAEB6B00FC9D81 /* SidebarViewController.swift */; };
DB852D1C26FB021500FC9D81 /* RootSplitViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB852D1B26FB021500FC9D81 /* RootSplitViewController.swift */; };
DB852D1F26FB037800FC9D81 /* SidebarViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB852D1E26FB037800FC9D81 /* SidebarViewModel.swift */; };
@ -417,6 +415,7 @@
DB8AF54525C13647002E6C99 /* NeedsDependency.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB8AF54325C13647002E6C99 /* NeedsDependency.swift */; };
DB8AF55025C13703002E6C99 /* MainTabBarController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB8AF54F25C13703002E6C99 /* MainTabBarController.swift */; };
DB8AF55D25C138B7002E6C99 /* UIViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB8AF55C25C138B7002E6C99 /* UIViewController.swift */; };
DB8D8E2F28192EED009FD90F /* Introspect in Frameworks */ = {isa = PBXBuildFile; productRef = DB8D8E2E28192EED009FD90F /* Introspect */; };
DB8F7076279E954700E1225B /* DataSourceFacade+Follow.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB8F7075279E954700E1225B /* DataSourceFacade+Follow.swift */; };
DB8FABC726AEC7B2008E5AF4 /* Intents.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = DB8FAB9E26AEC3A2008E5AF4 /* Intents.framework */; };
DB8FABCA26AEC7B2008E5AF4 /* IntentHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB8FABC926AEC7B2008E5AF4 /* IntentHandler.swift */; };
@ -1146,6 +1145,8 @@
DB75BF1D263C1C1B00EDBF1F /* CustomScheduler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomScheduler.swift; sourceTree = "<group>"; };
DB789A0A25F9F2950071ACA0 /* ComposeViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComposeViewController.swift; sourceTree = "<group>"; };
DB789A1125F9F2CC0071ACA0 /* ComposeViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComposeViewModel.swift; sourceTree = "<group>"; };
DB7A9F902818EAF10016AF98 /* MastodonRegisterView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MastodonRegisterView.swift; sourceTree = "<group>"; };
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>"; };
@ -1379,6 +1380,7 @@
5D526FE225BE9AC400460CB9 /* MastodonSDK in Frameworks */,
DB6804862637CD4C00430867 /* AppShared.framework in Frameworks */,
DBF96326262EC0A6001D8D25 /* AuthenticationServices.framework in Frameworks */,
DB8D8E2F28192EED009FD90F /* Introspect in Frameworks */,
DBAC6483267D0B21007FE9FD /* DifferenceKit in Frameworks */,
DB552D4F26BBD10C00E481F6 /* OrderedCollections in Frameworks */,
2D61336925C18A4F00CAE157 /* AlamofireNetworkActivityIndicator in Frameworks */,
@ -2633,6 +2635,7 @@
children = (
DB0618082785B2790030EE79 /* Cell */,
DB72601B25E36A2100235243 /* MastodonServerRulesViewController.swift */,
DB7A9F922818F33C0016AF98 /* MastodonServerRulesViewController+Debug.swift */,
DB72602625E36A6F00235243 /* MastodonServerRulesViewModel.swift */,
DB0617FE27855D6C0030EE79 /* MastodonServerRulesViewModel+Diffable.swift */,
);
@ -3149,6 +3152,7 @@
2D939AE725EE1CF80076FA61 /* MastodonRegisterViewController+Avatar.swift */,
DBE0822325CD3F1E00FD6BBD /* MastodonRegisterViewModel.swift */,
DB0618062785A8880030EE79 /* MastodonRegisterViewModel+Diffable.swift */,
DB7A9F902818EAF10016AF98 /* MastodonRegisterView.swift */,
);
path = Register;
sourceTree = "<group>";
@ -3324,6 +3328,7 @@
DB552D4E26BBD10C00E481F6 /* OrderedCollections */,
DBA5A52E26F07ED800CACBAA /* PanModal */,
DB02EA0C280D184B00E751C5 /* CommonOSLog */,
DB8D8E2E28192EED009FD90F /* Introspect */,
);
productName = Mastodon;
productReference = DB427DD225BAA00100D1B89D /* Mastodon.app */;
@ -3540,6 +3545,7 @@
DB01E23126A98F0900C3965B /* XCRemoteSwiftPackageReference "MetaTextKit" */,
DB552D4D26BBD10C00E481F6 /* XCRemoteSwiftPackageReference "swift-collections" */,
DBA5A52D26F07ED800CACBAA /* XCRemoteSwiftPackageReference "PanModal" */,
DB8D8E2D28192EED009FD90F /* XCRemoteSwiftPackageReference "SwiftUI-Introspect" */,
);
productRefGroup = DB427DD325BAA00100D1B89D /* Products */;
projectDirPath = "";
@ -3904,7 +3910,6 @@
DB63F75A279953F200455B82 /* SearchHistoryUserCollectionViewCell+ViewModel.swift in Sources */,
DB023D26279FFB0A005AC798 /* ShareActivityProvider.swift in Sources */,
DB71FD5225F8CCAA00512AE1 /* APIService+Status.swift in Sources */,
DB8481152788121200BBEABA /* MastodonRegisterTextFieldTableViewCell.swift in Sources */,
5D0393962612D266007FE196 /* WebViewModel.swift in Sources */,
5B24BBDA262DB14800A9381B /* ReportViewModel.swift in Sources */,
2D5A3D3825CF8D9F002347D6 /* ScrollViewContainer.swift in Sources */,
@ -3956,7 +3961,6 @@
DBDFF1952805561700557A48 /* DiscoveryPostsViewModel+Diffable.swift in Sources */,
DB03A795272A981400EE37C5 /* ContentSplitViewController.swift in Sources */,
DBDFF19C28055BD600557A48 /* DiscoveryViewModel.swift in Sources */,
DB06180A2785B2AB0030EE79 /* MastodonRegisterAvatarTableViewCell.swift in Sources */,
DBB45B6227B51112002DC5A7 /* SuggestionAccountViewModel+Diffable.swift in Sources */,
DBB3BA2A26A81C020004F2D4 /* FLAnimatedImageView.swift in Sources */,
DB3E6FF32806D97400B035AE /* DiscoveryNewsViewModel+State.swift in Sources */,
@ -4024,6 +4028,7 @@
DB36679F268ABAF20027D07F /* ComposeStatusAttachmentSection.swift in Sources */,
2DA7D04425CA52B200804E11 /* TimelineLoaderTableViewCell.swift in Sources */,
DB63F7542799491600455B82 /* DataSourceFacade+SearchHistory.swift in Sources */,
DB7A9F912818EAF10016AF98 /* MastodonRegisterView.swift in Sources */,
DBF1572F27046F1A00EC00B7 /* SecondaryPlaceholderViewController.swift in Sources */,
DB03F7F32689AEA3007B274C /* ComposeRepliedToStatusContentTableViewCell.swift in Sources */,
2D4AD8A826316D3500613EFC /* SelectedAccountItem.swift in Sources */,
@ -4118,7 +4123,6 @@
5DF1056425F887CB00D6C0D4 /* AVPlayer.swift in Sources */,
DB3E6FEF2806D82600B035AE /* DiscoveryNewsViewModel.swift in Sources */,
DBBF1DCB2652539E00E5B703 /* AutoCompleteItem.swift in Sources */,
DB84811727883C2600BBEABA /* MastodonRegisterPasswordHintTableViewCell.swift in Sources */,
2DA6054725F716A2006356F9 /* PlaybackState.swift in Sources */,
DB98EB6727B216560082E365 /* ReportResultViewModel+Diffable.swift in Sources */,
DBC7A672260C897100E57475 /* StatusContentWarningEditorView.swift in Sources */,
@ -4274,10 +4278,10 @@
DB0FCB6C27950E29006C02E2 /* MastodonMentionContainer.swift in Sources */,
DB6D9F502635761F008423CD /* SubscriptionAlerts.swift in Sources */,
0F20220726134DA4000C64BF /* HashtagTimelineViewModel+Diffable.swift in Sources */,
DB7A9F932818F33C0016AF98 /* MastodonServerRulesViewController+Debug.swift in Sources */,
DBE54AC62636C89F004E7C0B /* NotificationPreference.swift in Sources */,
2D5A3D2825CF8BC9002347D6 /* HomeTimelineViewModel+Diffable.swift in Sources */,
DB98339C25C96DE600AD9700 /* APIService+Account.swift in Sources */,
DB0618072785A8880030EE79 /* MastodonRegisterViewModel+Diffable.swift in Sources */,
DB6B74FC272FF55800C70B6E /* UserSection.swift in Sources */,
2DF75BA725D10E1000694EC8 /* APIService+Favorite.swift in Sources */,
DB0FCB862796BDA1006C02E2 /* SearchSection.swift in Sources */,
@ -5465,6 +5469,14 @@
minimumVersion = 4.2.2;
};
};
DB8D8E2D28192EED009FD90F /* XCRemoteSwiftPackageReference "SwiftUI-Introspect" */ = {
isa = XCRemoteSwiftPackageReference;
repositoryURL = "https://github.com/siteline/SwiftUI-Introspect.git";
requirement = {
kind = upToNextMajorVersion;
minimumVersion = 0.1.4;
};
};
DB9A487C2603456B008B817C /* XCRemoteSwiftPackageReference "UITextView-Placeholder" */ = {
isa = XCRemoteSwiftPackageReference;
repositoryURL = "https://github.com/MainasuK/UITextView-Placeholder";
@ -5587,6 +5599,11 @@
package = DB3D0FF125BAA61700EAA174 /* XCRemoteSwiftPackageReference "AlamofireImage" */;
productName = AlamofireImage;
};
DB8D8E2E28192EED009FD90F /* Introspect */ = {
isa = XCSwiftPackageProductDependency;
package = DB8D8E2D28192EED009FD90F /* XCRemoteSwiftPackageReference "SwiftUI-Introspect" */;
productName = Introspect;
};
DB9A487D2603456B008B817C /* UITextView+Placeholder */ = {
isa = XCSwiftPackageProductDependency;
package = DB9A487C2603456B008B817C /* XCRemoteSwiftPackageReference "UITextView-Placeholder" */;

View File

@ -109,7 +109,7 @@
<key>MastodonIntent.xcscheme_^#shared#^_</key>
<dict>
<key>orderHint</key>
<integer>34</integer>
<integer>24</integer>
</dict>
<key>MastodonIntents.xcscheme_^#shared#^_</key>
<dict>
@ -124,12 +124,12 @@
<key>NotificationService.xcscheme_^#shared#^_</key>
<dict>
<key>orderHint</key>
<integer>29</integer>
<integer>23</integer>
</dict>
<key>ShareActionExtension.xcscheme_^#shared#^_</key>
<dict>
<key>orderHint</key>
<integer>33</integer>
<integer>22</integer>
</dict>
</dict>
<key>SuppressBuildableAutocreation</key>

View File

@ -58,6 +58,10 @@ extension HomeTimelineViewController {
guard let self = self else { return }
self.showWelcomeAction(action)
},
UIAction(title: "Register", image: UIImage(systemName: "list.bullet.rectangle.portrait.fill"), attributes: []) { [weak self] action in
guard let self = self else { return }
self.showRegisterAction(action)
},
UIAction(title: "Confirm Email", image: UIImage(systemName: "envelope"), attributes: []) { [weak self] action in
guard let self = self else { return }
self.showConfirmEmail(action)
@ -294,6 +298,33 @@ extension HomeTimelineViewController {
@objc private func showWelcomeAction(_ sender: UIAction) {
coordinator.present(scene: .welcome, from: self, transition: .modal(animated: true, completion: nil))
}
@objc private func showRegisterAction(_ sender: UIAction) {
Task { @MainActor in
try await showRegisterController()
} // end Task
}
@MainActor
func showRegisterController(domain: String = "mstdn.jp") async throws {
let viewController = try await MastodonRegisterViewController.create(
context: context,
coordinator: coordinator,
domain: "mstdn.jp"
)
let navigationController = UINavigationController(rootViewController: viewController)
navigationController.modalPresentationStyle = .fullScreen
present(navigationController, animated: true) {
viewController.navigationItem.leftBarButtonItem = UIBarButtonItem(
systemItem: .close,
primaryAction: UIAction(handler: { [weak viewController] _ in
guard let viewController = viewController else { return }
viewController.dismiss(animated: true)
}),
menu: nil
)
}
}
@objc private func showConfirmEmail(_ sender: UIAction) {
let mastodonConfirmEmailViewModel = MastodonConfirmEmailViewModel()

View File

@ -341,7 +341,10 @@ extension MastodonPickServerViewController {
) else {
throw APIService.APIError.explicit(.badResponse)
}
return MastodonPickServerViewModel.SignUpResponseSecond(instance: response.instance, authenticateInfo: authenticateInfo)
return MastodonPickServerViewModel.SignUpResponseSecond(
instance: response.instance,
authenticateInfo: authenticateInfo
)
}
.compactMap { [weak self] response -> AnyPublisher<MastodonPickServerViewModel.SignUpResponseThird, Error>? in
guard let self = self else { return nil }
@ -353,7 +356,13 @@ extension MastodonPickServerViewController {
clientSecret: authenticateInfo.clientSecret,
redirectURI: authenticateInfo.redirectURI
)
.map { MastodonPickServerViewModel.SignUpResponseThird(instance: instance, authenticateInfo: authenticateInfo, applicationToken: $0) }
.map {
MastodonPickServerViewModel.SignUpResponseThird(
instance: instance,
authenticateInfo: authenticateInfo,
applicationToken: $0
)
}
.eraseToAnyPublisher()
}
.switchToLatest()

View File

@ -0,0 +1,304 @@
//
// MastodonRegisterView.swift
// Mastodon
//
// Created by MainasuK on 2022-4-27.
//
import UIKit
import SwiftUI
import MastodonLocalization
import MastodonSDK
import MastodonAsset
struct MastodonRegisterView: View {
@ObservedObject var viewModel: MastodonRegisterViewModel
@State var usernameRightViewWidth: CGFloat = 300
var body: some View {
ScrollView(.vertical) {
let margin: CGFloat = 16
// header
HStack {
Text(L10n.Scene.Register.title(viewModel.domain))
.font(Font(MastodonPickServerViewController.largeTitleFont as CTFont))
.foregroundColor(Color(Asset.Colors.Label.primary.color))
Spacer()
}
.padding(.horizontal, margin)
// Avatar selector
Menu {
// Photo Library
Button {
viewModel.avatarMediaMenuActionPublisher.send(.photoLibrary)
} label: {
Label(L10n.Scene.Compose.MediaSelection.photoLibrary, systemImage: "photo")
}
// Camera
if UIImagePickerController.isSourceTypeAvailable(.camera) {
Button {
viewModel.avatarMediaMenuActionPublisher.send(.camera)
} label: {
Label(L10n.Scene.Compose.MediaSelection.camera, systemImage: "camera")
}
}
// Browse
Button {
viewModel.avatarMediaMenuActionPublisher.send(.browse)
} label: {
Label(L10n.Scene.Compose.MediaSelection.browse, systemImage: "folder")
}
// Delete
if viewModel.avatarImage != nil {
Divider()
if #available(iOS 15.0, *) {
Button(role: .destructive) {
viewModel.avatarMediaMenuActionPublisher.send(.delete)
} label: {
Label(L10n.Scene.Register.Input.Avatar.delete, systemImage: "delete.left")
}
} else {
// Fallback on earlier ve rsions
Button {
viewModel.avatarMediaMenuActionPublisher.send(.delete)
} label: {
Label(L10n.Scene.Register.Input.Avatar.delete, systemImage: "delete.left")
}
}
}
} label: {
let avatarImage = viewModel.avatarImage ?? Asset.Scene.Onboarding.avatarPlaceholder.image
Image(uiImage: avatarImage)
.resizable()
.frame(width: 88, height: 88, alignment: .center)
.overlay(ZStack {
Color.black.opacity(0.5)
.frame(height: 22, alignment: .bottom)
Text(L10n.Common.Controls.Actions.edit)
.font(.system(size: 13, weight: .semibold))
.foregroundColor(.white)
}, alignment: .bottom)
.cornerRadius(22)
}
.padding(EdgeInsets(top: 20, leading: 0, bottom: 20, trailing: 0))
// Display Name & Uesrname
VStack(alignment: .leading, spacing: 11) {
TextField(L10n.Scene.Register.Input.DisplayName.placeholder.localizedCapitalized, text: $viewModel.name)
.textContentType(.name)
.disableAutocorrection(true)
.modifier(FormTextFieldModifier(validateState: viewModel.displayNameValidateState))
HStack {
TextField(L10n.Scene.Register.Input.Username.placeholder.localizedCapitalized, text: $viewModel.username)
.textContentType(.username)
.autocapitalization(.none)
.disableAutocorrection(true)
.keyboardType(.asciiCapable)
Text("@\(viewModel.domain)")
.lineLimit(1)
.truncationMode(.middle)
.measureWidth { usernameRightViewWidth = $0 }
.frame(width: min(300.0, usernameRightViewWidth), alignment: .trailing)
}
.modifier(FormTextFieldModifier(validateState: viewModel.usernameValidateState))
.environment(\.layoutDirection, .leftToRight) // force LTR
if let errorPrompt = viewModel.usernameErrorPrompt {
Text(errorPrompt)
.modifier(FormFootnoteModifier())
}
}
.padding(.horizontal, margin)
.padding(.bottom, 22)
// Email & Password & Password hint
VStack(alignment: .leading, spacing: 11) {
TextField(L10n.Scene.Register.Input.Email.placeholder.localizedCapitalized, text: $viewModel.email)
.textContentType(.emailAddress)
.autocapitalization(.none)
.disableAutocorrection(true)
.keyboardType(.emailAddress)
.modifier(FormTextFieldModifier(validateState: viewModel.emailValidateState))
if let errorPrompt = viewModel.emailErrorPrompt {
Text(errorPrompt)
.modifier(FormFootnoteModifier())
}
SecureField(L10n.Scene.Register.Input.Password.placeholder.localizedCapitalized, text: $viewModel.password)
.textContentType(.newPassword)
.modifier(FormTextFieldModifier(validateState: viewModel.passwordValidateState))
Text(L10n.Scene.Register.Input.Password.hint)
.modifier(FormFootnoteModifier(foregroundColor: .secondary))
if let errorPrompt = viewModel.passwordErrorPrompt {
Text(errorPrompt)
.modifier(FormFootnoteModifier())
}
}
.padding(.horizontal, margin)
.padding(.bottom, 22)
// Reason
if viewModel.approvalRequired {
VStack(alignment: .leading, spacing: 11) {
TextField(L10n.Scene.Register.Input.Invite.registrationUserInviteRequest.localizedCapitalized, text: $viewModel.reason)
.modifier(FormTextFieldModifier(validateState: viewModel.reasonValidateState))
if let errorPrompt = viewModel.reasonErrorPrompt {
Text(errorPrompt)
.modifier(FormFootnoteModifier())
}
}
.padding(.horizontal, margin)
}
Spacer()
.frame(minHeight: viewModel.bottomPaddingHeight)
}
.background(
Color(viewModel.backgroundColor)
.onTapGesture {
viewModel.endEditing.send()
}
)
}
struct FormTextFieldModifier: ViewModifier {
var validateState: MastodonRegisterViewModel.ValidateState
func body(content: Content) -> some View {
ZStack {
let shadowColor: 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)
}
}()
Color(Asset.Scene.Onboarding.textFieldBackground.color)
.cornerRadius(10)
.shadow(color: shadowColor, radius: 1, x: 0, y: 2)
.animation(.easeInOut, value: validateState)
content
.padding()
.background(Color(Asset.Scene.Onboarding.textFieldBackground.color))
.cornerRadius(10)
}
}
}
struct FormFootnoteModifier: ViewModifier {
var foregroundColor = Color(Asset.Colors.TextField.invalid.color)
func body(content: Content) -> some View {
content
.font(.footnote)
.foregroundColor(foregroundColor)
.padding(.horizontal)
}
}
}
struct WidthKey: PreferenceKey {
static let defaultValue: CGFloat = 0
static func reduce(value: inout CGFloat, nextValue: () -> CGFloat) {
value = nextValue()
}
}
extension View {
func measureWidth(_ f: @escaping (CGFloat) -> ()) -> some View {
overlay(GeometryReader { proxy in
Color.clear.preference(key: WidthKey.self, value: proxy.size.width)
}
.onPreferenceChange(WidthKey.self, perform: f))
}
}
#if DEBUG
struct MastodonRegisterView_Previews: PreviewProvider {
static var viewMdoel: MastodonRegisterViewModel {
let domain = "mstdn.jp"
return MastodonRegisterViewModel(
context: .shared,
domain: domain,
authenticateInfo: AuthenticationViewModel.AuthenticateInfo(
domain: domain,
application: Mastodon.Entity.Application(
name: "Preview",
website: nil,
vapidKey: nil,
redirectURI: nil,
clientID: "",
clientSecret: ""
),
redirectURI: ""
)!,
instance: Mastodon.Entity.Instance(domain: "mstdn.jp"),
applicationToken: Mastodon.Entity.Token(
accessToken: "",
tokenType: "",
scope: "",
createdAt: Date()
)
)
}
static var viewMdoel2: MastodonRegisterViewModel {
let domain = "mstdn.jp"
return MastodonRegisterViewModel(
context: .shared,
domain: domain,
authenticateInfo: AuthenticationViewModel.AuthenticateInfo(
domain: domain,
application: Mastodon.Entity.Application(
name: "Preview",
website: nil,
vapidKey: nil,
redirectURI: nil,
clientID: "",
clientSecret: ""
),
redirectURI: ""
)!,
instance: Mastodon.Entity.Instance(domain: "mstdn.jp", approvalRequired: true),
applicationToken: Mastodon.Entity.Token(
accessToken: "",
tokenType: "",
scope: "",
createdAt: Date()
)
)
}
static var previews: some View {
Group {
NavigationView {
MastodonRegisterView(viewModel: viewMdoel)
.navigationBarTitle(Text(""))
.navigationBarTitleDisplayMode(.inline)
}
NavigationView {
MastodonRegisterView(viewModel: viewMdoel)
.navigationBarTitle(Text(""))
.navigationBarTitleDisplayMode(.inline)
}
.preferredColorScheme(.dark)
NavigationView {
MastodonRegisterView(viewModel: viewMdoel)
.navigationBarTitle(Text(""))
.navigationBarTitleDisplayMode(.inline)
}
.environment(\.sizeCategory, .accessibilityExtraLarge)
NavigationView {
MastodonRegisterView(viewModel: viewMdoel2)
.navigationBarTitle(Text(""))
.navigationBarTitleDisplayMode(.inline)
}
}
}
}
#endif

View File

@ -11,6 +11,7 @@ import MastodonSDK
import os.log
import PhotosUI
import UIKit
import SwiftUI
import MastodonUI
import MastodonAsset
import MastodonLocalization
@ -28,6 +29,7 @@ final class MastodonRegisterViewController: UIViewController, NeedsDependency, O
weak var coordinator: SceneCoordinator! { willSet { precondition(!isViewLoaded) } }
var viewModel: MastodonRegisterViewModel!
private(set) lazy var mastodonRegisterView = MastodonRegisterView(viewModel: viewModel)
// picker
private(set) lazy var imagePicker: PHPickerViewController = {
@ -52,22 +54,6 @@ final class MastodonRegisterViewController: UIViewController, NeedsDependency, O
return documentPickerController
}()
let tapGestureRecognizer = UITapGestureRecognizer.singleTapGestureRecognizer
let tableView: UITableView = {
let tableView = UITableView()
tableView.rowHeight = UITableView.automaticDimension
tableView.separatorStyle = .none
tableView.backgroundColor = .clear
tableView.keyboardDismissMode = .onDrag
if #available(iOS 15.0, *) {
tableView.sectionHeaderTopPadding = .leastNonzeroMagnitude
} else {
// Fallback on earlier versions
}
return tableView
}()
let navigationActionView: NavigationActionView = {
let navigationActionView = NavigationActionView()
navigationActionView.backgroundColor = Asset.Scene.Onboarding.background.color
@ -88,17 +74,21 @@ extension MastodonRegisterViewController {
navigationItem.leftBarButtonItem = UIBarButtonItem()
setupOnboardingAppearance()
viewModel.backgroundColor = view.backgroundColor ?? .clear
defer {
setupNavigationBarBackgroundView()
}
tableView.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(tableView)
let hostingViewController = UIHostingController(rootView: mastodonRegisterView)
hostingViewController.view.preservesSuperviewLayoutMargins = true
addChild(hostingViewController)
hostingViewController.view.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(hostingViewController.view)
NSLayoutConstraint.activate([
tableView.topAnchor.constraint(equalTo: view.topAnchor),
tableView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
tableView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
tableView.bottomAnchor.constraint(equalTo: view.bottomAnchor),
hostingViewController.view.topAnchor.constraint(equalTo: view.topAnchor),
hostingViewController.view.leadingAnchor.constraint(equalTo: view.leadingAnchor),
hostingViewController.view.trailingAnchor.constraint(equalTo: view.trailingAnchor),
hostingViewController.view.bottomAnchor.constraint(equalTo: view.bottomAnchor),
])
navigationActionView.translatesAutoresizingMaskIntoConstraints = false
@ -116,7 +106,7 @@ extension MastodonRegisterViewController {
.observe(\.bounds, options: [.initial, .new]) { [weak self] navigationActionView, _ in
guard let self = self else { return }
let inset = navigationActionView.frame.height
self.tableView.contentInset.bottom = inset
self.viewModel.bottomPaddingHeight = inset
}
.store(in: &observations)
@ -130,19 +120,14 @@ extension MastodonRegisterViewController {
self.navigationActionView.nextButton.isEnabled = isAllValid
}
.store(in: &disposeBag)
viewModel.setupDiffableDataSource(tableView: tableView)
KeyboardResponderService
.configure(
scrollView: tableView,
layoutNeedsUpdate: viewModel.viewDidAppear.eraseToAnyPublisher()
)
.store(in: &disposeBag)
// gesture
view.addGestureRecognizer(tapGestureRecognizer)
tapGestureRecognizer.addTarget(self, action: #selector(tapGestureRecognizerHandler))
viewModel.endEditing
.receive(on: DispatchQueue.main)
.sink { [weak self] _ in
guard let self = self else { return }
self.view.endEditing(true)
}
.store(in: &disposeBag)
// // return
// if viewModel.approvalRequired {
@ -150,64 +135,6 @@ extension MastodonRegisterViewController {
// } else {
// passwordTextField.returnKeyType = .done
// }
//
// viewModel.usernameValidateState
// .receive(on: DispatchQueue.main)
// .sink { [weak self] validateState in
// guard let self = self else { return }
// self.setTextFieldValidAppearance(self.usernameTextField, validateState: validateState)
// }
// .store(in: &disposeBag)
// viewModel.usernameErrorPrompt
// .receive(on: DispatchQueue.main)
// .sink { [weak self] prompt in
// guard let self = self else { return }
// self.usernameErrorPromptLabel.attributedText = prompt
// }
// .store(in: &disposeBag)
// viewModel.displayNameValidateState
// .receive(on: DispatchQueue.main)
// .sink { [weak self] validateState in
// guard let self = self else { return }
// self.setTextFieldValidAppearance(self.displayNameTextField, validateState: validateState)
// }
// .store(in: &disposeBag)
// viewModel.emailValidateState
// .receive(on: DispatchQueue.main)
// .sink { [weak self] validateState in
// guard let self = self else { return }
// self.setTextFieldValidAppearance(self.emailTextField, validateState: validateState)
// }
// .store(in: &disposeBag)
// viewModel.emailErrorPrompt
// .receive(on: DispatchQueue.main)
// .sink { [weak self] prompt in
// guard let self = self else { return }
// self.emailErrorPromptLabel.attributedText = prompt
// }
// .store(in: &disposeBag)
// viewModel.passwordValidateState
// .receive(on: DispatchQueue.main)
// .sink { [weak self] validateState in
// guard let self = self else { return }
// self.setTextFieldValidAppearance(self.passwordTextField, validateState: validateState)
// self.passwordCheckLabel.attributedText = MastodonRegisterViewModel.attributeStringForPassword(validateState: validateState)
// }
// .store(in: &disposeBag)
// viewModel.passwordErrorPrompt
// .receive(on: DispatchQueue.main)
// .sink { [weak self] prompt in
// guard let self = self else { return }
// self.passwordErrorPromptLabel.attributedText = prompt
// }
// .store(in: &disposeBag)
// viewModel.reasonErrorPrompt
// .receive(on: DispatchQueue.main)
// .sink { [weak self] prompt in
// guard let self = self else { return }
// self.reasonErrorPromptLabel.attributedText = prompt
// }
// .store(in: &disposeBag)
viewModel.$error
.receive(on: DispatchQueue.main)
@ -261,10 +188,6 @@ extension MastodonRegisterViewController {
extension MastodonRegisterViewController {
@objc private func tapGestureRecognizerHandler(_ sender: UITapGestureRecognizer) {
view.endEditing(true)
}
@objc private func backButtonPressed(_ sender: UIButton) {
logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public)")
navigationController?.popViewController(animated: true)

View File

@ -143,7 +143,7 @@ extension MastodonRegisterViewModel {
snapshot.appendItems([.header(domain: domain)], toSection: .main)
snapshot.appendItems([.avatar, .name, .username, .email, .password, .hint], toSection: .main)
if approvalRequired {
snapshot.appendItems([.reason], toSection: .main)
snapshot.appendItems([.reason], toSection: .main)
}
diffableDataSource?.applySnapshot(snapshot, animated: false, completion: nil)
}
@ -164,51 +164,6 @@ extension MastodonRegisterViewModel {
.store(in: &cell.disposeBag)
}
enum AvatarMediaMenuAction {
case photoLibrary
case camera
case browse
case delete
}
private func createAvatarMediaContextMenu() -> UIMenu {
var children: [UIMenuElement] = []
// Photo Library
let photoLibraryAction = UIAction(title: L10n.Scene.Compose.MediaSelection.photoLibrary, image: UIImage(systemName: "rectangle.on.rectangle"), identifier: nil, discoverabilityTitle: nil, attributes: [], state: .off) { [weak self] _ in
guard let self = self else { return }
self.avatarMediaMenuActionPublisher.send(.photoLibrary)
}
children.append(photoLibraryAction)
// Camera
if UIImagePickerController.isSourceTypeAvailable(.camera) {
let cameraAction = UIAction(title: L10n.Scene.Compose.MediaSelection.camera, image: UIImage(systemName: "camera"), identifier: nil, discoverabilityTitle: nil, attributes: [], state: .off, handler: { [weak self] _ in
guard let self = self else { return }
self.avatarMediaMenuActionPublisher.send(.camera)
})
children.append(cameraAction)
}
// Browse
let browseAction = UIAction(title: L10n.Scene.Compose.MediaSelection.browse, image: UIImage(systemName: "ellipsis"), identifier: nil, discoverabilityTitle: nil, attributes: [], state: .off) { [weak self] _ in
guard let self = self else { return }
self.avatarMediaMenuActionPublisher.send(.browse)
}
children.append(browseAction)
// Delete
if avatarImage != nil {
let deleteAction = UIAction(title: L10n.Scene.Register.Input.Avatar.delete, image: UIImage(systemName: "delete.left"), identifier: nil, discoverabilityTitle: nil, attributes: [.destructive], state: .off) { [weak self] _ in
guard let self = self else { return }
self.avatarMediaMenuActionPublisher.send(.delete)
}
children.append(deleteAction)
}
return UIMenu(title: "", image: nil, identifier: nil, options: .displayInline, children: children)
}
private func configureTextFieldCell(
cell: MastodonRegisterTextFieldTableViewCell,
validateState: Published<ValidateState>.Publisher

View File

@ -12,7 +12,7 @@ import UIKit
import MastodonAsset
import MastodonLocalization
final class MastodonRegisterViewModel {
final class MastodonRegisterViewModel: ObservableObject {
var disposeBag = Set<AnyCancellable>()
// input
@ -23,6 +23,7 @@ final class MastodonRegisterViewModel {
let applicationToken: Mastodon.Entity.Token
let viewDidAppear = CurrentValueSubject<Void, Never>(Void())
@Published var backgroundColor: UIColor = Asset.Scene.Onboarding.background.color
@Published var avatarImage: UIImage? = nil
@Published var name = ""
@Published var username = ""
@ -30,10 +31,12 @@ final class MastodonRegisterViewModel {
@Published var password = ""
@Published var reason = ""
let usernameErrorPrompt = CurrentValueSubject<NSAttributedString?, Never>(nil)
let emailErrorPrompt = CurrentValueSubject<NSAttributedString?, Never>(nil)
let passwordErrorPrompt = CurrentValueSubject<NSAttributedString?, Never>(nil)
let reasonErrorPrompt = CurrentValueSubject<NSAttributedString?, Never>(nil)
@Published var usernameErrorPrompt: String? = nil
@Published var emailErrorPrompt: String? = nil
@Published var passwordErrorPrompt: String? = nil
@Published var reasonErrorPrompt: String? = nil
@Published var bottomPaddingHeight: CGFloat = .zero
// output
var diffableDataSource: UITableViewDiffableDataSource<RegisterSection, RegisterItem>?
@ -51,6 +54,7 @@ final class MastodonRegisterViewModel {
@Published var error: Error? = nil
let avatarMediaMenuActionPublisher = PassthroughSubject<AvatarMediaMenuAction, Never>()
let endEditing = PassthroughSubject<Void, Never>()
init(
context: AppContext,
@ -97,45 +101,46 @@ final class MastodonRegisterViewModel {
.assign(to: \.usernameValidateState, on: self)
.store(in: &disposeBag)
// TODO: check username available
// username
// .filter { !$0.isEmpty }
// .debounce(for: .milliseconds(300), scheduler: DispatchQueue.main)
// .removeDuplicates()
// .compactMap { [weak self] text -> AnyPublisher<Result<Mastodon.Response.Content<Mastodon.Entity.Account>, Error>, Never>? in
// guard let self = self else { return nil }
// let query = Mastodon.API.Account.AccountLookupQuery(acct: text)
// return context.apiService.accountLookup(domain: domain, query: query, authorization: self.applicationAuthorization)
// .map {
// response -> Result<Mastodon.Response.Content<Mastodon.Entity.Account>, Error> in
// Result.success(response)
// }
// .catch { error in
// Just(Result.failure(error))
// }
// .eraseToAnyPublisher()
// }
// .switchToLatest()
// .sink { [weak self] result in
// guard let self = self else { return }
// switch result {
// case .success:
// let text = L10n.Scene.Register.Error.Reason.taken(L10n.Scene.Register.Error.Item.username)
// self.usernameErrorPrompt.value = MastodonRegisterViewModel.errorPromptAttributedString(for: text)
// self.usernameValidateState.value = .invalid
// case .failure:
// break
// }
// }
// .store(in: &disposeBag)
//
// usernameValidateState
// .sink { [weak self] validateState in
// if validateState == .valid {
// self?.usernameErrorPrompt.value = nil
// }
// }
// .store(in: &disposeBag)
// check username available
$username
.filter { !$0.isEmpty }
.debounce(for: .milliseconds(300), scheduler: DispatchQueue.main)
.removeDuplicates()
.compactMap { [weak self] text -> AnyPublisher<Result<Mastodon.Response.Content<Mastodon.Entity.Account>, Error>, Never>? in
guard let self = self else { return nil }
let query = Mastodon.API.Account.AccountLookupQuery(acct: text)
return context.apiService.accountLookup(domain: domain, query: query, authorization: self.applicationAuthorization)
.map {
response -> Result<Mastodon.Response.Content<Mastodon.Entity.Account>, Error> in
Result.success(response)
}
.catch { error in
Just(Result.failure(error))
}
.eraseToAnyPublisher()
}
.switchToLatest()
.receive(on: DispatchQueue.main)
.sink { [weak self] result in
guard let self = self else { return }
switch result {
case .success:
let text = L10n.Scene.Register.Error.Reason.taken(L10n.Scene.Register.Error.Item.username)
self.usernameErrorPrompt = text
self.usernameValidateState = .invalid
case .failure:
break
}
}
.store(in: &disposeBag)
$usernameValidateState
.sink { [weak self] validateState in
if validateState == .valid {
self?.usernameErrorPrompt = nil
}
}
.store(in: &disposeBag)
$email
.map { email in
@ -163,27 +168,31 @@ final class MastodonRegisterViewModel {
.store(in: &disposeBag)
}
// error
// .sink { [weak self] error in
// guard let self = self else { return }
// let error = error as? Mastodon.API.Error
// let mastodonError = error?.mastodonError
// if case let .generic(genericMastodonError) = mastodonError,
// let details = genericMastodonError.details
// {
// self.usernameErrorPrompt.value = details.usernameErrorDescriptions.first.flatMap { MastodonRegisterViewModel.errorPromptAttributedString(for: $0) }
// self.emailErrorPrompt.value = details.emailErrorDescriptions.first.flatMap { MastodonRegisterViewModel.errorPromptAttributedString(for: $0) }
// self.passwordErrorPrompt.value = details.passwordErrorDescriptions.first.flatMap { MastodonRegisterViewModel.errorPromptAttributedString(for: $0) }
// self.reasonErrorPrompt.value = details.reasonErrorDescriptions.first.flatMap { MastodonRegisterViewModel.errorPromptAttributedString(for: $0) }
// } else {
// self.usernameErrorPrompt.value = nil
// self.emailErrorPrompt.value = nil
// self.passwordErrorPrompt.value = nil
// self.reasonErrorPrompt.value = nil
// }
// }
// .store(in: &disposeBag)
//
$error
.sink { [weak self] error in
guard let self = self else { return }
let error = error as? Mastodon.API.Error
let mastodonError = error?.mastodonError
if case let .generic(genericMastodonError) = mastodonError,
let details = genericMastodonError.details
{
self.usernameErrorPrompt = details.usernameErrorDescriptions.first
details.usernameErrorDescriptions.first.flatMap { _ in self.usernameValidateState = .invalid }
self.emailErrorPrompt = details.emailErrorDescriptions.first
details.emailErrorDescriptions.first.flatMap { _ in self.emailValidateState = .invalid }
self.passwordErrorPrompt = details.passwordErrorDescriptions.first
details.passwordErrorDescriptions.first.flatMap { _ in self.passwordValidateState = .invalid }
self.reasonErrorPrompt = details.reasonErrorDescriptions.first
details.reasonErrorDescriptions.first.flatMap { _ in self.reasonValidateState = .invalid }
} else {
self.usernameErrorPrompt = nil
self.emailErrorPrompt = nil
self.passwordErrorPrompt = nil
self.reasonErrorPrompt = nil
}
}
.store(in: &disposeBag)
let publisherOne = Publishers.CombineLatest4(
$usernameValidateState,
$displayNameValidateState,
@ -213,7 +222,7 @@ final class MastodonRegisterViewModel {
}
extension MastodonRegisterViewModel {
enum ValidateState {
enum ValidateState: Hashable {
case empty
case invalid
case valid
@ -271,3 +280,52 @@ extension MastodonRegisterViewModel {
return attributeString
}
}
extension MastodonRegisterViewModel {
enum AvatarMediaMenuAction {
case photoLibrary
case camera
case browse
case delete
}
private func createAvatarMediaContextMenu() -> UIMenu {
var children: [UIMenuElement] = []
// Photo Library
let photoLibraryAction = UIAction(title: L10n.Scene.Compose.MediaSelection.photoLibrary, image: UIImage(systemName: "rectangle.on.rectangle"), identifier: nil, discoverabilityTitle: nil, attributes: [], state: .off) { [weak self] _ in
guard let self = self else { return }
self.avatarMediaMenuActionPublisher.send(.photoLibrary)
}
children.append(photoLibraryAction)
// Camera
if UIImagePickerController.isSourceTypeAvailable(.camera) {
let cameraAction = UIAction(title: L10n.Scene.Compose.MediaSelection.camera, image: UIImage(systemName: "camera"), identifier: nil, discoverabilityTitle: nil, attributes: [], state: .off, handler: { [weak self] _ in
guard let self = self else { return }
self.avatarMediaMenuActionPublisher.send(.camera)
})
children.append(cameraAction)
}
// Browse
let browseAction = UIAction(title: L10n.Scene.Compose.MediaSelection.browse, image: UIImage(systemName: "ellipsis"), identifier: nil, discoverabilityTitle: nil, attributes: [], state: .off) { [weak self] _ in
guard let self = self else { return }
self.avatarMediaMenuActionPublisher.send(.browse)
}
children.append(browseAction)
// Delete
if avatarImage != nil {
let deleteAction = UIAction(title: L10n.Scene.Register.Input.Avatar.delete, image: UIImage(systemName: "delete.left"), identifier: nil, discoverabilityTitle: nil, attributes: [.destructive], state: .off) { [weak self] _ in
guard let self = self else { return }
self.avatarMediaMenuActionPublisher.send(.delete)
}
children.append(deleteAction)
}
return UIMenu(title: "", image: nil, identifier: nil, options: .displayInline, children: children)
}
}

View File

@ -0,0 +1,50 @@
//
// MastodonServerRulesViewController+Debug.swift
// Mastodon
//
// Created by MainasuK on 2022-4-27.
//
import UIKit
#if DEBUG
extension MastodonRegisterViewController {
@MainActor
static func create(
context: AppContext,
coordinator: SceneCoordinator,
domain: String
) async throws -> MastodonRegisterViewController {
let viewController = MastodonRegisterViewController()
viewController.context = context
viewController.coordinator = coordinator
let instanceResponse = try await context.apiService.instance(domain: domain).singleOutput()
let applicationResponse = try await context.apiService.createApplication(domain: domain).singleOutput()
let accessTokenResponse = try await context.apiService.applicationAccessToken(
domain: domain,
clientID: applicationResponse.value.clientID!,
clientSecret: applicationResponse.value.clientSecret!,
redirectURI: applicationResponse.value.redirectURI!
).singleOutput()
viewController.viewModel = MastodonRegisterViewModel(
context: context,
domain: domain,
authenticateInfo: .init(
domain: domain,
application: applicationResponse.value
)!,
instance: instanceResponse.value,
applicationToken: accessTokenResponse.value
)
return viewController
}
}
#endif

View File

@ -270,7 +270,15 @@ extension MainTabBarController {
updateTabBarDisplay()
#if DEBUG
// selectedIndex = 1
// Debug Register viewController
// Task { @MainActor in
// let _homeTimelineViewController = viewControllers
// .compactMap { $0 as? UINavigationController }
// .compactMap { $0.topViewController }
// .compactMap { $0 as? HomeTimelineViewController }
// .first
// try await _homeTimelineViewController?.showRegisterController()
// } // end Task
#endif
}

View File

@ -1,5 +1,5 @@
//
// APIService+HomeTimeline.swift
// µ.swift
// Mastodon
//
// Created by MainasuK Cirno on 2021/2/3.

View File

@ -38,7 +38,7 @@ extension Mastodon.Entity {
// https://github.com/mastodon/mastodon/pull/16485
public let configuration: Configuration?
public init(domain: String) {
public init(domain: String, approvalRequired: Bool? = nil) {
self.uri = domain
self.title = domain
self.description = ""
@ -47,7 +47,7 @@ extension Mastodon.Entity {
self.version = nil
self.languages = nil
self.registrations = nil
self.approvalRequired = nil
self.approvalRequired = approvalRequired
self.invitesEnabled = nil
self.urls = nil
self.statistics = nil