chore: make onboarding ready

This commit is contained in:
CMK 2021-02-26 18:27:47 +08:00
parent 24fc57d982
commit 7aa45ff230
30 changed files with 350 additions and 619 deletions

View File

@ -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 havent.",
"resend_email": "Resend email"
"resend_email": "Resend Email"
},
"open_email_app": {
"title": "Check your inbox.",

View File

@ -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 = "<group>"; };
0FAA101125E105390017CCDE /* PrimaryActionButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PrimaryActionButton.swift; sourceTree = "<group>"; };
0FAA101B25E10E760017CCDE /* UIFont.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UIFont.swift; sourceTree = "<group>"; };
0FAA102625E1126A0017CCDE /* PickServerViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PickServerViewController.swift; sourceTree = "<group>"; };
0FB3D2F625E4C24D00AAD544 /* PickServerViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PickServerViewModel.swift; sourceTree = "<group>"; };
0FAA102625E1126A0017CCDE /* MastodonPickServerViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MastodonPickServerViewController.swift; sourceTree = "<group>"; };
0FB3D2F625E4C24D00AAD544 /* MastodonPickServerViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MastodonPickServerViewModel.swift; sourceTree = "<group>"; };
0FB3D2FD25E4CB6400AAD544 /* PickServerTitleCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PickServerTitleCell.swift; sourceTree = "<group>"; };
0FB3D30725E524C600AAD544 /* PickServerCategoriesCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PickServerCategoriesCell.swift; sourceTree = "<group>"; };
0FB3D30E25E525CD00AAD544 /* PickServerCategoryView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PickServerCategoryView.swift; sourceTree = "<group>"; };
@ -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 = "<group>"; };
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 = "<group>"; };
DB0140A025C40C0600F9F3CF /* MastodonPinBasedAuthenticationViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MastodonPinBasedAuthenticationViewController.swift; sourceTree = "<group>"; };
DB0140A725C40C1500F9F3CF /* MastodonPinBasedAuthenticationViewModelNavigationDelegateShim.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MastodonPinBasedAuthenticationViewModelNavigationDelegateShim.swift; sourceTree = "<group>"; };
DB0140AD25C40C7300F9F3CF /* MastodonPinBasedAuthenticationViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MastodonPinBasedAuthenticationViewModel.swift; sourceTree = "<group>"; };
@ -303,6 +302,7 @@
DB118A8B25E4BFB500FAB162 /* HighlightDimmableButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HighlightDimmableButton.swift; sourceTree = "<group>"; };
DB2B3ABD25E37E15007045F9 /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/InfoPlist.strings; sourceTree = "<group>"; };
DB2B3AE825E38850007045F9 /* UIViewPreview.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UIViewPreview.swift; sourceTree = "<group>"; };
DB2F073325E8ECF000957B2D /* AuthenticationViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AuthenticationViewModel.swift; sourceTree = "<group>"; };
DB3D0FED25BAA42200EAA174 /* MastodonSDK */ = {isa = PBXFileReference; lastKnownFileType = folder; path = MastodonSDK; sourceTree = "<group>"; };
DB3D100E25BAA75E00EAA174 /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/Localizable.strings; sourceTree = "<group>"; };
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 = "<group>"; };
DB5086BD25CC0D9900C2C187 /* SplashPreference.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SplashPreference.swift; sourceTree = "<group>"; };
DB68586325E619B700F0A850 /* NSKeyValueObservation.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NSKeyValueObservation.swift; sourceTree = "<group>"; };
DB68A04925E9027700CFDF14 /* DarkContentStatusBarStyleNavigationController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DarkContentStatusBarStyleNavigationController.swift; sourceTree = "<group>"; };
DB72601B25E36A2100235243 /* MastodonServerRulesViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MastodonServerRulesViewController.swift; sourceTree = "<group>"; };
DB72602625E36A6F00235243 /* MastodonServerRulesViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MastodonServerRulesViewModel.swift; sourceTree = "<group>"; };
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 = "<group>"; };
DB8AF55C25C138B7002E6C99 /* UIViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UIViewController.swift; sourceTree = "<group>"; };
DB8AF56725C13E2A002E6C99 /* HomeTimelineIndex.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomeTimelineIndex.swift; sourceTree = "<group>"; };
DB98334625C8056600AD9700 /* AuthenticationViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AuthenticationViewModel.swift; sourceTree = "<group>"; };
DB98336A25C9420100AD9700 /* APIService+App.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "APIService+App.swift"; sourceTree = "<group>"; };
DB98337025C9443200AD9700 /* APIService+Authentication.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "APIService+Authentication.swift"; sourceTree = "<group>"; };
DB98337E25C9452D00AD9700 /* APIService+APIError.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "APIService+APIError.swift"; sourceTree = "<group>"; };
@ -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 = "<group>";
@ -598,7 +598,6 @@
2D38F1C525CD37F400561493 /* ContentOffsetAdjustableTimelineViewControllerDelegate.swift */,
2D38F20725CD491300561493 /* DisposeBagCollectable.swift */,
2D5A3D3725CF8D9F002347D6 /* ScrollViewContainer.swift */,
2D82B9FE25E7863200E36F0F /* OnboardingViewControllerAppearance.swift */,
);
path = Protocol;
sourceTree = "<group>";
@ -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 = "<group>";
};
DB0140A625C40C0900F9F3CF /* PinBased */ = {
DB0140A625C40C0900F9F3CF /* PinBasedAuthentication */ = {
isa = PBXGroup;
children = (
DB0140A025C40C0600F9F3CF /* MastodonPinBasedAuthenticationViewController.swift */,
DB0140AD25C40C7300F9F3CF /* MastodonPinBasedAuthenticationViewModel.swift */,
DB0140A725C40C1500F9F3CF /* MastodonPinBasedAuthenticationViewModelNavigationDelegateShim.swift */,
);
path = PinBased;
path = PinBasedAuthentication;
sourceTree = "<group>";
};
DB084B5125CBC56300F898ED /* CoreDataStack */ = {
@ -856,6 +855,23 @@
path = Preference;
sourceTree = "<group>";
};
DB68A03825E900CC00CFDF14 /* Share */ = {
isa = PBXGroup;
children = (
2D82B9FE25E7863200E36F0F /* OnboardingViewControllerAppearance.swift */,
DB2F073325E8ECF000957B2D /* AuthenticationViewModel.swift */,
);
path = Share;
sourceTree = "<group>";
};
DB68A04F25E9028800CFDF14 /* NavigationController */ = {
isa = PBXGroup;
children = (
DB68A04925E9027700CFDF14 /* DarkContentStatusBarStyleNavigationController.swift */,
);
path = NavigationController;
sourceTree = "<group>";
};
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 */,

View File

@ -7,12 +7,17 @@
<key>CoreDataStack.xcscheme_^#shared#^_</key>
<dict>
<key>orderHint</key>
<integer>9</integer>
<integer>11</integer>
</dict>
<key>Mastodon - RTL.xcscheme_^#shared#^_</key>
<dict>
<key>orderHint</key>
<integer>8</integer>
<integer>9</integer>
</dict>
<key>Mastodon - Release.xcscheme_^#shared#^_</key>
<dict>
<key>orderHint</key>
<integer>1</integer>
</dict>
<key>Mastodon.xcscheme_^#shared#^_</key>
<dict>

View File

@ -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):

