diff --git a/Localization/app.json b/Localization/app.json index ce5ba81e0..e20e901db 100644 --- a/Localization/app.json +++ b/Localization/app.json @@ -22,8 +22,8 @@ "cancel": "Cancel", "take_photo": "Take photo", "save_photo": "Save photo", - "sign_in": "Sign in", - "sign_up": "Sign up", + "sign_in": "Sign In", + "sign_up": "Sign Up", "see_more": "See More", "preview": "Preview", "open_in_safari": "Open in Safari" @@ -110,7 +110,7 @@ "dont_receive_email": { "title": "Check your email", "description": "Check if your email address is correct as well as your junk folder if you haven’t.", - "resend_email": "Resend email" + "resend_email": "Resend Email" }, "open_email_app": { "title": "Check your inbox.", diff --git a/Mastodon.xcodeproj/project.pbxproj b/Mastodon.xcodeproj/project.pbxproj index 58e0f5c62..654132b8a 100644 --- a/Mastodon.xcodeproj/project.pbxproj +++ b/Mastodon.xcodeproj/project.pbxproj @@ -10,8 +10,8 @@ 0FAA0FDF25E0B57E0017CCDE /* WelcomeViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0FAA0FDE25E0B57E0017CCDE /* WelcomeViewController.swift */; }; 0FAA101225E105390017CCDE /* PrimaryActionButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0FAA101125E105390017CCDE /* PrimaryActionButton.swift */; }; 0FAA101C25E10E760017CCDE /* UIFont.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0FAA101B25E10E760017CCDE /* UIFont.swift */; }; - 0FAA102725E1126A0017CCDE /* PickServerViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0FAA102625E1126A0017CCDE /* PickServerViewController.swift */; }; - 0FB3D2F725E4C24D00AAD544 /* PickServerViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0FB3D2F625E4C24D00AAD544 /* PickServerViewModel.swift */; }; + 0FAA102725E1126A0017CCDE /* MastodonPickServerViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0FAA102625E1126A0017CCDE /* MastodonPickServerViewController.swift */; }; + 0FB3D2F725E4C24D00AAD544 /* MastodonPickServerViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0FB3D2F625E4C24D00AAD544 /* MastodonPickServerViewModel.swift */; }; 0FB3D2FE25E4CB6400AAD544 /* PickServerTitleCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0FB3D2FD25E4CB6400AAD544 /* PickServerTitleCell.swift */; }; 0FB3D30825E524C600AAD544 /* PickServerCategoriesCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0FB3D30725E524C600AAD544 /* PickServerCategoriesCell.swift */; }; 0FB3D30F25E525CD00AAD544 /* PickServerCategoryView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0FB3D30E25E525CD00AAD544 /* PickServerCategoryView.swift */; }; @@ -83,7 +83,6 @@ 5D526FE225BE9AC400460CB9 /* MastodonSDK in Frameworks */ = {isa = PBXBuildFile; productRef = 5D526FE125BE9AC400460CB9 /* MastodonSDK */; }; 5E0DEC05797A7E6933788DDB /* Pods_MastodonTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 452147B2903DF38070FE56A2 /* Pods_MastodonTests.framework */; }; 5E44BF88AD33646E64727BCF /* Pods_MastodonTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = CD92E0F10BDE4FE7C4B999F2 /* Pods_MastodonTests.framework */; }; - DB01409625C40B6700F9F3CF /* AuthenticationViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB01409525C40B6700F9F3CF /* AuthenticationViewController.swift */; }; DB0140A125C40C0600F9F3CF /* MastodonPinBasedAuthenticationViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB0140A025C40C0600F9F3CF /* MastodonPinBasedAuthenticationViewController.swift */; }; DB0140A825C40C1500F9F3CF /* MastodonPinBasedAuthenticationViewModelNavigationDelegateShim.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB0140A725C40C1500F9F3CF /* MastodonPinBasedAuthenticationViewModelNavigationDelegateShim.swift */; }; DB0140AE25C40C7300F9F3CF /* MastodonPinBasedAuthenticationViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB0140AD25C40C7300F9F3CF /* MastodonPinBasedAuthenticationViewModel.swift */; }; @@ -95,6 +94,7 @@ DB118A8C25E4BFB500FAB162 /* HighlightDimmableButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB118A8B25E4BFB500FAB162 /* HighlightDimmableButton.swift */; }; DB2B3ABC25E37E15007045F9 /* InfoPlist.strings in Resources */ = {isa = PBXBuildFile; fileRef = DB2B3ABE25E37E15007045F9 /* InfoPlist.strings */; }; DB2B3AE925E38850007045F9 /* UIViewPreview.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB2B3AE825E38850007045F9 /* UIViewPreview.swift */; }; + DB2F073525E8ECF000957B2D /* AuthenticationViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB2F073325E8ECF000957B2D /* AuthenticationViewModel.swift */; }; DB3D0FF325BAA61700EAA174 /* AlamofireImage in Frameworks */ = {isa = PBXBuildFile; productRef = DB3D0FF225BAA61700EAA174 /* AlamofireImage */; }; DB3D100D25BAA75E00EAA174 /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = DB3D100F25BAA75E00EAA174 /* Localizable.strings */; }; DB427DD625BAA00100D1B89D /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB427DD525BAA00100D1B89D /* AppDelegate.swift */; }; @@ -118,6 +118,7 @@ DB5086B825CC0D6400C2C187 /* Kingfisher in Frameworks */ = {isa = PBXBuildFile; productRef = DB5086B725CC0D6400C2C187 /* Kingfisher */; }; DB5086BE25CC0D9900C2C187 /* SplashPreference.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB5086BD25CC0D9900C2C187 /* SplashPreference.swift */; }; DB68586425E619B700F0A850 /* NSKeyValueObservation.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB68586325E619B700F0A850 /* NSKeyValueObservation.swift */; }; + DB68A04A25E9027700CFDF14 /* DarkContentStatusBarStyleNavigationController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB68A04925E9027700CFDF14 /* DarkContentStatusBarStyleNavigationController.swift */; }; DB72601C25E36A2100235243 /* MastodonServerRulesViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB72601B25E36A2100235243 /* MastodonServerRulesViewController.swift */; }; DB72602725E36A6F00235243 /* MastodonServerRulesViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB72602625E36A6F00235243 /* MastodonServerRulesViewModel.swift */; }; DB89B9F725C10FD0008580ED /* CoreDataStack.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = DB89B9EE25C10FD0008580ED /* CoreDataStack.framework */; }; @@ -142,7 +143,6 @@ DB8AF55025C13703002E6C99 /* MainTabBarController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB8AF54F25C13703002E6C99 /* MainTabBarController.swift */; }; DB8AF55D25C138B7002E6C99 /* UIViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB8AF55C25C138B7002E6C99 /* UIViewController.swift */; }; DB8AF56825C13E2A002E6C99 /* HomeTimelineIndex.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB8AF56725C13E2A002E6C99 /* HomeTimelineIndex.swift */; }; - DB98334725C8056600AD9700 /* AuthenticationViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB98334625C8056600AD9700 /* AuthenticationViewModel.swift */; }; DB98336B25C9420100AD9700 /* APIService+App.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB98336A25C9420100AD9700 /* APIService+App.swift */; }; DB98337125C9443200AD9700 /* APIService+Authentication.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB98337025C9443200AD9700 /* APIService+Authentication.swift */; }; DB98337F25C9452D00AD9700 /* APIService+APIError.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB98337E25C9452D00AD9700 /* APIService+APIError.swift */; }; @@ -217,8 +217,8 @@ 0FAA0FDE25E0B57E0017CCDE /* WelcomeViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WelcomeViewController.swift; sourceTree = ""; }; 0FAA101125E105390017CCDE /* PrimaryActionButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PrimaryActionButton.swift; sourceTree = ""; }; 0FAA101B25E10E760017CCDE /* UIFont.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UIFont.swift; sourceTree = ""; }; - 0FAA102625E1126A0017CCDE /* PickServerViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PickServerViewController.swift; sourceTree = ""; }; - 0FB3D2F625E4C24D00AAD544 /* PickServerViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PickServerViewModel.swift; sourceTree = ""; }; + 0FAA102625E1126A0017CCDE /* MastodonPickServerViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MastodonPickServerViewController.swift; sourceTree = ""; }; + 0FB3D2F625E4C24D00AAD544 /* MastodonPickServerViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MastodonPickServerViewModel.swift; sourceTree = ""; }; 0FB3D2FD25E4CB6400AAD544 /* PickServerTitleCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PickServerTitleCell.swift; sourceTree = ""; }; 0FB3D30725E524C600AAD544 /* PickServerCategoriesCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PickServerCategoriesCell.swift; sourceTree = ""; }; 0FB3D30E25E525CD00AAD544 /* PickServerCategoryView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PickServerCategoryView.swift; sourceTree = ""; }; @@ -292,7 +292,6 @@ A4ABE34829701A4496C5BB64 /* Pods_Mastodon.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Mastodon.framework; sourceTree = BUILT_PRODUCTS_DIR; }; BB482D32A7B9825BF5327C4F /* Pods-Mastodon-MastodonUITests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Mastodon-MastodonUITests.release.xcconfig"; path = "Target Support Files/Pods-Mastodon-MastodonUITests/Pods-Mastodon-MastodonUITests.release.xcconfig"; sourceTree = ""; }; CD92E0F10BDE4FE7C4B999F2 /* Pods_MastodonTests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_MastodonTests.framework; sourceTree = BUILT_PRODUCTS_DIR; }; - DB01409525C40B6700F9F3CF /* AuthenticationViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AuthenticationViewController.swift; sourceTree = ""; }; DB0140A025C40C0600F9F3CF /* MastodonPinBasedAuthenticationViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MastodonPinBasedAuthenticationViewController.swift; sourceTree = ""; }; DB0140A725C40C1500F9F3CF /* MastodonPinBasedAuthenticationViewModelNavigationDelegateShim.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MastodonPinBasedAuthenticationViewModelNavigationDelegateShim.swift; sourceTree = ""; }; DB0140AD25C40C7300F9F3CF /* MastodonPinBasedAuthenticationViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MastodonPinBasedAuthenticationViewModel.swift; sourceTree = ""; }; @@ -303,6 +302,7 @@ DB118A8B25E4BFB500FAB162 /* HighlightDimmableButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HighlightDimmableButton.swift; sourceTree = ""; }; DB2B3ABD25E37E15007045F9 /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/InfoPlist.strings; sourceTree = ""; }; DB2B3AE825E38850007045F9 /* UIViewPreview.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UIViewPreview.swift; sourceTree = ""; }; + DB2F073325E8ECF000957B2D /* AuthenticationViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AuthenticationViewModel.swift; sourceTree = ""; }; DB3D0FED25BAA42200EAA174 /* MastodonSDK */ = {isa = PBXFileReference; lastKnownFileType = folder; path = MastodonSDK; sourceTree = ""; }; DB3D100E25BAA75E00EAA174 /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/Localizable.strings; sourceTree = ""; }; DB427DD225BAA00100D1B89D /* Mastodon.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Mastodon.app; sourceTree = BUILT_PRODUCTS_DIR; }; @@ -331,6 +331,7 @@ DB5086AA25CC0BBB00C2C187 /* AvatarConfigurableView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AvatarConfigurableView.swift; sourceTree = ""; }; DB5086BD25CC0D9900C2C187 /* SplashPreference.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SplashPreference.swift; sourceTree = ""; }; DB68586325E619B700F0A850 /* NSKeyValueObservation.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NSKeyValueObservation.swift; sourceTree = ""; }; + DB68A04925E9027700CFDF14 /* DarkContentStatusBarStyleNavigationController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DarkContentStatusBarStyleNavigationController.swift; sourceTree = ""; }; DB72601B25E36A2100235243 /* MastodonServerRulesViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MastodonServerRulesViewController.swift; sourceTree = ""; }; DB72602625E36A6F00235243 /* MastodonServerRulesViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MastodonServerRulesViewModel.swift; sourceTree = ""; }; DB89B9EE25C10FD0008580ED /* CoreDataStack.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = CoreDataStack.framework; sourceTree = BUILT_PRODUCTS_DIR; }; @@ -357,7 +358,6 @@ DB8AF54F25C13703002E6C99 /* MainTabBarController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MainTabBarController.swift; sourceTree = ""; }; DB8AF55C25C138B7002E6C99 /* UIViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UIViewController.swift; sourceTree = ""; }; DB8AF56725C13E2A002E6C99 /* HomeTimelineIndex.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomeTimelineIndex.swift; sourceTree = ""; }; - DB98334625C8056600AD9700 /* AuthenticationViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AuthenticationViewModel.swift; sourceTree = ""; }; DB98336A25C9420100AD9700 /* APIService+App.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "APIService+App.swift"; sourceTree = ""; }; DB98337025C9443200AD9700 /* APIService+Authentication.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "APIService+Authentication.swift"; sourceTree = ""; }; DB98337E25C9452D00AD9700 /* APIService+APIError.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "APIService+APIError.swift"; sourceTree = ""; }; @@ -445,8 +445,8 @@ 0FB3D31825E525DE00AAD544 /* CollectionViewCell */, 0FB3D30D25E525C000AAD544 /* View */, 0FB3D2FC25E4CB4B00AAD544 /* TableViewCell */, - 0FAA102625E1126A0017CCDE /* PickServerViewController.swift */, - 0FB3D2F625E4C24D00AAD544 /* PickServerViewModel.swift */, + 0FAA102625E1126A0017CCDE /* MastodonPickServerViewController.swift */, + 0FB3D2F625E4C24D00AAD544 /* MastodonPickServerViewModel.swift */, ); path = PickServer; sourceTree = ""; @@ -598,7 +598,6 @@ 2D38F1C525CD37F400561493 /* ContentOffsetAdjustableTimelineViewControllerDelegate.swift */, 2D38F20725CD491300561493 /* DisposeBagCollectable.swift */, 2D5A3D3725CF8D9F002347D6 /* ScrollViewContainer.swift */, - 2D82B9FE25E7863200E36F0F /* OnboardingViewControllerAppearance.swift */, ); path = Protocol; sourceTree = ""; @@ -636,6 +635,7 @@ 2D7631A425C1532200929FB9 /* Share */ = { isa = PBXGroup; children = ( + DB68A04F25E9028800CFDF14 /* NavigationController */, DB9D6C2025E502C60051B173 /* ViewModel */, 2D7631A525C1532D00929FB9 /* View */, ); @@ -695,27 +695,26 @@ DB01409B25C40BB600F9F3CF /* Onboarding */ = { isa = PBXGroup; children = ( + DB68A03825E900CC00CFDF14 /* Share */, 0FAA0FDD25E0B5700017CCDE /* Welcome */, 0FAA102525E1125D0017CCDE /* PickServer */, + DB0140A625C40C0900F9F3CF /* PinBasedAuthentication */, DBE0821A25CD382900FD6BBD /* Register */, DB72602125E36A2500235243 /* ServerRules */, 2D364F7025E66D5B00204FDC /* ResendEmail */, 2D59819925E4A55C000FB903 /* ConfirmEmail */, - DB0140A625C40C0900F9F3CF /* PinBased */, - DB01409525C40B6700F9F3CF /* AuthenticationViewController.swift */, - DB98334625C8056600AD9700 /* AuthenticationViewModel.swift */, ); path = Onboarding; sourceTree = ""; }; - DB0140A625C40C0900F9F3CF /* PinBased */ = { + DB0140A625C40C0900F9F3CF /* PinBasedAuthentication */ = { isa = PBXGroup; children = ( DB0140A025C40C0600F9F3CF /* MastodonPinBasedAuthenticationViewController.swift */, DB0140AD25C40C7300F9F3CF /* MastodonPinBasedAuthenticationViewModel.swift */, DB0140A725C40C1500F9F3CF /* MastodonPinBasedAuthenticationViewModelNavigationDelegateShim.swift */, ); - path = PinBased; + path = PinBasedAuthentication; sourceTree = ""; }; DB084B5125CBC56300F898ED /* CoreDataStack */ = { @@ -856,6 +855,23 @@ path = Preference; sourceTree = ""; }; + DB68A03825E900CC00CFDF14 /* Share */ = { + isa = PBXGroup; + children = ( + 2D82B9FE25E7863200E36F0F /* OnboardingViewControllerAppearance.swift */, + DB2F073325E8ECF000957B2D /* AuthenticationViewModel.swift */, + ); + path = Share; + sourceTree = ""; + }; + DB68A04F25E9028800CFDF14 /* NavigationController */ = { + isa = PBXGroup; + children = ( + DB68A04925E9027700CFDF14 /* DarkContentStatusBarStyleNavigationController.swift */, + ); + path = NavigationController; + sourceTree = ""; + }; DB72602125E36A2500235243 /* ServerRules */ = { isa = PBXGroup; children = ( @@ -1412,7 +1428,7 @@ 2D59819B25E4A581000FB903 /* MastodonConfirmEmailViewController.swift in Sources */, DB45FB1D25CA9D23005A8AC7 /* APIService+HomeTimeline.swift in Sources */, 2D7631B325C159F700929FB9 /* Item.swift in Sources */, - 0FB3D2F725E4C24D00AAD544 /* PickServerViewModel.swift in Sources */, + 0FB3D2F725E4C24D00AAD544 /* MastodonPickServerViewModel.swift in Sources */, 2D61335E25C1894B00CAE157 /* APIService.swift in Sources */, 2D38F1F725CD47AC00561493 /* HomeTimelineViewModel+LoadOldestState.swift in Sources */, 0FB3D33225E5F50E00AAD544 /* PickServerSearchCell.swift in Sources */, @@ -1427,7 +1443,7 @@ 2D42FF8525C8224F004A627A /* HitTestExpandedButton.swift in Sources */, DB72601C25E36A2100235243 /* MastodonServerRulesViewController.swift in Sources */, 2D42FF8F25C8228A004A627A /* UIButton.swift in Sources */, - 0FAA102725E1126A0017CCDE /* PickServerViewController.swift in Sources */, + 0FAA102725E1126A0017CCDE /* MastodonPickServerViewController.swift in Sources */, DB68586425E619B700F0A850 /* NSKeyValueObservation.swift in Sources */, 2D61335825C188A000CAE157 /* APIService+Persist+Timeline.swift in Sources */, DB45FAE325CA7181005A8AC7 /* MastodonUser.swift in Sources */, @@ -1467,13 +1483,14 @@ DB9D6BE925E4F5340051B173 /* SearchViewController.swift in Sources */, 2D38F1C625CD37F400561493 /* ContentOffsetAdjustableTimelineViewControllerDelegate.swift in Sources */, 0FB3D30F25E525CD00AAD544 /* PickServerCategoryView.swift in Sources */, + DB2F073525E8ECF000957B2D /* AuthenticationViewModel.swift in Sources */, + DB68A04A25E9027700CFDF14 /* DarkContentStatusBarStyleNavigationController.swift in Sources */, 0FB3D33825E6401400AAD544 /* PickServerCell.swift in Sources */, 2D364F7825E66D8300204FDC /* MastodonResendEmailViewModel.swift in Sources */, DB8AF54525C13647002E6C99 /* NeedsDependency.swift in Sources */, DB9D6BF825E4F5690051B173 /* NotificationViewController.swift in Sources */, DB45FADD25CA6F6B005A8AC7 /* APIService+CoreData+MastodonUser.swift in Sources */, 2D32EABA25CB9B0500C9ED86 /* UIView.swift in Sources */, - DB98334725C8056600AD9700 /* AuthenticationViewModel.swift in Sources */, 2D38F20825CD491300561493 /* DisposeBagCollectable.swift in Sources */, DB0140CF25C42AEE00F9F3CF /* OSLog.swift in Sources */, 2D76319F25C1521200929FB9 /* StatusSection.swift in Sources */, @@ -1504,7 +1521,6 @@ 2D7631A825C1535600929FB9 /* StatusTableViewCell.swift in Sources */, 2D76316525C14BD100929FB9 /* PublicTimelineViewController.swift in Sources */, 2D69CFF425CA9E2200C3A1B2 /* LoadMoreConfigurableTableViewContainer.swift in Sources */, - DB01409625C40B6700F9F3CF /* AuthenticationViewController.swift in Sources */, DB427DD825BAA00100D1B89D /* SceneDelegate.swift in Sources */, DB5086AB25CC0BBB00C2C187 /* AvatarConfigurableView.swift in Sources */, DB0140AE25C40C7300F9F3CF /* MastodonPinBasedAuthenticationViewModel.swift in Sources */, diff --git a/Mastodon.xcodeproj/xcuserdata/mainasuk.xcuserdatad/xcschemes/xcschememanagement.plist b/Mastodon.xcodeproj/xcuserdata/mainasuk.xcuserdatad/xcschemes/xcschememanagement.plist index c99f0b7b5..026fc28dc 100644 --- a/Mastodon.xcodeproj/xcuserdata/mainasuk.xcuserdatad/xcschemes/xcschememanagement.plist +++ b/Mastodon.xcodeproj/xcuserdata/mainasuk.xcuserdatad/xcschemes/xcschememanagement.plist @@ -7,12 +7,17 @@ CoreDataStack.xcscheme_^#shared#^_ orderHint - 9 + 11 Mastodon - RTL.xcscheme_^#shared#^_ orderHint - 8 + 9 + + Mastodon - Release.xcscheme_^#shared#^_ + + orderHint + 1 Mastodon.xcscheme_^#shared#^_ diff --git a/Mastodon/Coordinator/SceneCoordinator.swift b/Mastodon/Coordinator/SceneCoordinator.swift index 304facf8a..fa2963386 100644 --- a/Mastodon/Coordinator/SceneCoordinator.swift +++ b/Mastodon/Coordinator/SceneCoordinator.swift @@ -38,44 +38,61 @@ extension SceneCoordinator { } enum Scene { + // onboarding case welcome - case pickServer(viewMode: PickServerViewModel) - case authentication(viewModel: AuthenticationViewModel) + case mastodonPickServer(viewMode: MastodonPickServerViewModel) case mastodonPinBasedAuthentication(viewModel: MastodonPinBasedAuthenticationViewModel) case mastodonRegister(viewModel: MastodonRegisterViewModel) case mastodonServerRules(viewModel: MastodonServerRulesViewModel) case mastodonConfirmEmail(viewModel: MastodonConfirmEmailViewModel) case mastodonResendEmail(viewModel: MastodonResendEmailViewModel) + // misc case alertController(alertController: UIAlertController) #if DEBUG case publicTimeline #endif + + var isOnboarding: Bool { + switch self { + case .welcome, + .mastodonPickServer, + .mastodonPinBasedAuthentication, + .mastodonRegister, + .mastodonServerRules, + .mastodonConfirmEmail, + .mastodonResendEmail: + return true + default: + return false + } + } } } extension SceneCoordinator { func setup() { - // Check user authentication status - - let request = MastodonAuthentication.sortedFetchRequest + let viewController = MainTabBarController(context: appContext, coordinator: self) + sceneDelegate.window?.rootViewController = viewController + } + + func setupOnboardingIfNeeds(animated: Bool) { + // Check user authentication status and show onboarding if needs do { - let fetchResult = try appContext.managedObjectContext.fetch(request) - DispatchQueue.main.async { - var rootViewController: UIViewController - if fetchResult.isEmpty { - let welcomViewController = WelcomeViewController() - self.setupDependency(for: welcomViewController) - rootViewController = UINavigationController(rootViewController: welcomViewController) - } else { - rootViewController = MainTabBarController(context: self.appContext, coordinator: self) + let request = MastodonAuthentication.sortedFetchRequest + if try appContext.managedObjectContext.fetch(request).isEmpty { + DispatchQueue.main.async { + self.present( + scene: .welcome, + from: nil, + transition: .modal(animated: animated, completion: nil) + ) } - self.sceneDelegate.window?.rootViewController = rootViewController } } catch { - assertionFailure("CoreDataStack error at app launch!") + assertionFailure(error.localizedDescription) } } @@ -103,7 +120,13 @@ extension SceneCoordinator { presentingViewController.showDetailViewController(navigationController, sender: sender) case .modal(let animated, let completion): - let modalNavigationController = UINavigationController(rootViewController: viewController) + let modalNavigationController: UINavigationController = { + if scene.isOnboarding { + return DarkContentStatusBarStyleNavigationController(rootViewController: viewController) + } else { + return UINavigationController(rootViewController: viewController) + } + }() if let adaptivePresentationControllerDelegate = viewController as? UIAdaptivePresentationControllerDelegate { modalNavigationController.presentationController?.delegate = adaptivePresentationControllerDelegate } @@ -143,12 +166,8 @@ private extension SceneCoordinator { case .welcome: let _viewController = WelcomeViewController() viewController = _viewController - case .pickServer(let viewModel): - let _viewController = PickServerViewController() - _viewController.viewModel = viewModel - viewController = _viewController - case .authentication(let viewModel): - let _viewController = AuthenticationViewController() + case .mastodonPickServer(let viewModel): + let _viewController = MastodonPickServerViewController() _viewController.viewModel = viewModel viewController = _viewController case .mastodonPinBasedAuthentication(let viewModel): diff --git a/Mastodon/Generated/Strings.swift b/Mastodon/Generated/Strings.swift index 7223cfc73..47cbabab8 100644 --- a/Mastodon/Generated/Strings.swift +++ b/Mastodon/Generated/Strings.swift @@ -50,9 +50,9 @@ internal enum L10n { internal static let savePhoto = L10n.tr("Localizable", "Common.Controls.Actions.SavePhoto") /// See More internal static let seeMore = L10n.tr("Localizable", "Common.Controls.Actions.SeeMore") - /// Sign in + /// Sign In internal static let signIn = L10n.tr("Localizable", "Common.Controls.Actions.SignIn") - /// Sign up + /// Sign Up internal static let signUp = L10n.tr("Localizable", "Common.Controls.Actions.SignUp") /// Take photo internal static let takePhoto = L10n.tr("Localizable", "Common.Controls.Actions.TakePhoto") @@ -101,7 +101,7 @@ internal enum L10n { internal enum DontReceiveEmail { /// Check if your email address is correct as well as your junk folder if you haven’t. internal static let description = L10n.tr("Localizable", "Scene.ConfirmEmail.DontReceiveEmail.Description") - /// Resend email + /// Resend Email internal static let resendEmail = L10n.tr("Localizable", "Scene.ConfirmEmail.DontReceiveEmail.ResendEmail") /// Check your email internal static let title = L10n.tr("Localizable", "Scene.ConfirmEmail.DontReceiveEmail.Title") diff --git a/Mastodon/Resources/en.lproj/Localizable.strings b/Mastodon/Resources/en.lproj/Localizable.strings index 3902036cd..b3df9a77f 100644 --- a/Mastodon/Resources/en.lproj/Localizable.strings +++ b/Mastodon/Resources/en.lproj/Localizable.strings @@ -13,8 +13,8 @@ "Common.Controls.Actions.Save" = "Save"; "Common.Controls.Actions.SavePhoto" = "Save photo"; "Common.Controls.Actions.SeeMore" = "See More"; -"Common.Controls.Actions.SignIn" = "Sign in"; -"Common.Controls.Actions.SignUp" = "Sign up"; +"Common.Controls.Actions.SignIn" = "Sign In"; +"Common.Controls.Actions.SignUp" = "Sign Up"; "Common.Controls.Actions.TakePhoto" = "Take photo"; "Common.Controls.Status.MediaContentWarning" = "Tap to reveal that may be sensitive"; "Common.Controls.Status.ShowPost" = "Show Post"; @@ -26,7 +26,7 @@ "Scene.ConfirmEmail.Button.DontReceiveEmail" = "I never got an email"; "Scene.ConfirmEmail.Button.OpenEmailApp" = "Open Email App"; "Scene.ConfirmEmail.DontReceiveEmail.Description" = "Check if your email address is correct as well as your junk folder if you haven’t."; -"Scene.ConfirmEmail.DontReceiveEmail.ResendEmail" = "Resend email"; +"Scene.ConfirmEmail.DontReceiveEmail.ResendEmail" = "Resend Email"; "Scene.ConfirmEmail.DontReceiveEmail.Title" = "Check your email"; "Scene.ConfirmEmail.OpenEmailApp.Description" = "We just sent you an email. Check your junk folder if you haven’t."; "Scene.ConfirmEmail.OpenEmailApp.Mail" = "Mail"; @@ -62,4 +62,4 @@ any server."; "Scene.ServerRules.Subtitle" = "These rules are set by the admins of %@."; "Scene.ServerRules.Title" = "Some ground rules."; "Scene.Welcome.Slogan" = "Social networking -back in your hands."; \ No newline at end of file +back in your hands."; diff --git a/Mastodon/Scene/HomeTimeline/HomeTimelineViewController+DebugAction.swift b/Mastodon/Scene/HomeTimeline/HomeTimelineViewController+DebugAction.swift index db6ddfa67..69f0347e0 100644 --- a/Mastodon/Scene/HomeTimeline/HomeTimelineViewController+DebugAction.swift +++ b/Mastodon/Scene/HomeTimeline/HomeTimelineViewController+DebugAction.swift @@ -37,35 +37,5 @@ extension HomeTimelineViewController { coordinator.present(scene: .publicTimeline, from: self, transition: .show) } - @objc private func signOutAction(_ sender: UIAction) { - guard let activeMastodonAuthenticationBox = context.authenticationService.activeMastodonAuthenticationBox.value else { - return - } - let currentAccountCount = context.authenticationService.mastodonAuthentications.value.count - let isAuthenticationExistWhenSignOut = currentAccountCount - 1 > 0 - // prepare advance - let authenticationViewModel = AuthenticationViewModel(context: context, coordinator: coordinator, isAuthenticationExist: isAuthenticationExistWhenSignOut) - - context.authenticationService.signOutMastodonUser( - domain: activeMastodonAuthenticationBox.domain, - userID: activeMastodonAuthenticationBox.userID - ) - .receive(on: DispatchQueue.main) - .sink { [weak self] result in - guard let self = self else { return } - switch result { - case .failure(let error): - assertionFailure(error.localizedDescription) - case .success(let isSignOut): - os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s: sign out %s", ((#file as NSString).lastPathComponent), #line, #function, isSignOut ? "success" : "fail") - guard isSignOut else { return } - if !isAuthenticationExistWhenSignOut { - self.coordinator.present(scene: .authentication(viewModel: authenticationViewModel), from: nil, transition: .modal(animated: true, completion: nil)) - } - } - } - .store(in: &disposeBag) - } - } #endif diff --git a/Mastodon/Scene/HomeTimeline/HomeTimelineViewController.swift b/Mastodon/Scene/HomeTimeline/HomeTimelineViewController.swift index 125ce8545..d3906fd90 100644 --- a/Mastodon/Scene/HomeTimeline/HomeTimelineViewController.swift +++ b/Mastodon/Scene/HomeTimeline/HomeTimelineViewController.swift @@ -70,8 +70,20 @@ extension HomeTimelineViewController { return imageView }() navigationItem.leftBarButtonItem = settingBarButtonItem - settingBarButtonItem.target = self - settingBarButtonItem.action = #selector(HomeTimelineViewController.settingBarButtonItemPressed(_:)) + #if DEBUG + // long press to trigger debug menu + settingBarButtonItem.menu = debugMenu + #else + // settingBarButtonItem.target = self + // settingBarButtonItem.action = #selector(HomeTimelineViewController.settingBarButtonItemPressed(_:)) + settingBarButtonItem.menu = UIMenu(title: "Settings", image: nil, identifier: nil, options: .displayInline, children: [ + UIAction(title: "Sign Out", image: UIImage(systemName: "escape"), attributes: .destructive) { [weak self] action in + guard let self = self else { return } + self.signOutAction(action) + } + ]) + #endif + navigationItem.rightBarButtonItem = composeBarButtonItem composeBarButtonItem.target = self composeBarButtonItem.action = #selector(HomeTimelineViewController.composeBarButtonItemPressed(_:)) @@ -111,11 +123,6 @@ extension HomeTimelineViewController { } } .store(in: &disposeBag) - - #if DEBUG - // long press to trigger debug menu - settingBarButtonItem.menu = debugMenu - #endif } @@ -169,6 +176,30 @@ extension HomeTimelineViewController { } } + @objc func signOutAction(_ sender: UIAction) { + guard let activeMastodonAuthenticationBox = context.authenticationService.activeMastodonAuthenticationBox.value else { + return + } + + context.authenticationService.signOutMastodonUser( + domain: activeMastodonAuthenticationBox.domain, + userID: activeMastodonAuthenticationBox.userID + ) + .receive(on: DispatchQueue.main) + .sink { [weak self] result in + guard let self = self else { return } + switch result { + case .failure(let error): + assertionFailure(error.localizedDescription) + case .success(let isSignOut): + os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s: sign out %s", ((#file as NSString).lastPathComponent), #line, #function, isSignOut ? "success" : "fail") + guard isSignOut else { return } + self.coordinator.setup() + self.coordinator.setupOnboardingIfNeeds(animated: true) + } + } + .store(in: &disposeBag) + } } diff --git a/Mastodon/Scene/Onboarding/AuthenticationViewController.swift b/Mastodon/Scene/Onboarding/AuthenticationViewController.swift deleted file mode 100644 index 090d84234..000000000 --- a/Mastodon/Scene/Onboarding/AuthenticationViewController.swift +++ /dev/null @@ -1,354 +0,0 @@ -// -// AuthenticationViewController.swift -// Mastodon -// -// Created by MainasuK Cirno on 2021/1/29. -// - -import os.log -import UIKit -import Combine -import MastodonSDK -import UITextField_Shake - -final class AuthenticationViewController: UIViewController, NeedsDependency, OnboardingViewControllerAppearance{ - - var disposeBag = Set() - - weak var context: AppContext! { willSet { precondition(!isViewLoaded) } } - weak var coordinator: SceneCoordinator! { willSet { precondition(!isViewLoaded) } } - - var viewModel: AuthenticationViewModel! - - let domainLabel: UILabel = { - let label = UILabel() - label.font = .preferredFont(forTextStyle: .headline) - label.textColor = Asset.Colors.Label.primary.color - label.text = "Domain:" - return label - }() - - let domainTextField: UITextField = { - let textField = UITextField() - textField.placeholder = "example.com" - textField.autocapitalizationType = .none - textField.autocorrectionType = .no - textField.keyboardType = .URL - return textField - }() - - let signInButton: UIButton = { - let button = UIButton(type: .system) - button.titleLabel?.font = .preferredFont(forTextStyle: .headline) - button.setBackgroundImage(UIImage.placeholder(color: Asset.Colors.Background.secondarySystemBackground.color), for: .normal) - button.setBackgroundImage(UIImage.placeholder(color: Asset.Colors.Background.secondarySystemBackground.color.withAlphaComponent(0.8)), for: .disabled) - button.setTitleColor(Asset.Colors.Label.primary.color, for: .normal) - button.setTitle("Sign in", for: .normal) - button.layer.masksToBounds = true - button.layer.cornerRadius = 8 - button.layer.cornerCurve = .continuous - return button - }() - - let signUpButton: UIButton = { - let button = UIButton(type: .system) - button.titleLabel?.font = .preferredFont(forTextStyle: .subheadline) - button.setTitleColor(Asset.Colors.Button.highlight.color, for: .normal) - button.setTitleColor(.systemGray, for: .disabled) - button.setTitle("Sign up", for: .normal) - return button - }() - - let signInActivityIndicatorView: UIActivityIndicatorView = { - let activityIndicatorView = UIActivityIndicatorView(style: .medium) - activityIndicatorView.hidesWhenStopped = true - return activityIndicatorView - }() - - let signUpActivityIndicatorView: UIActivityIndicatorView = { - let activityIndicatorView = UIActivityIndicatorView(style: .medium) - activityIndicatorView.hidesWhenStopped = true - return activityIndicatorView - }() -} - -extension AuthenticationViewController { - - override func viewDidLoad() { - super.viewDidLoad() - - self.setupOnboardingAppearance() - - domainLabel.translatesAutoresizingMaskIntoConstraints = false - view.addSubview(domainLabel) - NSLayoutConstraint.activate([ - domainLabel.topAnchor.constraint(equalTo: view.layoutMarginsGuide.topAnchor, constant: 16), - domainLabel.leadingAnchor.constraint(equalTo: view.readableContentGuide.leadingAnchor), - domainLabel.trailingAnchor.constraint(equalTo: view.readableContentGuide.trailingAnchor), - ]) - - domainTextField.translatesAutoresizingMaskIntoConstraints = false - view.addSubview(domainTextField) - NSLayoutConstraint.activate([ - domainTextField.topAnchor.constraint(equalTo: domainLabel.bottomAnchor, constant: 8), - domainTextField.leadingAnchor.constraint(equalTo: view.readableContentGuide.leadingAnchor), - domainTextField.trailingAnchor.constraint(equalTo: view.readableContentGuide.trailingAnchor), - ]) - - signInButton.translatesAutoresizingMaskIntoConstraints = false - view.addSubview(signInButton) - NSLayoutConstraint.activate([ - signInButton.topAnchor.constraint(equalTo: domainTextField.bottomAnchor, constant: 20), - signInButton.leadingAnchor.constraint(equalTo: view.readableContentGuide.leadingAnchor), - signInButton.trailingAnchor.constraint(equalTo: view.readableContentGuide.trailingAnchor), - signInButton.heightAnchor.constraint(equalToConstant: 44).priority(.defaultHigh), - ]) - - signInActivityIndicatorView.translatesAutoresizingMaskIntoConstraints = false - view.addSubview(signInActivityIndicatorView) - NSLayoutConstraint.activate([ - signInActivityIndicatorView.centerXAnchor.constraint(equalTo: signInButton.centerXAnchor), - signInActivityIndicatorView.centerYAnchor.constraint(equalTo: signInButton.centerYAnchor), - ]) - - signUpButton.translatesAutoresizingMaskIntoConstraints = false - view.addSubview(signUpButton) - NSLayoutConstraint.activate([ - signUpButton.topAnchor.constraint(equalTo: signInButton.bottomAnchor, constant: 8), - signUpButton.leadingAnchor.constraint(equalTo: view.readableContentGuide.leadingAnchor), - signUpButton.trailingAnchor.constraint(equalTo: view.readableContentGuide.trailingAnchor), - signUpButton.heightAnchor.constraint(equalToConstant: 44).priority(.defaultHigh), - ]) - - signUpActivityIndicatorView.translatesAutoresizingMaskIntoConstraints = false - view.addSubview(signUpActivityIndicatorView) - NSLayoutConstraint.activate([ - signUpActivityIndicatorView.centerXAnchor.constraint(equalTo: signUpButton.centerXAnchor), - signUpActivityIndicatorView.centerYAnchor.constraint(equalTo: signUpButton.centerYAnchor), - ]) - - NotificationCenter.default.publisher(for: UITextField.textDidChangeNotification, object: domainTextField) - .compactMap { notification in - guard let textField = notification.object as? UITextField? else { return nil } - return textField?.text ?? "" - } - .assign(to: \.value, on: viewModel.input) - .store(in: &disposeBag) - - viewModel.isAuthenticating - .receive(on: DispatchQueue.main) - .sink { [weak self] isAuthenticating in - guard let self = self else { return } - isAuthenticating ? self.signInActivityIndicatorView.startAnimating() : self.signInActivityIndicatorView.stopAnimating() - self.signInButton.setTitle(isAuthenticating ? "" : "Sign in", for: .normal) - } - .store(in: &disposeBag) - - viewModel.isRegistering - .receive(on: DispatchQueue.main) - .sink { [weak self] isRegistering in - guard let self = self else { return } - isRegistering ? self.signUpActivityIndicatorView.startAnimating() : self.signUpActivityIndicatorView.stopAnimating() - self.signUpButton.setTitle(isRegistering ? "" : "Sign up", for: .normal) - } - .store(in: &disposeBag) - - viewModel.isIdle - .receive(on: DispatchQueue.main) - .sink { [weak self] isIdle in - guard let self = self else { return } - self.signInButton.isEnabled = isIdle - self.signUpButton.isEnabled = isIdle - } - .store(in: &disposeBag) - - - viewModel.authenticated - .receive(on: DispatchQueue.main) - .sink { [weak self] domain, user in - guard let self = self else { return } - // reset view hierarchy only if needs - if self.viewModel.viewHierarchyShouldReset { - self.context.authenticationService.activeMastodonUser(domain: domain, userID: user.id) - .receive(on: DispatchQueue.main) - .sink { [weak self] result in - guard let self = self else { return } - switch result { - case .failure(let error): - assertionFailure(error.localizedDescription) - case .success(let isActived): - assert(isActived) - self.coordinator.setup() - } - } - .store(in: &self.disposeBag) - } else { - self.dismiss(animated: true, completion: nil) - } - } - .store(in: &disposeBag) - - viewModel.error - .compactMap { $0 } - .receive(on: DispatchQueue.main) - .sink { [weak self] error in - guard let self = self else { return } - let alertController = UIAlertController(for: error, title: nil, preferredStyle: .alert) - let okAction = UIAlertAction(title: "OK", style: .default, handler: nil) - alertController.addAction(okAction) - self.coordinator.present( - scene: .alertController(alertController: alertController), - from: nil, - transition: .alertController(animated: true, completion: nil) - ) - } - .store(in: &disposeBag) - - signInButton.addTarget(self, action: #selector(AuthenticationViewController.signInButtonPressed(_:)), for: .touchUpInside) - signUpButton.addTarget(self, action: #selector(AuthenticationViewController.signUpButtonPressed(_:)), for: .touchUpInside) - - } - - override func viewWillAppear(_ animated: Bool) { - super.viewWillAppear(animated) - - domainTextField.becomeFirstResponder() - } - -} - -extension AuthenticationViewController { - - @objc private func signInButtonPressed(_ sender: UIButton) { - os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s", ((#file as NSString).lastPathComponent), #line, #function) - guard viewModel.isDomainValid.value, let domain = viewModel.domain.value else { - domainTextField.shake() - return - } - guard viewModel.isIdle.value else { return } - viewModel.isAuthenticating.value = true - context.apiService.createApplication(domain: domain) - .tryMap { response -> AuthenticationViewModel.AuthenticateInfo in - let application = response.value - guard let info = AuthenticationViewModel.AuthenticateInfo(domain: domain, application: application) else { - throw APIService.APIError.explicit(.badResponse) - } - return info - } - .receive(on: DispatchQueue.main) - .sink { [weak self] completion in - guard let self = self else { return } - // trigger state update - self.viewModel.isAuthenticating.value = false - - switch completion { - case .failure(let error): - os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s: sign in fail: %s", ((#file as NSString).lastPathComponent), #line, #function, error.localizedDescription) - self.viewModel.error.value = error - case .finished: - break - } - } receiveValue: { [weak self] info in - guard let self = self else { return } - let mastodonPinBasedAuthenticationViewModel = MastodonPinBasedAuthenticationViewModel(authenticateURL: info.authorizeURL) - self.viewModel.authenticate( - info: info, - pinCodePublisher: mastodonPinBasedAuthenticationViewModel.pinCodePublisher - ) - self.viewModel.mastodonPinBasedAuthenticationViewController = self.coordinator.present( - scene: .mastodonPinBasedAuthentication(viewModel: mastodonPinBasedAuthenticationViewModel), - from: nil, - transition: .modal(animated: true, completion: nil) - ) - } - .store(in: &disposeBag) - } - - private struct SignUpResponseFirst { - let instance: Mastodon.Response.Content - let application: Mastodon.Response.Content - } - - private struct SignUpResponseSecond { - let instance: Mastodon.Response.Content - let authenticateInfo: AuthenticationViewModel.AuthenticateInfo - } - - private struct SignUpResponseThird { - let instance: Mastodon.Response.Content - let authenticateInfo: AuthenticationViewModel.AuthenticateInfo - let applicationToken: Mastodon.Response.Content - } - - @objc private func signUpButtonPressed(_ sender: UIButton) { - os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s", ((#file as NSString).lastPathComponent), #line, #function) - guard viewModel.isDomainValid.value, let domain = viewModel.domain.value else { - domainTextField.shake() - return - } - guard viewModel.isIdle.value else { return } - viewModel.isRegistering.value = true - - context.apiService.instance(domain: domain) - .compactMap { [weak self] response -> AnyPublisher? in - guard let self = self else { return nil } - guard response.value.registrations != false else { - return Fail(error: AuthenticationViewModel.AuthenticationError.registrationClosed).eraseToAnyPublisher() - } - return self.context.apiService.createApplication(domain: domain) - .map { SignUpResponseFirst(instance: response, application: $0) } - .eraseToAnyPublisher() - } - .switchToLatest() - .tryMap { response -> SignUpResponseSecond in - let application = response.application.value - guard let authenticateInfo = AuthenticationViewModel.AuthenticateInfo(domain: domain, application: application) else { - throw APIService.APIError.explicit(.badResponse) - } - return SignUpResponseSecond(instance: response.instance, authenticateInfo: authenticateInfo) - } - .compactMap { [weak self] response -> AnyPublisher? in - guard let self = self else { return nil } - let instance = response.instance - let authenticateInfo = response.authenticateInfo - return self.context.apiService.applicationAccessToken( - domain: domain, - clientID: authenticateInfo.clientID, - clientSecret: authenticateInfo.clientSecret - ) - .map { SignUpResponseThird(instance: instance, authenticateInfo: authenticateInfo, applicationToken: $0) } - .eraseToAnyPublisher() - } - .switchToLatest() - .receive(on: DispatchQueue.main) - .sink { [weak self] completion in - guard let self = self else { return } - self.viewModel.isRegistering.value = false - - switch completion { - case .failure(let error): - self.viewModel.error.send(error) - case .finished: - break - } - } receiveValue: { [weak self] response in - guard let self = self else { return } - let mastodonRegisterViewModel = MastodonRegisterViewModel( - domain: domain, - authenticateInfo: response.authenticateInfo, - instance: response.instance.value, - applicationToken: response.applicationToken.value - ) - self.coordinator.present(scene: .mastodonRegister(viewModel: mastodonRegisterViewModel), from: self, transition: .show) - } - .store(in: &disposeBag) - } - -} - -// MARK: - UIAdaptivePresentationControllerDelegate -extension AuthenticationViewController: UIAdaptivePresentationControllerDelegate { - func adaptivePresentationStyle(for controller: UIPresentationController, traitCollection: UITraitCollection) -> UIModalPresentationStyle { - return .fullScreen - } -} diff --git a/Mastodon/Scene/Onboarding/ConfirmEmail/MastodonConfirmEmailViewController.swift b/Mastodon/Scene/Onboarding/ConfirmEmail/MastodonConfirmEmailViewController.swift index cf7e9062c..2d69f0dd3 100644 --- a/Mastodon/Scene/Onboarding/ConfirmEmail/MastodonConfirmEmailViewController.swift +++ b/Mastodon/Scene/Onboarding/ConfirmEmail/MastodonConfirmEmailViewController.swift @@ -11,7 +11,8 @@ import os.log import ThirdPartyMailer import UIKit -final class MastodonConfirmEmailViewController: UIViewController, NeedsDependency, OnboardingViewControllerAppearance { +final class MastodonConfirmEmailViewController: UIViewController, NeedsDependency { + var disposeBag = Set() weak var context: AppContext! { willSet { precondition(!isViewLoaded) } } @@ -57,13 +58,18 @@ final class MastodonConfirmEmailViewController: UIViewController, NeedsDependenc button.addTarget(self, action: #selector(dontReceiveButtonPressed(_:)), for: UIControl.Event.touchUpInside) return button }() + + deinit { + os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s", ((#file as NSString).lastPathComponent), #line, #function) + } + } extension MastodonConfirmEmailViewController { override func viewDidLoad() { - self.setupOnboardingAppearance() + setupOnboardingAppearance() // resizedView let resizedView = UIView() @@ -107,19 +113,15 @@ extension MastodonConfirmEmailViewController { case .finished: break } - } receiveValue: { _ in - self.coordinator.setup() + } receiveValue: { response in + os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s: user %s's email confirmed", ((#file as NSString).lastPathComponent), #line, #function, response.value.username) + self.dismiss(animated: true, completion: nil) } .store(in: &self.disposeBag) } .store(in: &self.disposeBag) } - override func viewWillAppear(_ animated: Bool) { - super.viewWillAppear(animated) - navigationController?.setNavigationBarHidden(false, animated: false) - } - } extension MastodonConfirmEmailViewController { @@ -174,3 +176,6 @@ extension MastodonConfirmEmailViewController { self.coordinator.present(scene: .alertController(alertController: alertController), from: self, transition: .alertController(animated: true, completion: nil)) } } + +// MARK: - OnboardingViewControllerAppearance +extension MastodonConfirmEmailViewController: OnboardingViewControllerAppearance { } diff --git a/Mastodon/Scene/Onboarding/PickServer/CollectionViewCell/PickServerCategoryCollectionViewCell.swift b/Mastodon/Scene/Onboarding/PickServer/CollectionViewCell/PickServerCategoryCollectionViewCell.swift index 587ffbae4..1f02baad6 100644 --- a/Mastodon/Scene/Onboarding/PickServer/CollectionViewCell/PickServerCategoryCollectionViewCell.swift +++ b/Mastodon/Scene/Onboarding/PickServer/CollectionViewCell/PickServerCategoryCollectionViewCell.swift @@ -9,7 +9,7 @@ import UIKit class PickServerCategoryCollectionViewCell: UICollectionViewCell { - var category: PickServerViewModel.Category? { + var category: MastodonPickServerViewModel.Category? { didSet { categoryView.category = category } diff --git a/Mastodon/Scene/Onboarding/PickServer/PickServerViewController.swift b/Mastodon/Scene/Onboarding/PickServer/MastodonPickServerViewController.swift similarity index 88% rename from Mastodon/Scene/Onboarding/PickServer/PickServerViewController.swift rename to Mastodon/Scene/Onboarding/PickServer/MastodonPickServerViewController.swift index 1e5027025..9e10cd329 100644 --- a/Mastodon/Scene/Onboarding/PickServer/PickServerViewController.swift +++ b/Mastodon/Scene/Onboarding/PickServer/MastodonPickServerViewController.swift @@ -1,5 +1,5 @@ // -// PickServerViewController.swift +// MastodonPickServerViewController.swift // Mastodon // // Created by BradGao on 2021/2/20. @@ -10,14 +10,14 @@ import Combine import OSLog import MastodonSDK -final class PickServerViewController: UIViewController, NeedsDependency { +final class MastodonPickServerViewController: UIViewController, NeedsDependency { private var disposeBag = Set() weak var context: AppContext! { willSet { precondition(!isViewLoaded) } } weak var coordinator: SceneCoordinator! { willSet { precondition(!isViewLoaded) } } - var viewModel: PickServerViewModel! + var viewModel: MastodonPickServerViewModel! private var isAuthenticating = CurrentValueSubject(false) @@ -58,7 +58,7 @@ final class PickServerViewController: UIViewController, NeedsDependency { } -extension PickServerViewController { +extension MastodonPickServerViewController { override var preferredStatusBarStyle: UIStatusBarStyle { return .darkContent @@ -72,9 +72,9 @@ extension PickServerViewController { view.addSubview(nextStepButton) NSLayoutConstraint.activate([ - nextStepButton.leadingAnchor.constraint(equalTo: view.readableContentGuide.leadingAnchor, constant: 12), - view.readableContentGuide.trailingAnchor.constraint(equalTo: nextStepButton.trailingAnchor, constant: 12), - nextStepButton.heightAnchor.constraint(equalToConstant: PickServerViewController.actionButtonHeight).priority(.defaultHigh), + nextStepButton.leadingAnchor.constraint(equalTo: view.readableContentGuide.leadingAnchor, constant: MastodonPickServerViewController.actionButtonMargin), + view.readableContentGuide.trailingAnchor.constraint(equalTo: nextStepButton.trailingAnchor, constant: MastodonPickServerViewController.actionButtonMargin), + nextStepButton.heightAnchor.constraint(equalToConstant: MastodonPickServerViewController.actionButtonHeight).priority(.defaultHigh), view.layoutMarginsGuide.bottomAnchor.constraint(equalTo: nextStepButton.bottomAnchor, constant: WelcomeViewController.viewBottomPaddingHeight), ]) @@ -139,11 +139,11 @@ extension PickServerViewController { viewModel .authenticated - .receive(on: DispatchQueue.main) .flatMap { [weak self] (domain, user) -> AnyPublisher, Never> in guard let self = self else { return Just(.success(false)).eraseToAnyPublisher() } return self.context.authenticationService.activeMastodonUser(domain: domain, userID: user.id) } + .receive(on: DispatchQueue.main) .sink { [weak self] result in guard let self = self else { return } switch result { @@ -151,7 +151,7 @@ extension PickServerViewController { assertionFailure(error.localizedDescription) case .success(let isActived): assert(isActived) - self.coordinator.setup() + self.dismiss(animated: true, completion: nil) } } .store(in: &disposeBag) @@ -181,9 +181,9 @@ extension PickServerViewController { guard let server = viewModel.selectedServer.value else { return } isAuthenticating.send(true) context.apiService.createApplication(domain: server.domain) - .tryMap { response -> PickServerViewModel.AuthenticateInfo in + .tryMap { response -> MastodonPickServerViewModel.AuthenticateInfo in let application = response.value - guard let info = PickServerViewModel.AuthenticateInfo(domain: server.domain, application: application) else { + guard let info = MastodonPickServerViewModel.AuthenticateInfo(domain: server.domain, application: application) else { throw APIService.APIError.explicit(.badResponse) } return info @@ -222,24 +222,24 @@ extension PickServerViewController { isAuthenticating.send(true) context.apiService.instance(domain: server.domain) - .compactMap { [weak self] response -> AnyPublisher? in + .compactMap { [weak self] response -> AnyPublisher? in guard let self = self else { return nil } guard response.value.registrations != false else { return Fail(error: AuthenticationViewModel.AuthenticationError.registrationClosed).eraseToAnyPublisher() } return self.context.apiService.createApplication(domain: server.domain) - .map { PickServerViewModel.SignUpResponseFirst(instance: response, application: $0) } + .map { MastodonPickServerViewModel.SignUpResponseFirst(instance: response, application: $0) } .eraseToAnyPublisher() } .switchToLatest() - .tryMap { response -> PickServerViewModel.SignUpResponseSecond in + .tryMap { response -> MastodonPickServerViewModel.SignUpResponseSecond in let application = response.application.value guard let authenticateInfo = AuthenticationViewModel.AuthenticateInfo(domain: server.domain, application: application) else { throw APIService.APIError.explicit(.badResponse) } - return PickServerViewModel.SignUpResponseSecond(instance: response.instance, authenticateInfo: authenticateInfo) + return MastodonPickServerViewModel.SignUpResponseSecond(instance: response.instance, authenticateInfo: authenticateInfo) } - .compactMap { [weak self] response -> AnyPublisher? in + .compactMap { [weak self] response -> AnyPublisher? in guard let self = self else { return nil } let instance = response.instance let authenticateInfo = response.authenticateInfo @@ -248,7 +248,7 @@ extension PickServerViewController { clientID: authenticateInfo.clientID, clientSecret: authenticateInfo.clientSecret ) - .map { PickServerViewModel.SignUpResponseThird(instance: instance, authenticateInfo: authenticateInfo, applicationToken: $0) } + .map { MastodonPickServerViewModel.SignUpResponseThird(instance: instance, authenticateInfo: authenticateInfo, applicationToken: $0) } .eraseToAnyPublisher() } .switchToLatest() @@ -277,7 +277,7 @@ extension PickServerViewController { } } -extension PickServerViewController: UITableViewDelegate { +extension MastodonPickServerViewController: UITableViewDelegate { func tableView(_ tableView: UITableView, heightForHeaderInSection section: Int) -> CGFloat { let category = Section.allCases[section] switch category { @@ -316,7 +316,7 @@ extension PickServerViewController: UITableViewDelegate { } } -extension PickServerViewController: UITableViewDataSource { +extension MastodonPickServerViewController: UITableViewDataSource { func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? { return UIView() } @@ -374,7 +374,7 @@ extension PickServerViewController: UITableViewDataSource { } } -extension PickServerViewController: PickServerCellDelegate { +extension MastodonPickServerViewController: PickServerCellDelegate { func pickServerCell(modeChange server: Mastodon.Entity.Server, newMode: PickServerCell.Mode, updates: (() -> Void)) { if newMode == .collapse { expandServerDomainSet.remove(server.domain) @@ -392,18 +392,18 @@ extension PickServerViewController: PickServerCellDelegate { } } -extension PickServerViewController: PickServerSearchCellDelegate { +extension MastodonPickServerViewController: PickServerSearchCellDelegate { func pickServerSearchCell(didChange searchText: String?) { viewModel.searchText.send(searchText) } } -extension PickServerViewController: PickServerCategoriesDataSource, PickServerCategoriesDelegate { +extension MastodonPickServerViewController: PickServerCategoriesDataSource, PickServerCategoriesDelegate { func numberOfCategories() -> Int { return viewModel.categories.count } - func category(at index: Int) -> PickServerViewModel.Category { + func category(at index: Int) -> MastodonPickServerViewModel.Category { return viewModel.categories[index] } @@ -417,4 +417,4 @@ extension PickServerViewController: PickServerCategoriesDataSource, PickServerCa } // MARK: - OnboardingViewControllerAppearance -extension PickServerViewController: OnboardingViewControllerAppearance { } +extension MastodonPickServerViewController: OnboardingViewControllerAppearance { } diff --git a/Mastodon/Scene/Onboarding/PickServer/PickServerViewModel.swift b/Mastodon/Scene/Onboarding/PickServer/MastodonPickServerViewModel.swift similarity index 98% rename from Mastodon/Scene/Onboarding/PickServer/PickServerViewModel.swift rename to Mastodon/Scene/Onboarding/PickServer/MastodonPickServerViewModel.swift index b6bc07984..3a701f09f 100644 --- a/Mastodon/Scene/Onboarding/PickServer/PickServerViewModel.swift +++ b/Mastodon/Scene/Onboarding/PickServer/MastodonPickServerViewModel.swift @@ -1,5 +1,5 @@ // -// PickServerViewModel.swift +// MastodonPickServerViewModel.swift // Mastodon // // Created by BradGao on 2021/2/23. @@ -11,7 +11,7 @@ import Combine import MastodonSDK import CoreDataStack -class PickServerViewModel: NSObject { +class MastodonPickServerViewModel: NSObject { enum PickServerMode { case signUp case signIn @@ -173,7 +173,7 @@ class PickServerViewModel: NSObject { } // MARK: - SignIn methods & structs -extension PickServerViewModel { +extension MastodonPickServerViewModel { enum AuthenticationError: Error, LocalizedError { case badCredentials case registrationClosed @@ -322,7 +322,7 @@ extension PickServerViewModel { } // MARK: - SignUp methods & structs -extension PickServerViewModel { +extension MastodonPickServerViewModel { struct SignUpResponseFirst { let instance: Mastodon.Response.Content let application: Mastodon.Response.Content diff --git a/Mastodon/Scene/Onboarding/PickServer/TableViewCell/PickServerCategoriesCell.swift b/Mastodon/Scene/Onboarding/PickServer/TableViewCell/PickServerCategoriesCell.swift index 1668fc7fb..8f66e9847 100644 --- a/Mastodon/Scene/Onboarding/PickServer/TableViewCell/PickServerCategoriesCell.swift +++ b/Mastodon/Scene/Onboarding/PickServer/TableViewCell/PickServerCategoriesCell.swift @@ -10,7 +10,7 @@ import MastodonSDK protocol PickServerCategoriesDataSource: class { func numberOfCategories() -> Int - func category(at index: Int) -> PickServerViewModel.Category + func category(at index: Int) -> MastodonPickServerViewModel.Category func selectedIndex() -> Int } diff --git a/Mastodon/Scene/Onboarding/PickServer/View/PickServerCategoryView.swift b/Mastodon/Scene/Onboarding/PickServer/View/PickServerCategoryView.swift index a4a4f0ef6..30fcbc1f9 100644 --- a/Mastodon/Scene/Onboarding/PickServer/View/PickServerCategoryView.swift +++ b/Mastodon/Scene/Onboarding/PickServer/View/PickServerCategoryView.swift @@ -9,7 +9,7 @@ import UIKit import MastodonSDK class PickServerCategoryView: UIView { - var category: PickServerViewModel.Category? { + var category: MastodonPickServerViewModel.Category? { didSet { updateCategory() } diff --git a/Mastodon/Scene/Onboarding/PinBased/MastodonPinBasedAuthenticationViewController.swift b/Mastodon/Scene/Onboarding/PinBasedAuthentication/MastodonPinBasedAuthenticationViewController.swift similarity index 100% rename from Mastodon/Scene/Onboarding/PinBased/MastodonPinBasedAuthenticationViewController.swift rename to Mastodon/Scene/Onboarding/PinBasedAuthentication/MastodonPinBasedAuthenticationViewController.swift diff --git a/Mastodon/Scene/Onboarding/PinBased/MastodonPinBasedAuthenticationViewModel.swift b/Mastodon/Scene/Onboarding/PinBasedAuthentication/MastodonPinBasedAuthenticationViewModel.swift similarity index 100% rename from Mastodon/Scene/Onboarding/PinBased/MastodonPinBasedAuthenticationViewModel.swift rename to Mastodon/Scene/Onboarding/PinBasedAuthentication/MastodonPinBasedAuthenticationViewModel.swift diff --git a/Mastodon/Scene/Onboarding/PinBased/MastodonPinBasedAuthenticationViewModelNavigationDelegateShim.swift b/Mastodon/Scene/Onboarding/PinBasedAuthentication/MastodonPinBasedAuthenticationViewModelNavigationDelegateShim.swift similarity index 100% rename from Mastodon/Scene/Onboarding/PinBased/MastodonPinBasedAuthenticationViewModelNavigationDelegateShim.swift rename to Mastodon/Scene/Onboarding/PinBasedAuthentication/MastodonPinBasedAuthenticationViewModelNavigationDelegateShim.swift diff --git a/Mastodon/Scene/Onboarding/Register/MastodonRegisterViewController.swift b/Mastodon/Scene/Onboarding/Register/MastodonRegisterViewController.swift index 266de70b7..ff979c3dd 100644 --- a/Mastodon/Scene/Onboarding/Register/MastodonRegisterViewController.swift +++ b/Mastodon/Scene/Onboarding/Register/MastodonRegisterViewController.swift @@ -92,9 +92,9 @@ final class MastodonRegisterViewController: UIViewController, NeedsDependency, O textField.autocapitalizationType = .none textField.autocorrectionType = .no textField.backgroundColor = .white - textField.textColor = Asset.Colors.Label.secondary.color + textField.textColor = Asset.Colors.Label.primary.color textField.attributedPlaceholder = NSAttributedString(string: L10n.Scene.Register.Input.Username.placeholder, - attributes: [NSAttributedString.Key.foregroundColor: Asset.Colors.lightSecondaryText.color, + attributes: [NSAttributedString.Key.foregroundColor: Asset.Colors.Label.secondary.color, NSAttributedString.Key.font: UIFont.preferredFont(forTextStyle: .headline)]) textField.borderStyle = UITextField.BorderStyle.roundedRect let paddingView = UIView(frame: CGRect(x: 0, y: 0, width: 5, height: textField.frame.height)) @@ -113,9 +113,9 @@ final class MastodonRegisterViewController: UIViewController, NeedsDependency, O textField.autocapitalizationType = .none textField.autocorrectionType = .no textField.backgroundColor = .white - textField.textColor = Asset.Colors.Label.secondary.color + textField.textColor = Asset.Colors.Label.primary.color textField.attributedPlaceholder = NSAttributedString(string: L10n.Scene.Register.Input.DisplayName.placeholder, - attributes: [NSAttributedString.Key.foregroundColor: Asset.Colors.lightSecondaryText.color, + attributes: [NSAttributedString.Key.foregroundColor: Asset.Colors.Label.secondary.color, NSAttributedString.Key.font: UIFont.preferredFont(forTextStyle: .headline)]) textField.borderStyle = UITextField.BorderStyle.roundedRect let paddingView = UIView(frame: CGRect(x: 0, y: 0, width: 5, height: textField.frame.height)) @@ -130,9 +130,9 @@ final class MastodonRegisterViewController: UIViewController, NeedsDependency, O textField.autocorrectionType = .no textField.keyboardType = .emailAddress textField.backgroundColor = .white - textField.textColor = Asset.Colors.Label.secondary.color + textField.textColor = Asset.Colors.Label.primary.color textField.attributedPlaceholder = NSAttributedString(string: L10n.Scene.Register.Input.Email.placeholder, - attributes: [NSAttributedString.Key.foregroundColor: Asset.Colors.lightSecondaryText.color, + attributes: [NSAttributedString.Key.foregroundColor: Asset.Colors.Label.secondary.color, NSAttributedString.Key.font: UIFont.preferredFont(forTextStyle: .headline)]) textField.borderStyle = UITextField.BorderStyle.roundedRect let paddingView = UIView(frame: CGRect(x: 0, y: 0, width: 5, height: textField.frame.height)) @@ -154,9 +154,9 @@ final class MastodonRegisterViewController: UIViewController, NeedsDependency, O textField.keyboardType = .asciiCapable textField.isSecureTextEntry = true textField.backgroundColor = .white - textField.textColor = Asset.Colors.Label.secondary.color + textField.textColor = Asset.Colors.Label.primary.color textField.attributedPlaceholder = NSAttributedString(string: L10n.Scene.Register.Input.Password.placeholder, - attributes: [NSAttributedString.Key.foregroundColor: Asset.Colors.lightSecondaryText.color, + attributes: [NSAttributedString.Key.foregroundColor: Asset.Colors.Label.secondary.color, NSAttributedString.Key.font: UIFont.preferredFont(forTextStyle: .headline)]) textField.borderStyle = UITextField.BorderStyle.roundedRect let paddingView = UIView(frame: CGRect(x: 0, y: 0, width: 5, height: textField.frame.height)) @@ -170,9 +170,9 @@ final class MastodonRegisterViewController: UIViewController, NeedsDependency, O textField.autocapitalizationType = .none textField.autocorrectionType = .no textField.backgroundColor = .white - textField.textColor = Asset.Colors.Label.secondary.color + textField.textColor = Asset.Colors.Label.primary.color textField.attributedPlaceholder = NSAttributedString(string: L10n.Scene.Register.Input.Invite.registrationUserInviteRequest, - attributes: [NSAttributedString.Key.foregroundColor: Asset.Colors.lightSecondaryText.color, + attributes: [NSAttributedString.Key.foregroundColor: Asset.Colors.Label.secondary.color, NSAttributedString.Key.font: UIFont.preferredFont(forTextStyle: .headline)]) textField.borderStyle = UITextField.BorderStyle.roundedRect let paddingView = UIView(frame: CGRect(x: 0, y: 0, width: 5, height: textField.frame.height)) @@ -181,18 +181,18 @@ final class MastodonRegisterViewController: UIViewController, NeedsDependency, O return textField }() - let signUpButton: UIButton = { + let buttonContainer = UIView() + let signUpButton: PrimaryActionButton = { let button = PrimaryActionButton() button.isEnabled = false button.setTitle(L10n.Common.Controls.Actions.continue, for: .normal) return button }() - let signUpActivityIndicatorView: UIActivityIndicatorView = { - let activityIndicatorView = UIActivityIndicatorView(style: .medium) - activityIndicatorView.hidesWhenStopped = true - return activityIndicatorView - }() + deinit { + os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s", ((#file as NSString).lastPathComponent), #line, #function) + } + } extension MastodonRegisterViewController { @@ -294,19 +294,17 @@ extension MastodonRegisterViewController { stackView.setCustomSpacing(32, after: passwordCheckLabel) // button + stackView.addArrangedSubview(buttonContainer) signUpButton.translatesAutoresizingMaskIntoConstraints = false - stackView.addArrangedSubview(signUpButton) + buttonContainer.addSubview(signUpButton) NSLayoutConstraint.activate([ + signUpButton.topAnchor.constraint(equalTo: buttonContainer.topAnchor), + signUpButton.leadingAnchor.constraint(equalTo: buttonContainer.leadingAnchor, constant: MastodonRegisterViewController.actionButtonMargin), + buttonContainer.trailingAnchor.constraint(equalTo: signUpButton.trailingAnchor, constant: MastodonRegisterViewController.actionButtonMargin), + buttonContainer.bottomAnchor.constraint(equalTo: signUpButton.bottomAnchor), signUpButton.heightAnchor.constraint(equalToConstant: MastodonRegisterViewController.actionButtonHeight).priority(.defaultHigh), ]) - - signUpActivityIndicatorView.translatesAutoresizingMaskIntoConstraints = false - scrollView.addSubview(signUpActivityIndicatorView) - NSLayoutConstraint.activate([ - signUpActivityIndicatorView.centerXAnchor.constraint(equalTo: signUpButton.centerXAnchor), - signUpActivityIndicatorView.centerYAnchor.constraint(equalTo: signUpButton.centerYAnchor), - ]) - + Publishers.CombineLatest( KeyboardResponderService.shared.state.eraseToAnyPublisher(), KeyboardResponderService.shared.willEndFrame.eraseToAnyPublisher() @@ -332,7 +330,7 @@ extension MastodonRegisterViewController { self.scrollView.verticalScrollIndicatorInsets.bottom = padding + 16 if self.passwordTextField.isFirstResponder { - let contentFrame = self.scrollView.convert(self.signUpButton.frame, to: nil) + let contentFrame = self.buttonContainer.convert(self.signUpButton.frame, to: nil) let labelPadding = contentFrame.maxY - endFrame.minY let contentOffsetY = self.scrollView.contentOffset.y DispatchQueue.main.async { @@ -346,9 +344,7 @@ extension MastodonRegisterViewController { .receive(on: DispatchQueue.main) .sink { [weak self] isRegistering in guard let self = self else { return } - isRegistering ? self.signUpActivityIndicatorView.startAnimating() : self.signUpActivityIndicatorView.stopAnimating() - self.signUpButton.setTitle(isRegistering ? "" : L10n.Common.Controls.Actions.continue, for: .normal) - self.signUpButton.isEnabled = !isRegistering + isRegistering ? self.signUpButton.showLoading() : self.signUpButton.stopLoading() } .store(in: &disposeBag) @@ -463,7 +459,7 @@ extension MastodonRegisterViewController { .receive(on: DispatchQueue.main) .sink { [weak self] _ in guard let self = self else { return } - self.viewModel.invite.value = self.inviteTextField.text?.trimmingCharacters(in: .whitespacesAndNewlines) ?? "" + self.viewModel.reason.value = self.inviteTextField.text?.trimmingCharacters(in: .whitespacesAndNewlines) ?? "" } .store(in: &disposeBag) } @@ -471,11 +467,6 @@ extension MastodonRegisterViewController { signUpButton.addTarget(self, action: #selector(MastodonRegisterViewController.signUpButtonPressed(_:)), for: .touchUpInside) } - override func viewWillAppear(_ animated: Bool) { - super.viewWillAppear(animated) - - navigationController?.setNavigationBarHidden(false, animated: false) - } } extension MastodonRegisterViewController: UITextFieldDelegate { @@ -493,7 +484,7 @@ extension MastodonRegisterViewController: UITextFieldDelegate { case passwordTextField: viewModel.password.value = text case inviteTextField: - viewModel.invite.value = text + viewModel.reason.value = text default: break } @@ -536,13 +527,24 @@ extension MastodonRegisterViewController { let username = viewModel.username.value let email = viewModel.email.value let password = viewModel.password.value + let query = Mastodon.API.Account.RegisterQuery( + reason: viewModel.reason.value, + username: username, + email: email, + password: password, + agreement: true, // TODO: + locale: "en" // TODO: + ) if let rules = viewModel.instance.rules, !rules.isEmpty { // show server rules before register let mastodonServerRulesViewModel = MastodonServerRulesViewModel( context: context, domain: viewModel.domain, - rules: rules + authenticateInfo: viewModel.authenticateInfo, + rules: rules, + registerQuery: query, + applicationAuthorization: viewModel.applicationAuthorization ) viewModel.isRegistering.value = false @@ -551,51 +553,28 @@ extension MastodonRegisterViewController { return } else { // register without show server rules + context.apiService.accountRegister( + domain: viewModel.domain, + query: query, + authorization: viewModel.applicationAuthorization + ) + .receive(on: DispatchQueue.main) + .sink { [weak self] completion in + guard let self = self else { return } + self.viewModel.isRegistering.value = false + switch completion { + case .failure(let error): + self.viewModel.error.send(error) + case .finished: + break + } + } receiveValue: { [weak self] response in + guard let self = self else { return } + let userToken = response.value + let viewModel = MastodonConfirmEmailViewModel(context: self.context, email: email, authenticateInfo: self.viewModel.authenticateInfo, userToken: userToken) + self.coordinator.present(scene: .mastodonConfirmEmail(viewModel: viewModel), from: self, transition: .show) + } + .store(in: &disposeBag) } - - let query = Mastodon.API.Account.RegisterQuery( - reason: viewModel.invite.value, - username: username, - email: email, - password: password, - agreement: true, // TODO: - locale: "en" // TODO: - ) - - } } - -extension MastodonRegisterViewController { -// func register(query: Mastodon.API.Account.RegisterQuery) -> AnyPublisher, Error> { -// context.apiService.accountRegister( -// domain: viewModel.domain, -// query: query, -// authorization: viewModel.applicationAuthorization -// ) -// .receive(on: DispatchQueue.main) -// .sink { [weak self] completion in -// guard let self = self else { return } -// self.viewModel.isRegistering.value = false -// switch completion { -// case .failure(let error): -// self.viewModel.error.send(error) -// case .finished: -// break -// } -// } receiveValue: { [weak self] response in -// guard let self = self else { return } -// let userToken = response.value -// -// let alertController = UIAlertController(title: L10n.Scene.Register.success, message: L10n.Scene.Register.checkEmail, preferredStyle: .alert) -// let okAction = UIAlertAction(title: L10n.Common.Controls.Actions.ok, style: .default) { [weak self] _ in -// guard let self = self else { return } -// let viewModel = MastodonConfirmEmailViewModel(context: self.context, email: email, authenticateInfo: self.viewModel.authenticateInfo, userToken: userToken) -// self.coordinator.present(scene: .mastodonConfirmEmail(viewModel: viewModel), from: self, transition: .show) -// } -// alertController.addAction(okAction) -// self.coordinator.present(scene: .alertController(alertController: alertController), from: self, transition: .alertController(animated: true, completion: nil)) -// } -// .store(in: &disposeBag) -// } -} diff --git a/Mastodon/Scene/Onboarding/Register/MastodonRegisterViewModel.swift b/Mastodon/Scene/Onboarding/Register/MastodonRegisterViewModel.swift index 9afa531f8..5a9098347 100644 --- a/Mastodon/Scene/Onboarding/Register/MastodonRegisterViewModel.swift +++ b/Mastodon/Scene/Onboarding/Register/MastodonRegisterViewModel.swift @@ -23,33 +23,19 @@ final class MastodonRegisterViewModel { let displayName = CurrentValueSubject("") let email = CurrentValueSubject("") let password = CurrentValueSubject("") - let invite = CurrentValueSubject("") - - let isUsernameValidateDalay = CurrentValueSubject(true) - let isDisplayNameValidateDalay = CurrentValueSubject(true) - let isEmailValidateDalay = CurrentValueSubject(true) - let isPasswordValidateDalay = CurrentValueSubject(true) - let isInviteValidateDelay = CurrentValueSubject(true) - let isRegistering = CurrentValueSubject(false) + let reason = CurrentValueSubject("") // output - lazy var approvalRequired: Bool = { - if let approvalRequired = instance.approvalRequired { - return approvalRequired - } - return false - }() - + let approvalRequired: Bool let applicationAuthorization: Mastodon.API.OAuth.Authorization - let usernameValidateState = CurrentValueSubject(.empty) let displayNameValidateState = CurrentValueSubject(.empty) let emailValidateState = CurrentValueSubject(.empty) let passwordValidateState = CurrentValueSubject(.empty) let inviteValidateState = CurrentValueSubject(.empty) + let isRegistering = CurrentValueSubject(false) let isAllValid = CurrentValueSubject(false) - let error = CurrentValueSubject(nil) init( @@ -62,6 +48,7 @@ final class MastodonRegisterViewModel { self.authenticateInfo = authenticateInfo self.instance = instance self.applicationToken = applicationToken + self.approvalRequired = instance.approvalRequired ?? false self.applicationAuthorization = Mastodon.API.OAuth.Authorization(accessToken: applicationToken.accessToken) username @@ -107,7 +94,7 @@ final class MastodonRegisterViewModel { .assign(to: \.value, on: passwordValidateState) .store(in: &disposeBag) if approvalRequired { - invite + reason .map { invite in guard !invite.isEmpty else { return .empty } return .valid diff --git a/Mastodon/Scene/Onboarding/ResendEmail/MastodonResendEmailViewController.swift b/Mastodon/Scene/Onboarding/ResendEmail/MastodonResendEmailViewController.swift index a10755fe3..25b9ca402 100644 --- a/Mastodon/Scene/Onboarding/ResendEmail/MastodonResendEmailViewController.swift +++ b/Mastodon/Scene/Onboarding/ResendEmail/MastodonResendEmailViewController.swift @@ -10,7 +10,8 @@ import os.log import UIKit import WebKit -final class MastodonResendEmailViewController: UIViewController, NeedsDependency, WKNavigationDelegate { +final class MastodonResendEmailViewController: UIViewController, NeedsDependency { + weak var context: AppContext! { willSet { precondition(!isViewLoaded) } } weak var coordinator: SceneCoordinator! { willSet { precondition(!isViewLoaded) } } @@ -35,6 +36,7 @@ final class MastodonResendEmailViewController: UIViewController, NeedsDependency } } } + } extension MastodonResendEmailViewController { diff --git a/Mastodon/Scene/Onboarding/ResendEmail/MastodonResendEmailViewModel.swift b/Mastodon/Scene/Onboarding/ResendEmail/MastodonResendEmailViewModel.swift index e8e16e6e5..dc907cc6c 100644 --- a/Mastodon/Scene/Onboarding/ResendEmail/MastodonResendEmailViewModel.swift +++ b/Mastodon/Scene/Onboarding/ResendEmail/MastodonResendEmailViewModel.swift @@ -11,6 +11,7 @@ import os.log import WebKit final class MastodonResendEmailViewModel { + // input let resendEmailURL: URL let email: String @@ -25,6 +26,7 @@ final class MastodonResendEmailViewModel { deinit { os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s", (#file as NSString).lastPathComponent, #line, #function) } + } extension MastodonResendEmailViewModel { diff --git a/Mastodon/Scene/Onboarding/ServerRules/MastodonServerRulesViewController.swift b/Mastodon/Scene/Onboarding/ServerRules/MastodonServerRulesViewController.swift index 1e2556f9b..6f2554ce1 100644 --- a/Mastodon/Scene/Onboarding/ServerRules/MastodonServerRulesViewController.swift +++ b/Mastodon/Scene/Onboarding/ServerRules/MastodonServerRulesViewController.swift @@ -7,9 +7,12 @@ import os.log import UIKit +import Combine final class MastodonServerRulesViewController: UIViewController, NeedsDependency { + var disposeBag = Set() + weak var context: AppContext! { willSet { precondition(!isViewLoaded) } } weak var coordinator: SceneCoordinator! { willSet { precondition(!isViewLoaded) } } @@ -56,7 +59,7 @@ final class MastodonServerRulesViewController: UIViewController, NeedsDependency return label }() - let confirmButton: UIButton = { + let confirmButton: PrimaryActionButton = { let button = PrimaryActionButton() button.titleLabel?.font = .preferredFont(forTextStyle: .headline) button.setTitleColor(.white, for: .normal) @@ -69,6 +72,10 @@ final class MastodonServerRulesViewController: UIViewController, NeedsDependency scrollView.alwaysBounceVertical = true return scrollView }() + + deinit { + os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s", ((#file as NSString).lastPathComponent), #line, #function) + } } @@ -109,7 +116,7 @@ extension MastodonServerRulesViewController { scrollView.translatesAutoresizingMaskIntoConstraints = false view.addSubview(scrollView) NSLayoutConstraint.activate([ - scrollView.frameLayoutGuide.topAnchor.constraint(equalTo: view.topAnchor), + scrollView.frameLayoutGuide.topAnchor.constraint(equalTo: view.layoutMarginsGuide.topAnchor), scrollView.frameLayoutGuide.leadingAnchor.constraint(equalTo: view.readableContentGuide.leadingAnchor), view.readableContentGuide.trailingAnchor.constraint(equalTo: scrollView.frameLayoutGuide.trailingAnchor), scrollView.frameLayoutGuide.widthAnchor.constraint(equalTo: scrollView.contentLayoutGuide.widthAnchor), @@ -136,6 +143,14 @@ extension MastodonServerRulesViewController { rulesLabel.attributedText = viewModel.rulesAttributedString confirmButton.addTarget(self, action: #selector(MastodonServerRulesViewController.confirmButtonPressed(_:)), for: .touchUpInside) + + viewModel.isRegistering + .receive(on: DispatchQueue.main) + .sink { [weak self] isRegistering in + guard let self = self else { return } + isRegistering ? self.confirmButton.showLoading() : self.confirmButton.stopLoading() + } + .store(in: &disposeBag) } } @@ -143,7 +158,31 @@ extension MastodonServerRulesViewController { extension MastodonServerRulesViewController { @objc private func confirmButtonPressed(_ sender: UIButton) { os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s", ((#file as NSString).lastPathComponent), #line, #function) - + + let email = viewModel.registerQuery.email + + context.apiService.accountRegister( + domain: viewModel.domain, + query: viewModel.registerQuery, + authorization: viewModel.applicationAuthorization + ) + .receive(on: DispatchQueue.main) + .sink { [weak self] completion in + guard let self = self else { return } + self.viewModel.isRegistering.value = false + switch completion { + case .failure(let error): + self.viewModel.error.send(error) + case .finished: + break + } + } receiveValue: { [weak self] response in + guard let self = self else { return } + let userToken = response.value + let viewModel = MastodonConfirmEmailViewModel(context: self.context, email: email, authenticateInfo: self.viewModel.authenticateInfo, userToken: userToken) + self.coordinator.present(scene: .mastodonConfirmEmail(viewModel: viewModel), from: self, transition: .show) + } + .store(in: &disposeBag) } } diff --git a/Mastodon/Scene/Onboarding/ServerRules/MastodonServerRulesViewModel.swift b/Mastodon/Scene/Onboarding/ServerRules/MastodonServerRulesViewModel.swift index 0c43392f5..9569ffe81 100644 --- a/Mastodon/Scene/Onboarding/ServerRules/MastodonServerRulesViewModel.swift +++ b/Mastodon/Scene/Onboarding/ServerRules/MastodonServerRulesViewModel.swift @@ -6,6 +6,7 @@ // import UIKit +import Combine import MastodonSDK final class MastodonServerRulesViewModel { @@ -13,12 +14,30 @@ final class MastodonServerRulesViewModel { // input let context: AppContext let domain: String + let authenticateInfo: AuthenticationViewModel.AuthenticateInfo let rules: [Mastodon.Entity.Instance.Rule] + let registerQuery: Mastodon.API.Account.RegisterQuery + let applicationAuthorization: Mastodon.API.OAuth.Authorization + + // output + let isRegistering = CurrentValueSubject(false) + let error = CurrentValueSubject(nil) + - init(context: AppContext, domain: String, rules: [Mastodon.Entity.Instance.Rule]) { + init( + context: AppContext, + domain: String, + authenticateInfo: AuthenticationViewModel.AuthenticateInfo, + rules: [Mastodon.Entity.Instance.Rule], + registerQuery: Mastodon.API.Account.RegisterQuery, + applicationAuthorization: Mastodon.API.OAuth.Authorization + ) { self.context = context self.domain = domain + self.authenticateInfo = authenticateInfo self.rules = rules + self.registerQuery = registerQuery + self.applicationAuthorization = applicationAuthorization } var rulesAttributedString: NSAttributedString { @@ -32,9 +51,6 @@ final class MastodonServerRulesViewModel { attributedString.append(indexString) attributedString.append(ruleString) } - // let paragraphStyle = NSMutableParagraphStyle() - // paragraphStyle.lineSpacing = 20 - // attributedString.addAttribute(NSAttributedString.Key.paragraphStyle, value: paragraphStyle, range: NSRange(location: 0, length: attributedString.length)) return attributedString } diff --git a/Mastodon/Scene/Onboarding/AuthenticationViewModel.swift b/Mastodon/Scene/Onboarding/Share/AuthenticationViewModel.swift similarity index 100% rename from Mastodon/Scene/Onboarding/AuthenticationViewModel.swift rename to Mastodon/Scene/Onboarding/Share/AuthenticationViewModel.swift diff --git a/Mastodon/Protocol/OnboardingViewControllerAppearance.swift b/Mastodon/Scene/Onboarding/Share/OnboardingViewControllerAppearance.swift similarity index 97% rename from Mastodon/Protocol/OnboardingViewControllerAppearance.swift rename to Mastodon/Scene/Onboarding/Share/OnboardingViewControllerAppearance.swift index 9ba359f25..c4b26321a 100644 --- a/Mastodon/Protocol/OnboardingViewControllerAppearance.swift +++ b/Mastodon/Scene/Onboarding/Share/OnboardingViewControllerAppearance.swift @@ -16,6 +16,7 @@ protocol OnboardingViewControllerAppearance: UIViewController { extension OnboardingViewControllerAppearance { static var actionButtonHeight: CGFloat { return 46 } + static var actionButtonMargin: CGFloat { return 12 } static var viewBottomPaddingHeight: CGFloat { return 11 } func setupOnboardingAppearance() { diff --git a/Mastodon/Scene/Onboarding/Welcome/WelcomeViewController.swift b/Mastodon/Scene/Onboarding/Welcome/WelcomeViewController.swift index 18c5caa08..e832e5a43 100644 --- a/Mastodon/Scene/Onboarding/Welcome/WelcomeViewController.swift +++ b/Mastodon/Scene/Onboarding/Welcome/WelcomeViewController.swift @@ -47,6 +47,11 @@ final class WelcomeViewController: UIViewController, NeedsDependency { button.translatesAutoresizingMaskIntoConstraints = false return button }() + + deinit { + os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s", ((#file as NSString).lastPathComponent), #line, #function) + } + } extension WelcomeViewController { @@ -74,14 +79,14 @@ extension WelcomeViewController { view.addSubview(signInButton) view.addSubview(signUpButton) NSLayoutConstraint.activate([ - signInButton.leadingAnchor.constraint(equalTo: view.readableContentGuide.leadingAnchor, constant: 12), - view.readableContentGuide.trailingAnchor.constraint(equalTo: signInButton.trailingAnchor, constant: 12), + signInButton.leadingAnchor.constraint(equalTo: view.readableContentGuide.leadingAnchor, constant: WelcomeViewController.actionButtonMargin), + view.readableContentGuide.trailingAnchor.constraint(equalTo: signInButton.trailingAnchor, constant: WelcomeViewController.actionButtonMargin), view.layoutMarginsGuide.bottomAnchor.constraint(equalTo: signInButton.bottomAnchor, constant: WelcomeViewController.viewBottomPaddingHeight), - signInButton.heightAnchor.constraint(equalToConstant: 46).priority(.defaultHigh), + signInButton.heightAnchor.constraint(equalToConstant: WelcomeViewController.actionButtonHeight).priority(.defaultHigh), signInButton.topAnchor.constraint(equalTo: signUpButton.bottomAnchor, constant: 9), - signUpButton.leadingAnchor.constraint(equalTo: view.readableContentGuide.leadingAnchor, constant: 12), - view.readableContentGuide.trailingAnchor.constraint(equalTo: signUpButton.trailingAnchor, constant: 12), + signUpButton.leadingAnchor.constraint(equalTo: view.readableContentGuide.leadingAnchor, constant: WelcomeViewController.actionButtonMargin), + view.readableContentGuide.trailingAnchor.constraint(equalTo: signUpButton.trailingAnchor, constant: WelcomeViewController.actionButtonMargin), signUpButton.heightAnchor.constraint(equalToConstant: WelcomeViewController.actionButtonHeight).priority(.defaultHigh), ]) @@ -89,19 +94,28 @@ extension WelcomeViewController { signInButton.addTarget(self, action: #selector(signInButtonDidClicked(_:)), for: .touchUpInside) } + override var preferredStatusBarStyle: UIStatusBarStyle { return .darkContent } + } extension WelcomeViewController { @objc private func signUpButtonDidClicked(_ sender: UIButton) { - coordinator.present(scene: .pickServer(viewMode: PickServerViewModel(context: context, mode: .signUp)), from: self, transition: .show) + coordinator.present(scene: .mastodonPickServer(viewMode: MastodonPickServerViewModel(context: context, mode: .signUp)), from: self, transition: .show) } @objc private func signInButtonDidClicked(_ sender: UIButton) { - coordinator.present(scene: .pickServer(viewMode: PickServerViewModel(context: context, mode: .signIn)), from: self, transition: .show) + coordinator.present(scene: .mastodonPickServer(viewMode: MastodonPickServerViewModel(context: context, mode: .signIn)), from: self, transition: .show) } } // MARK: - OnboardingViewControllerAppearance extension WelcomeViewController: OnboardingViewControllerAppearance { } + +// MARK: - UIAdaptivePresentationControllerDelegate +extension WelcomeViewController: UIAdaptivePresentationControllerDelegate { + func adaptivePresentationStyle(for controller: UIPresentationController, traitCollection: UITraitCollection) -> UIModalPresentationStyle { + return .fullScreen + } +} diff --git a/Mastodon/Scene/Share/NavigationController/DarkContentStatusBarStyleNavigationController.swift b/Mastodon/Scene/Share/NavigationController/DarkContentStatusBarStyleNavigationController.swift new file mode 100644 index 000000000..0fa4a0e20 --- /dev/null +++ b/Mastodon/Scene/Share/NavigationController/DarkContentStatusBarStyleNavigationController.swift @@ -0,0 +1,14 @@ +// +// DarkContentStatusBarStyleNavigationController.swift +// +// +// Created by MainasuK Cirno on 2021-2-26. +// + +import UIKit + +final class DarkContentStatusBarStyleNavigationController: UINavigationController { + override var preferredStatusBarStyle: UIStatusBarStyle { + return .darkContent + } +} diff --git a/Mastodon/Supporting Files/SceneDelegate.swift b/Mastodon/Supporting Files/SceneDelegate.swift index 934c115ce..e13395ccd 100644 --- a/Mastodon/Supporting Files/SceneDelegate.swift +++ b/Mastodon/Supporting Files/SceneDelegate.swift @@ -25,22 +25,7 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate { self.coordinator = sceneCoordinator sceneCoordinator.setup() - -// do { -// let request = MastodonAuthentication.sortedFetchRequest -// if try appContext.managedObjectContext.fetch(request).isEmpty { -// DispatchQueue.main.async { -// sceneCoordinator.present( -// scene: .welcome, -// from: nil, -// transition: .modal(animated: false, completion: nil) -// ) -// } -// } -// } catch { -// assertionFailure(error.localizedDescription) -// } - + sceneCoordinator.setupOnboardingIfNeeds(animated: false) window.makeKeyAndVisible() } diff --git a/MastodonSDK/Sources/MastodonSDK/API/Error/Mastodon+API+Error.swift b/MastodonSDK/Sources/MastodonSDK/API/Error/Mastodon+API+Error.swift index ee649b43e..05540feda 100644 --- a/MastodonSDK/Sources/MastodonSDK/API/Error/Mastodon+API+Error.swift +++ b/MastodonSDK/Sources/MastodonSDK/API/Error/Mastodon+API+Error.swift @@ -39,7 +39,7 @@ extension Mastodon.API.Error: LocalizedError { public var errorDescription: String? { guard let mastodonError = mastodonError else { - return nil + return "HTTP \(httpResponseStatus.code)" } switch mastodonError { case .generic(let error): @@ -49,7 +49,7 @@ extension Mastodon.API.Error: LocalizedError { public var failureReason: String? { guard let mastodonError = mastodonError else { - return nil + return httpResponseStatus.reasonPhrase } switch mastodonError { case .generic(let error):