From 2ae3f21a990e0cfc076e027ae2be38a9e29d77ab Mon Sep 17 00:00:00 2001 From: CMK Date: Wed, 27 Apr 2022 17:37:03 +0800 Subject: [PATCH] fix: add missing error prompt for sign up scene --- Mastodon.xcodeproj/project.pbxproj | 33 +- .../xcschemes/xcschememanagement.plist | 6 +- ...meTimelineViewController+DebugAction.swift | 31 ++ .../MastodonPickServerViewController.swift | 13 +- .../Register/MastodonRegisterView.swift | 304 ++++++++++++++++++ .../MastodonRegisterViewController.swift | 117 ++----- .../MastodonRegisterViewModel+Diffable.swift | 47 +-- .../Register/MastodonRegisterViewModel.swift | 190 +++++++---- ...todonServerRulesViewController+Debug.swift | 50 +++ .../Root/MainTab/MainTabBarController.swift | 10 +- .../APIService/APIService+HomeTimeline.swift | 2 +- .../Entity/Mastodon+Entity+Instance.swift | 4 +- 12 files changed, 581 insertions(+), 226 deletions(-) create mode 100644 Mastodon/Scene/Onboarding/Register/MastodonRegisterView.swift create mode 100644 Mastodon/Scene/Onboarding/ServerRules/MastodonServerRulesViewController+Debug.swift diff --git a/Mastodon.xcodeproj/project.pbxproj b/Mastodon.xcodeproj/project.pbxproj index 3cb611c51..23d536ddd 100644 --- a/Mastodon.xcodeproj/project.pbxproj +++ b/Mastodon.xcodeproj/project.pbxproj @@ -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 = ""; }; DB789A0A25F9F2950071ACA0 /* ComposeViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComposeViewController.swift; sourceTree = ""; }; DB789A1125F9F2CC0071ACA0 /* ComposeViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComposeViewModel.swift; sourceTree = ""; }; + DB7A9F902818EAF10016AF98 /* MastodonRegisterView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MastodonRegisterView.swift; sourceTree = ""; }; + DB7A9F922818F33C0016AF98 /* MastodonServerRulesViewController+Debug.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "MastodonServerRulesViewController+Debug.swift"; sourceTree = ""; }; DB7F48442620241000796008 /* ProfileHeaderViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfileHeaderViewModel.swift; sourceTree = ""; }; DB8190C52601FF0400020C08 /* AttachmentContainerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AttachmentContainerView.swift; sourceTree = ""; }; DB8481142788121200BBEABA /* MastodonRegisterTextFieldTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MastodonRegisterTextFieldTableViewCell.swift; sourceTree = ""; }; @@ -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 = ""; @@ -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" */; diff --git a/Mastodon.xcodeproj/xcuserdata/mainasuk.xcuserdatad/xcschemes/xcschememanagement.plist b/Mastodon.xcodeproj/xcuserdata/mainasuk.xcuserdatad/xcschemes/xcschememanagement.plist index dd92d9e44..df6f38a96 100644 --- a/Mastodon.xcodeproj/xcuserdata/mainasuk.xcuserdatad/xcschemes/xcschememanagement.plist +++ b/Mastodon.xcodeproj/xcuserdata/mainasuk.xcuserdatad/xcschemes/xcschememanagement.plist @@ -109,7 +109,7 @@ MastodonIntent.xcscheme_^#shared#^_ orderHint - 34 + 24 MastodonIntents.xcscheme_^#shared#^_ @@ -124,12 +124,12 @@ NotificationService.xcscheme_^#shared#^_ orderHint - 29 + 23 ShareActionExtension.xcscheme_^#shared#^_ orderHint - 33 + 22 SuppressBuildableAutocreation diff --git a/Mastodon/Scene/HomeTimeline/HomeTimelineViewController+DebugAction.swift b/Mastodon/Scene/HomeTimeline/HomeTimelineViewController+DebugAction.swift index 8b1d390f5..8b6eb9f42 100644 --- a/Mastodon/Scene/HomeTimeline/HomeTimelineViewController+DebugAction.swift +++ b/Mastodon/Scene/HomeTimeline/HomeTimelineViewController+DebugAction.swift @@ -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() diff --git a/Mastodon/Scene/Onboarding/PickServer/MastodonPickServerViewController.swift b/Mastodon/Scene/Onboarding/PickServer/MastodonPickServerViewController.swift index b86c952a4..ffc7708f5 100644 --- a/Mastodon/Scene/Onboarding/PickServer/MastodonPickServerViewController.swift +++ b/Mastodon/Scene/Onboarding/PickServer/MastodonPickServerViewController.swift @@ -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? 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() diff --git a/Mastodon/Scene/Onboarding/Register/MastodonRegisterView.swift b/Mastodon/Scene/Onboarding/Register/MastodonRegisterView.swift new file mode 100644 index 000000000..1a47de22f --- /dev/null +++ b/Mastodon/Scene/Onboarding/Register/MastodonRegisterView.swift @@ -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 diff --git a/Mastodon/Scene/Onboarding/Register/MastodonRegisterViewController.swift b/Mastodon/Scene/Onboarding/Register/MastodonRegisterViewController.swift index 903c9fbca..89c98759f 100644 --- a/Mastodon/Scene/Onboarding/Register/MastodonRegisterViewController.swift +++ b/Mastodon/Scene/Onboarding/Register/MastodonRegisterViewController.swift @@ -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) diff --git a/Mastodon/Scene/Onboarding/Register/MastodonRegisterViewModel+Diffable.swift b/Mastodon/Scene/Onboarding/Register/MastodonRegisterViewModel+Diffable.swift index beb16890b..d8dc8943d 100644 --- a/Mastodon/Scene/Onboarding/Register/MastodonRegisterViewModel+Diffable.swift +++ b/Mastodon/Scene/Onboarding/Register/MastodonRegisterViewModel+Diffable.swift @@ -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.Publisher diff --git a/Mastodon/Scene/Onboarding/Register/MastodonRegisterViewModel.swift b/Mastodon/Scene/Onboarding/Register/MastodonRegisterViewModel.swift index 1ef9cf47a..e7fbd307d 100644 --- a/Mastodon/Scene/Onboarding/Register/MastodonRegisterViewModel.swift +++ b/Mastodon/Scene/Onboarding/Register/MastodonRegisterViewModel.swift @@ -12,7 +12,7 @@ import UIKit import MastodonAsset import MastodonLocalization -final class MastodonRegisterViewModel { +final class MastodonRegisterViewModel: ObservableObject { var disposeBag = Set() // input @@ -23,6 +23,7 @@ final class MastodonRegisterViewModel { let applicationToken: Mastodon.Entity.Token let viewDidAppear = CurrentValueSubject(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(nil) - let emailErrorPrompt = CurrentValueSubject(nil) - let passwordErrorPrompt = CurrentValueSubject(nil) - let reasonErrorPrompt = CurrentValueSubject(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? @@ -51,6 +54,7 @@ final class MastodonRegisterViewModel { @Published var error: Error? = nil let avatarMediaMenuActionPublisher = PassthroughSubject() + let endEditing = PassthroughSubject() 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, 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, 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, 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, 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) + } + +} diff --git a/Mastodon/Scene/Onboarding/ServerRules/MastodonServerRulesViewController+Debug.swift b/Mastodon/Scene/Onboarding/ServerRules/MastodonServerRulesViewController+Debug.swift new file mode 100644 index 000000000..f6aaf5fba --- /dev/null +++ b/Mastodon/Scene/Onboarding/ServerRules/MastodonServerRulesViewController+Debug.swift @@ -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 + diff --git a/Mastodon/Scene/Root/MainTab/MainTabBarController.swift b/Mastodon/Scene/Root/MainTab/MainTabBarController.swift index f470082f2..1b08722d5 100644 --- a/Mastodon/Scene/Root/MainTab/MainTabBarController.swift +++ b/Mastodon/Scene/Root/MainTab/MainTabBarController.swift @@ -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 } diff --git a/Mastodon/Service/APIService/APIService+HomeTimeline.swift b/Mastodon/Service/APIService/APIService+HomeTimeline.swift index 39d4cf6e1..863510af4 100644 --- a/Mastodon/Service/APIService/APIService+HomeTimeline.swift +++ b/Mastodon/Service/APIService/APIService+HomeTimeline.swift @@ -1,5 +1,5 @@ // -// APIService+HomeTimeline.swift +// ยต.swift // Mastodon // // Created by MainasuK Cirno on 2021/2/3. diff --git a/MastodonSDK/Sources/MastodonSDK/Entity/Mastodon+Entity+Instance.swift b/MastodonSDK/Sources/MastodonSDK/Entity/Mastodon+Entity+Instance.swift index 7cf4890bc..5d649a841 100644 --- a/MastodonSDK/Sources/MastodonSDK/Entity/Mastodon+Entity+Instance.swift +++ b/MastodonSDK/Sources/MastodonSDK/Entity/Mastodon+Entity+Instance.swift @@ -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