View File

@ -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 havent.
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")

View File

@ -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 havent.";
"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 havent.";
"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.";
back in your hands.";

View File

@ -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

View File

@ -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)
}
}

View File

@ -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<AnyCancellable>()
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<Mastodon.Entity.Instance>
let application: Mastodon.Response.Content<Mastodon.Entity.Application>
}
private struct SignUpResponseSecond {
let instance: Mastodon.Response.Content<Mastodon.Entity.Instance>
let authenticateInfo: AuthenticationViewModel.AuthenticateInfo
}
private struct SignUpResponseThird {
let instance: Mastodon.Response.Content<Mastodon.Entity.Instance>
let authenticateInfo: AuthenticationViewModel.AuthenticateInfo
let applicationToken: Mastodon.Response.Content<Mastodon.Entity.Token>
}
@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<SignUpResponseFirst, Error>? in
guard let self = self else { return nil }
guard response.value.registrations != false else {
return Fail(error: AuthenticationViewModel.AuthenticationError.registrationClosed).eraseToAnyPublisher()
}
return self.context.apiService.createApplication(domain: 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<SignUpResponseThird, Error>? 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
}
}

View File

@ -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<AnyCancellable>()
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 { }

View File

@ -9,7 +9,7 @@ import UIKit
class PickServerCategoryCollectionViewCell: UICollectionViewCell {
var category: PickServerViewModel.Category? {
var category: MastodonPickServerViewModel.Category? {
didSet {
categoryView.category = category
}

View File

@ -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<AnyCancellable>()
weak var context: AppContext! { willSet { precondition(!isViewLoaded) } }
weak var coordinator: SceneCoordinator! { willSet { precondition(!isViewLoaded) } }
var viewModel: PickServerViewModel!
var viewModel: MastodonPickServerViewModel!
private var isAuthenticating = CurrentValueSubject<Bool, Never>(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<Result<Bool, Error>, Never> in
guard let self = self else { return Just(.success(false)).eraseToAnyPublisher() }
return self.context.authenticationService.activeMastodonUser(domain: domain, userID: user.id)
}
.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<PickServerViewModel.SignUpResponseFirst, Error>? in
.compactMap { [weak self] response -> AnyPublisher<MastodonPickServerViewModel.SignUpResponseFirst, Error>? in
guard let self = self else { return nil }
guard response.value.registrations != false else {
return Fail(error: AuthenticationViewModel.AuthenticationError.registrationClosed).eraseToAnyPublisher()
}
return self.context.apiService.createApplication(domain: server.domain)
.map { PickServerViewModel.SignUpResponseFirst(instance: response, application: $0) }
.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<PickServerViewModel.SignUpResponseThird, Error>? in
.compactMap { [weak self] response -> AnyPublisher<MastodonPickServerViewModel.SignUpResponseThird, Error>? 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 { }

View File

@ -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<Mastodon.Entity.Instance>
let application: Mastodon.Response.Content<Mastodon.Entity.Application>

View File

@ -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
}

View File

@ -9,7 +9,7 @@ import UIKit
import MastodonSDK
class PickServerCategoryView: UIView {
var category: PickServerViewModel.Category? {
var category: MastodonPickServerViewModel.Category? {
didSet {
updateCategory()
}

View File

@ -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<Mastodon.Response.Content<Mastodon.Entity.Token>, 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)
// }
}

View File

@ -23,33 +23,19 @@ final class MastodonRegisterViewModel {
let displayName = CurrentValueSubject<String, Never>("")
let email = CurrentValueSubject<String, Never>("")
let password = CurrentValueSubject<String, Never>("")
let invite = CurrentValueSubject<String, Never>("")
let isUsernameValidateDalay = CurrentValueSubject<Bool, Never>(true)
let isDisplayNameValidateDalay = CurrentValueSubject<Bool, Never>(true)
let isEmailValidateDalay = CurrentValueSubject<Bool, Never>(true)
let isPasswordValidateDalay = CurrentValueSubject<Bool, Never>(true)
let isInviteValidateDelay = CurrentValueSubject<Bool, Never>(true)
let isRegistering = CurrentValueSubject<Bool, Never>(false)
let reason = CurrentValueSubject<String, Never>("")
// 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<ValidateState, Never>(.empty)
let displayNameValidateState = CurrentValueSubject<ValidateState, Never>(.empty)
let emailValidateState = CurrentValueSubject<ValidateState, Never>(.empty)
let passwordValidateState = CurrentValueSubject<ValidateState, Never>(.empty)
let inviteValidateState = CurrentValueSubject<ValidateState, Never>(.empty)
let isRegistering = CurrentValueSubject<Bool, Never>(false)
let isAllValid = CurrentValueSubject<Bool, Never>(false)
let error = CurrentValueSubject<Error?, Never>(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

View File

@ -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 {

View File

@ -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 {

View File

@ -7,9 +7,12 @@
import os.log
import UIKit
import Combine
final class MastodonServerRulesViewController: UIViewController, NeedsDependency {
var disposeBag = Set<AnyCancellable>()
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)
}
}

View File

@ -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<Bool, Never>(false)
let error = CurrentValueSubject<Error?, Never>(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
}

View File

@ -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() {

View File

@ -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
}
}

View File

@ -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
}
}

View File

@ -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()
}

View File

@ -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):