diff --git a/.github/scripts/build.sh b/.github/scripts/build.sh index 76e65f49..f5894901 100755 --- a/.github/scripts/build.sh +++ b/.github/scripts/build.sh @@ -7,7 +7,6 @@ set -eo pipefail xcodebuild -workspace Mastodon.xcworkspace \ -scheme Mastodon \ - -disableAutomaticPackageResolution \ -destination "platform=iOS Simulator,name=iPhone SE (2nd generation)" \ clean \ - build | xcpretty \ No newline at end of file + build | xcpretty diff --git a/AppShared/Info.plist b/AppShared/Info.plist index 9fe845c6..f652792e 100644 --- a/AppShared/Info.plist +++ b/AppShared/Info.plist @@ -15,8 +15,8 @@ CFBundlePackageType $(PRODUCT_BUNDLE_PACKAGE_TYPE) CFBundleShortVersionString - 1.2.0 + 1.3.0 CFBundleVersion - 88 + 90 diff --git a/CoreDataStack/Info.plist b/CoreDataStack/Info.plist index 9fe845c6..f652792e 100644 --- a/CoreDataStack/Info.plist +++ b/CoreDataStack/Info.plist @@ -15,8 +15,8 @@ CFBundlePackageType $(PRODUCT_BUNDLE_PACKAGE_TYPE) CFBundleShortVersionString - 1.2.0 + 1.3.0 CFBundleVersion - 88 + 90 diff --git a/CoreDataStackTests/Info.plist b/CoreDataStackTests/Info.plist index 9fe845c6..f652792e 100644 --- a/CoreDataStackTests/Info.plist +++ b/CoreDataStackTests/Info.plist @@ -15,8 +15,8 @@ CFBundlePackageType $(PRODUCT_BUNDLE_PACKAGE_TYPE) CFBundleShortVersionString - 1.2.0 + 1.3.0 CFBundleVersion - 88 + 90 diff --git a/Localization/app.json b/Localization/app.json index 6d3b2fcc..0071f6f9 100644 --- a/Localization/app.json +++ b/Localization/app.json @@ -193,10 +193,14 @@ }, "scene": { "welcome": { - "slogan": "Social networking\nback in your hands." + "slogan": "Social networking\nback in your hands.", + "get_started": "Get Started", + "log_in": "Log In" }, "server_picker": { - "title": "Pick a server,\nany server.", + "title": "Mastodon is made of users in different communities.", + "subtitle": "Pick a community based on your interests, region, or a general purpose one.", + "subtitle_extend": "Pick a community based on your interests, region, or a general purpose one. Each community is operated by an entirely independent organization or individual.", "button": { "category": { "all": "All", @@ -223,7 +227,7 @@ "category": "CATEGORY" }, "input": { - "placeholder": "Find a server or join your own..." + "placeholder": "Search communities" }, "empty_state": { "finding_servers": "Finding available servers...", @@ -232,7 +236,7 @@ } }, "register": { - "title": "Tell us about you.", + "title": "Let’s get you set up on %s", "input": { "avatar": { "delete": "Delete" @@ -249,6 +253,12 @@ }, "password": { "placeholder": "password", + "require": "Your password needs at least:", + "character_limit": "8 characters", + "accessibility": { + "checked": "checked", + "unchecked": "unchecked" + }, "hint": "Your password needs at least eight characters" }, "invite": { @@ -286,7 +296,7 @@ }, "server_rules": { "title": "Some ground rules.", - "subtitle": "These rules are set by the admins of %s.", + "subtitle": "These are set and enforced by the %s moderators.", "prompt": "By continuing, you’re subject to the terms of service and privacy policy for %s.", "terms_of_service": "terms of service", "privacy_policy": "privacy policy", @@ -296,10 +306,10 @@ }, "confirm_email": { "title": "One last thing.", - "subtitle": "We just sent an email to %s,\ntap the link to confirm your account.", + "subtitle": "Tap the link we emailed to you to verify your account.", "button": { "open_email_app": "Open Email App", - "dont_receive_email": "I never got an email" + "resend": "Resend" }, "dont_receive_email": { "title": "Check your email", @@ -554,4 +564,4 @@ "accessibility_hint": "Double tap to dismiss this wizard" } } -} \ No newline at end of file +} diff --git a/Mastodon.xcodeproj/project.pbxproj b/Mastodon.xcodeproj/project.pbxproj index 6b7644e3..b79fb901 100644 --- a/Mastodon.xcodeproj/project.pbxproj +++ b/Mastodon.xcodeproj/project.pbxproj @@ -22,7 +22,7 @@ 0FAA101C25E10E760017CCDE /* UIFont.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0FAA101B25E10E760017CCDE /* UIFont.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 */; }; + 0FB3D2FE25E4CB6400AAD544 /* OnboardingHeadlineTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0FB3D2FD25E4CB6400AAD544 /* OnboardingHeadlineTableViewCell.swift */; }; 0FB3D30825E524C600AAD544 /* PickServerCategoriesCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0FB3D30725E524C600AAD544 /* PickServerCategoriesCell.swift */; }; 0FB3D30F25E525CD00AAD544 /* PickServerCategoryView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0FB3D30E25E525CD00AAD544 /* PickServerCategoryView.swift */; }; 0FB3D31E25E534C700AAD544 /* PickServerCategoryCollectionViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0FB3D31D25E534C700AAD544 /* PickServerCategoryCollectionViewCell.swift */; }; @@ -183,7 +183,6 @@ DB0140CF25C42AEE00F9F3CF /* OSLog.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB0140CE25C42AEE00F9F3CF /* OSLog.swift */; }; DB01E23326A98F0900C3965B /* MastodonMeta in Frameworks */ = {isa = PBXBuildFile; productRef = DB01E23226A98F0900C3965B /* MastodonMeta */; }; DB01E23526A98F0900C3965B /* MetaTextKit in Frameworks */ = {isa = PBXBuildFile; productRef = DB01E23426A98F0900C3965B /* MetaTextKit */; }; - DB023295267F0AB800031745 /* ASMetaEditableTextNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB023294267F0AB800031745 /* ASMetaEditableTextNode.swift */; }; DB029E95266A20430062874E /* MastodonAuthenticationController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB029E94266A20430062874E /* MastodonAuthenticationController.swift */; }; DB02CDAB26256A9500D0A2AF /* ThreadReplyLoaderTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB02CDAA26256A9500D0A2AF /* ThreadReplyLoaderTableViewCell.swift */; }; DB02CDBF2625AE5000D0A2AF /* AdaptiveUserInterfaceStyleBarButtonItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB02CDBE2625AE5000D0A2AF /* AdaptiveUserInterfaceStyleBarButtonItem.swift */; }; @@ -192,6 +191,18 @@ DB03F7F32689AEA3007B274C /* ComposeRepliedToStatusContentTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB03F7F22689AEA3007B274C /* ComposeRepliedToStatusContentTableViewCell.swift */; }; DB03F7F52689B782007B274C /* ComposeTableView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB03F7F42689B782007B274C /* ComposeTableView.swift */; }; DB040ED126538E3D00BEE9D8 /* Trie.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB040ED026538E3C00BEE9D8 /* Trie.swift */; }; + DB0617EB277EF3820030EE79 /* GradientBorderView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB0617EA277EF3820030EE79 /* GradientBorderView.swift */; }; + DB0617ED277F02C50030EE79 /* OnboardingNavigationController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB0617EC277F02C50030EE79 /* OnboardingNavigationController.swift */; }; + DB0617EF277F12720030EE79 /* NavigationActionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB0617EE277F12720030EE79 /* NavigationActionView.swift */; }; + DB0617F1278413D00030EE79 /* PickServerServerSectionTableHeaderView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB0617F0278413D00030EE79 /* PickServerServerSectionTableHeaderView.swift */; }; + DB0617F527855AB90030EE79 /* ServerRuleSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB0617F427855AB90030EE79 /* ServerRuleSection.swift */; }; + DB0617FD27855BFE0030EE79 /* ServerRuleItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB0617FC27855BFE0030EE79 /* ServerRuleItem.swift */; }; + DB0617FF27855D6C0030EE79 /* MastodonServerRulesViewModel+Diffable.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB0617FE27855D6C0030EE79 /* MastodonServerRulesViewModel+Diffable.swift */; }; + DB0618012785732C0030EE79 /* ServerRulesTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB0618002785732C0030EE79 /* ServerRulesTableViewCell.swift */; }; + DB0618032785A7100030EE79 /* RegisterSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB0618022785A7100030EE79 /* RegisterSection.swift */; }; + DB0618052785A73D0030EE79 /* RegisterItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB0618042785A73D0030EE79 /* RegisterItem.swift */; }; + DB0618072785A8880030EE79 /* MastodonRegisterViewModel+Diffable.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB0618062785A8880030EE79 /* MastodonRegisterViewModel+Diffable.swift */; }; + DB06180A2785B2AB0030EE79 /* MastodonRegisterAvatarTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB0618092785B2AB0030EE79 /* MastodonRegisterAvatarTableViewCell.swift */; }; DB084B5725CBC56C00F898ED /* Status.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB084B5625CBC56C00F898ED /* Status.swift */; }; DB0AC6FC25CD02E600D75117 /* APIService+Instance.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB0AC6FB25CD02E600D75117 /* APIService+Instance.swift */; }; DB0C946526A6FD4D0088FB11 /* AlamofireImage in Frameworks */ = {isa = PBXBuildFile; productRef = DB0C946426A6FD4D0088FB11 /* AlamofireImage */; }; @@ -215,7 +226,6 @@ DB1D84382657B275000346B3 /* SegmentedControlNavigateable.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB1D84372657B275000346B3 /* SegmentedControlNavigateable.swift */; }; DB1E346825F518E20079D7DF /* CategoryPickerSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB1E346725F518E20079D7DF /* CategoryPickerSection.swift */; }; DB1E347825F519300079D7DF /* PickServerItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB1E347725F519300079D7DF /* PickServerItem.swift */; }; - DB1EE7B2267F9525000CC337 /* StatusProvider+StatusNodeDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB1EE7B1267F9525000CC337 /* StatusProvider+StatusNodeDelegate.swift */; }; DB1FD43625F26899004CFCFC /* MastodonPickServerViewModel+LoadIndexedServerState.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB1FD43525F26899004CFCFC /* MastodonPickServerViewModel+LoadIndexedServerState.swift */; }; DB1FD44425F26CCC004CFCFC /* PickServerSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB1FD44325F26CCC004CFCFC /* PickServerSection.swift */; }; DB1FD45025F26FA1004CFCFC /* MastodonPickServerViewModel+Diffable.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB1FD44F25F26FA1004CFCFC /* MastodonPickServerViewModel+Diffable.swift */; }; @@ -242,7 +252,6 @@ DB427DE225BAA00100D1B89D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = DB427DE025BAA00100D1B89D /* LaunchScreen.storyboard */; }; DB427DED25BAA00100D1B89D /* MastodonTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB427DEC25BAA00100D1B89D /* MastodonTests.swift */; }; DB427DF825BAA00100D1B89D /* MastodonUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB427DF725BAA00100D1B89D /* MastodonUITests.swift */; }; - DB44384F25E8C1FA008912A2 /* CALayer.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB44384E25E8C1FA008912A2 /* CALayer.swift */; }; DB443CD42694627B00159B29 /* AppearanceView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB443CD32694627B00159B29 /* AppearanceView.swift */; }; DB44767B260B3B8C00B66B82 /* CustomEmojiPickerInputView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB44767A260B3B8C00B66B82 /* CustomEmojiPickerInputView.swift */; }; DB447681260B3ED600B66B82 /* CustomEmojiPickerSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB447680260B3ED600B66B82 /* CustomEmojiPickerSection.swift */; }; @@ -391,6 +400,8 @@ DB789A1225F9F2CC0071ACA0 /* ComposeViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB789A1125F9F2CC0071ACA0 /* ComposeViewModel.swift */; }; DB7F48452620241000796008 /* ProfileHeaderViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB7F48442620241000796008 /* ProfileHeaderViewModel.swift */; }; DB8190C62601FF0400020C08 /* AttachmentContainerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB8190C52601FF0400020C08 /* AttachmentContainerView.swift */; }; + DB8481152788121200BBEABA /* MastodonRegisterTextFieldTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB8481142788121200BBEABA /* MastodonRegisterTextFieldTableViewCell.swift */; }; + DB84811727883C2600BBEABA /* MastodonRegisterPasswordHintTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB84811627883C2600BBEABA /* MastodonRegisterPasswordHintTableViewCell.swift */; }; DB852D1926FAEB6B00FC9D81 /* SidebarViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB852D1826FAEB6B00FC9D81 /* SidebarViewController.swift */; }; DB852D1C26FB021500FC9D81 /* RootSplitViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB852D1B26FB021500FC9D81 /* RootSplitViewController.swift */; }; DB852D1F26FB037800FC9D81 /* SidebarViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB852D1E26FB037800FC9D81 /* SidebarViewModel.swift */; }; @@ -482,12 +493,6 @@ DBA94440265D137600C537E1 /* Mastodon+Entity+Field.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBA9443F265D137600C537E1 /* Mastodon+Entity+Field.swift */; }; DBABE3EC25ECAC4B00879EE5 /* WelcomeIllustrationView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBABE3EB25ECAC4B00879EE5 /* WelcomeIllustrationView.swift */; }; DBAC6483267D0B21007FE9FD /* DifferenceKit in Frameworks */ = {isa = PBXBuildFile; productRef = DBAC6482267D0B21007FE9FD /* DifferenceKit */; }; - DBAC6485267D0F9E007FE9FD /* StatusNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBAC6484267D0F9E007FE9FD /* StatusNode.swift */; }; - DBAC6488267D388B007FE9FD /* ASTableNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBAC6487267D388B007FE9FD /* ASTableNode.swift */; }; - DBAC648F267DC84D007FE9FD /* TableNodeDiffableDataSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBAC648E267DC84D007FE9FD /* TableNodeDiffableDataSource.swift */; }; - DBAC6497267DECCB007FE9FD /* TimelineMiddleLoaderNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBAC6496267DECCB007FE9FD /* TimelineMiddleLoaderNode.swift */; }; - DBAC6499267DF2C4007FE9FD /* TimelineBottomLoaderNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBAC6498267DF2C4007FE9FD /* TimelineBottomLoaderNode.swift */; }; - DBAC649B267DF8C8007FE9FD /* ActivityIndicatorNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBAC649A267DF8C8007FE9FD /* ActivityIndicatorNode.swift */; }; DBAC649E267DFE43007FE9FD /* DiffableDataSources in Frameworks */ = {isa = PBXBuildFile; productRef = DBAC649D267DFE43007FE9FD /* DiffableDataSources */; }; DBAC64A1267E6D02007FE9FD /* Fuzi in Frameworks */ = {isa = PBXBuildFile; productRef = DBAC64A0267E6D02007FE9FD /* Fuzi */; }; DBAE3F682615DD60004B8251 /* UserProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBAE3F672615DD60004B8251 /* UserProvider.swift */; }; @@ -569,14 +574,6 @@ DBC7A672260C897100E57475 /* StatusContentWarningEditorView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBC7A671260C897100E57475 /* StatusContentWarningEditorView.swift */; }; DBC7A67C260DFADE00E57475 /* StatusPublishService.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBC7A67B260DFADE00E57475 /* StatusPublishService.swift */; }; DBCBCBF4267CB070000F5B51 /* Decode85.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBCBCBF3267CB070000F5B51 /* Decode85.swift */; }; - DBCBCBFC2680ADB7000F5B51 /* AsyncHomeTimelineViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBCBCBFB2680ADB7000F5B51 /* AsyncHomeTimelineViewController.swift */; }; - DBCBCBFF2680AE98000F5B51 /* AsyncHomeTimelineViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBCBCBFE2680AE98000F5B51 /* AsyncHomeTimelineViewModel.swift */; }; - DBCBCC012680AF2A000F5B51 /* AsyncHomeTimelineViewModel+Diffable.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBCBCC002680AF2A000F5B51 /* AsyncHomeTimelineViewModel+Diffable.swift */; }; - DBCBCC032680AF6E000F5B51 /* AsyncHomeTimelineViewController+DebugAction.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBCBCC022680AF6E000F5B51 /* AsyncHomeTimelineViewController+DebugAction.swift */; }; - DBCBCC052680AFB9000F5B51 /* AsyncHomeTimelineViewController+Provider.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBCBCC042680AFB9000F5B51 /* AsyncHomeTimelineViewController+Provider.swift */; }; - DBCBCC072680AFEC000F5B51 /* AsyncHomeTimelineViewModel+LoadLatestState.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBCBCC062680AFEC000F5B51 /* AsyncHomeTimelineViewModel+LoadLatestState.swift */; }; - DBCBCC092680B01B000F5B51 /* AsyncHomeTimelineViewModel+LoadMiddleState.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBCBCC082680B01B000F5B51 /* AsyncHomeTimelineViewModel+LoadMiddleState.swift */; }; - DBCBCC0B2680B03F000F5B51 /* AsyncHomeTimelineViewModel+LoadOldestState.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBCBCC0A2680B03F000F5B51 /* AsyncHomeTimelineViewModel+LoadOldestState.swift */; }; DBCBCC0D2680B908000F5B51 /* HomeTimelinePreference.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBCBCC0C2680B908000F5B51 /* HomeTimelinePreference.swift */; }; DBCBED1726132DB500B49291 /* UserTimelineViewModel+Diffable.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBCBED1626132DB500B49291 /* UserTimelineViewModel+Diffable.swift */; }; DBCBED1D26132E1A00B49291 /* StatusFetchedResultsController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBCBED1C26132E1A00B49291 /* StatusFetchedResultsController.swift */; }; @@ -794,7 +791,7 @@ 0FAA101B25E10E760017CCDE /* UIFont.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UIFont.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 = ""; }; + 0FB3D2FD25E4CB6400AAD544 /* OnboardingHeadlineTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OnboardingHeadlineTableViewCell.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 = ""; }; 0FB3D31D25E534C700AAD544 /* PickServerCategoryCollectionViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PickServerCategoryCollectionViewCell.swift; sourceTree = ""; }; @@ -977,7 +974,6 @@ DB0009A826AEE5DC009B9D2D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.intentdefinition; name = Base; path = Base.lproj/Intents.intentdefinition; sourceTree = ""; }; DB0009AD26AEE5E4009B9D2D /* ar */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ar; path = ar.lproj/Intents.strings; sourceTree = ""; }; DB0140CE25C42AEE00F9F3CF /* OSLog.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OSLog.swift; sourceTree = ""; }; - DB023294267F0AB800031745 /* ASMetaEditableTextNode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ASMetaEditableTextNode.swift; sourceTree = ""; }; DB029E94266A20430062874E /* MastodonAuthenticationController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MastodonAuthenticationController.swift; sourceTree = ""; }; DB02CDAA26256A9500D0A2AF /* ThreadReplyLoaderTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ThreadReplyLoaderTableViewCell.swift; sourceTree = ""; }; DB02CDBE2625AE5000D0A2AF /* AdaptiveUserInterfaceStyleBarButtonItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AdaptiveUserInterfaceStyleBarButtonItem.swift; sourceTree = ""; }; @@ -986,6 +982,18 @@ DB03F7F22689AEA3007B274C /* ComposeRepliedToStatusContentTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComposeRepliedToStatusContentTableViewCell.swift; sourceTree = ""; }; DB03F7F42689B782007B274C /* ComposeTableView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComposeTableView.swift; sourceTree = ""; }; DB040ED026538E3C00BEE9D8 /* Trie.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Trie.swift; sourceTree = ""; }; + DB0617EA277EF3820030EE79 /* GradientBorderView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GradientBorderView.swift; sourceTree = ""; }; + DB0617EC277F02C50030EE79 /* OnboardingNavigationController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OnboardingNavigationController.swift; sourceTree = ""; }; + DB0617EE277F12720030EE79 /* NavigationActionView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NavigationActionView.swift; sourceTree = ""; }; + DB0617F0278413D00030EE79 /* PickServerServerSectionTableHeaderView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PickServerServerSectionTableHeaderView.swift; sourceTree = ""; }; + DB0617F427855AB90030EE79 /* ServerRuleSection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ServerRuleSection.swift; sourceTree = ""; }; + DB0617FC27855BFE0030EE79 /* ServerRuleItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ServerRuleItem.swift; sourceTree = ""; }; + DB0617FE27855D6C0030EE79 /* MastodonServerRulesViewModel+Diffable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "MastodonServerRulesViewModel+Diffable.swift"; sourceTree = ""; }; + DB0618002785732C0030EE79 /* ServerRulesTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ServerRulesTableViewCell.swift; sourceTree = ""; }; + DB0618022785A7100030EE79 /* RegisterSection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RegisterSection.swift; sourceTree = ""; }; + DB0618042785A73D0030EE79 /* RegisterItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RegisterItem.swift; sourceTree = ""; }; + DB0618062785A8880030EE79 /* MastodonRegisterViewModel+Diffable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "MastodonRegisterViewModel+Diffable.swift"; sourceTree = ""; }; + DB0618092785B2AB0030EE79 /* MastodonRegisterAvatarTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MastodonRegisterAvatarTableViewCell.swift; sourceTree = ""; }; DB084B5625CBC56C00F898ED /* Status.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Status.swift; sourceTree = ""; }; DB0AC6FB25CD02E600D75117 /* APIService+Instance.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "APIService+Instance.swift"; sourceTree = ""; }; DB0C946A26A700AB0088FB11 /* MastodonUser+Property.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "MastodonUser+Property.swift"; sourceTree = ""; }; @@ -1009,7 +1017,6 @@ DB1D84372657B275000346B3 /* SegmentedControlNavigateable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SegmentedControlNavigateable.swift; sourceTree = ""; }; DB1E346725F518E20079D7DF /* CategoryPickerSection.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CategoryPickerSection.swift; sourceTree = ""; }; DB1E347725F519300079D7DF /* PickServerItem.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PickServerItem.swift; sourceTree = ""; }; - DB1EE7B1267F9525000CC337 /* StatusProvider+StatusNodeDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "StatusProvider+StatusNodeDelegate.swift"; sourceTree = ""; }; DB1FD43525F26899004CFCFC /* MastodonPickServerViewModel+LoadIndexedServerState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "MastodonPickServerViewModel+LoadIndexedServerState.swift"; sourceTree = ""; }; DB1FD44325F26CCC004CFCFC /* PickServerSection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PickServerSection.swift; sourceTree = ""; }; DB1FD44F25F26FA1004CFCFC /* MastodonPickServerViewModel+Diffable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "MastodonPickServerViewModel+Diffable.swift"; sourceTree = ""; }; @@ -1043,7 +1050,6 @@ DB427DF325BAA00100D1B89D /* MastodonUITests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = MastodonUITests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; DB427DF725BAA00100D1B89D /* MastodonUITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MastodonUITests.swift; sourceTree = ""; }; DB427DF925BAA00100D1B89D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; - DB44384E25E8C1FA008912A2 /* CALayer.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CALayer.swift; sourceTree = ""; }; DB443CD32694627B00159B29 /* AppearanceView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppearanceView.swift; sourceTree = ""; }; DB44767A260B3B8C00B66B82 /* CustomEmojiPickerInputView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomEmojiPickerInputView.swift; sourceTree = ""; }; DB447680260B3ED600B66B82 /* CustomEmojiPickerSection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomEmojiPickerSection.swift; sourceTree = ""; }; @@ -1210,6 +1216,8 @@ DB789A1125F9F2CC0071ACA0 /* ComposeViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComposeViewModel.swift; sourceTree = ""; }; DB7F48442620241000796008 /* ProfileHeaderViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfileHeaderViewModel.swift; sourceTree = ""; }; DB8190C52601FF0400020C08 /* AttachmentContainerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AttachmentContainerView.swift; sourceTree = ""; }; + DB8481142788121200BBEABA /* MastodonRegisterTextFieldTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MastodonRegisterTextFieldTableViewCell.swift; sourceTree = ""; }; + DB84811627883C2600BBEABA /* MastodonRegisterPasswordHintTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MastodonRegisterPasswordHintTableViewCell.swift; sourceTree = ""; }; DB852D1826FAEB6B00FC9D81 /* SidebarViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SidebarViewController.swift; sourceTree = ""; }; DB852D1B26FB021500FC9D81 /* RootSplitViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RootSplitViewController.swift; sourceTree = ""; }; DB852D1E26FB037800FC9D81 /* SidebarViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SidebarViewModel.swift; sourceTree = ""; }; @@ -1329,12 +1337,6 @@ DBA9443D265CFA6400C537E1 /* ProfileFieldCollectionViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfileFieldCollectionViewCell.swift; sourceTree = ""; }; DBA9443F265D137600C537E1 /* Mastodon+Entity+Field.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Mastodon+Entity+Field.swift"; sourceTree = ""; }; DBABE3EB25ECAC4B00879EE5 /* WelcomeIllustrationView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WelcomeIllustrationView.swift; sourceTree = ""; }; - DBAC6484267D0F9E007FE9FD /* StatusNode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatusNode.swift; sourceTree = ""; }; - DBAC6487267D388B007FE9FD /* ASTableNode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ASTableNode.swift; sourceTree = ""; }; - DBAC648E267DC84D007FE9FD /* TableNodeDiffableDataSource.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TableNodeDiffableDataSource.swift; sourceTree = ""; }; - DBAC6496267DECCB007FE9FD /* TimelineMiddleLoaderNode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineMiddleLoaderNode.swift; sourceTree = ""; }; - DBAC6498267DF2C4007FE9FD /* TimelineBottomLoaderNode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineBottomLoaderNode.swift; sourceTree = ""; }; - DBAC649A267DF8C8007FE9FD /* ActivityIndicatorNode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ActivityIndicatorNode.swift; sourceTree = ""; }; DBAE3F672615DD60004B8251 /* UserProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserProvider.swift; sourceTree = ""; }; DBAE3F812615DDA3004B8251 /* ProfileViewController+UserProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ProfileViewController+UserProvider.swift"; sourceTree = ""; }; DBAE3F872615DDF4004B8251 /* UserProviderFacade.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserProviderFacade.swift; sourceTree = ""; }; @@ -1385,14 +1387,6 @@ DBC7A671260C897100E57475 /* StatusContentWarningEditorView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatusContentWarningEditorView.swift; sourceTree = ""; }; DBC7A67B260DFADE00E57475 /* StatusPublishService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatusPublishService.swift; sourceTree = ""; }; DBCBCBF3267CB070000F5B51 /* Decode85.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Decode85.swift; sourceTree = ""; }; - DBCBCBFB2680ADB7000F5B51 /* AsyncHomeTimelineViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AsyncHomeTimelineViewController.swift; sourceTree = ""; }; - DBCBCBFE2680AE98000F5B51 /* AsyncHomeTimelineViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AsyncHomeTimelineViewModel.swift; sourceTree = ""; }; - DBCBCC002680AF2A000F5B51 /* AsyncHomeTimelineViewModel+Diffable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "AsyncHomeTimelineViewModel+Diffable.swift"; sourceTree = ""; }; - DBCBCC022680AF6E000F5B51 /* AsyncHomeTimelineViewController+DebugAction.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "AsyncHomeTimelineViewController+DebugAction.swift"; sourceTree = ""; }; - DBCBCC042680AFB9000F5B51 /* AsyncHomeTimelineViewController+Provider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "AsyncHomeTimelineViewController+Provider.swift"; sourceTree = ""; }; - DBCBCC062680AFEC000F5B51 /* AsyncHomeTimelineViewModel+LoadLatestState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "AsyncHomeTimelineViewModel+LoadLatestState.swift"; sourceTree = ""; }; - DBCBCC082680B01B000F5B51 /* AsyncHomeTimelineViewModel+LoadMiddleState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "AsyncHomeTimelineViewModel+LoadMiddleState.swift"; sourceTree = ""; }; - DBCBCC0A2680B03F000F5B51 /* AsyncHomeTimelineViewModel+LoadOldestState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "AsyncHomeTimelineViewModel+LoadOldestState.swift"; sourceTree = ""; }; DBCBCC0C2680B908000F5B51 /* HomeTimelinePreference.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomeTimelinePreference.swift; sourceTree = ""; }; DBCBED1626132DB500B49291 /* UserTimelineViewModel+Diffable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UserTimelineViewModel+Diffable.swift"; sourceTree = ""; }; DBCBED1C26132E1A00B49291 /* StatusFetchedResultsController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatusFetchedResultsController.swift; sourceTree = ""; }; @@ -1622,9 +1616,6 @@ 0FB3D2FC25E4CB4B00AAD544 /* TableViewCell */ = { isa = PBXGroup; children = ( - 0FB3D2FD25E4CB6400AAD544 /* PickServerTitleCell.swift */, - 0FB3D30725E524C600AAD544 /* PickServerCategoriesCell.swift */, - 0FB3D33125E5F50E00AAD544 /* PickServerSearchCell.swift */, 0FB3D33725E6401400AAD544 /* PickServerCell.swift */, DB0F814F264D1E2500F2A12B /* PickServerLoaderTableViewCell.swift */, ); @@ -1636,6 +1627,7 @@ children = ( 0FB3D30E25E525CD00AAD544 /* PickServerCategoryView.swift */, DB9282B125F3222800823B15 /* PickServerEmptyStateView.swift */, + DB0617F0278413D00030EE79 /* PickServerServerSectionTableHeaderView.swift */, ); path = View; sourceTree = ""; @@ -1737,7 +1729,6 @@ isa = PBXGroup; children = ( DB1F239626117C360057430E /* View */, - DBCBCBFD2680ADBA000F5B51 /* AsyncHomeTimeline */, 2D38F1D425CD465300561493 /* HomeTimelineViewController.swift */, 2D38F1DE25CD46A400561493 /* HomeTimelineViewController+Provider.swift */, 2D5A3D6125CFD9CB002347D6 /* HomeTimelineViewController+DebugAction.swift */, @@ -1756,7 +1747,6 @@ 2D38F1FD25CD481700561493 /* StatusProvider.swift */, 2DF75B9A25D0E27500694EC8 /* StatusProviderFacade.swift */, 2DF75BA025D0E29D00694EC8 /* StatusProvider+StatusTableViewCellDelegate.swift */, - DB1EE7B1267F9525000CC337 /* StatusProvider+StatusNodeDelegate.swift */, DB59F0FD25EF5D96001F1DAB /* StatusProvider+UITableViewDelegate.swift */, DB71FD4525F8C6D200512AE1 /* StatusProvider+UITableViewDataSourcePrefetching.swift */, DB1D843526579DB5000346B3 /* StatusProvider+TableViewControllerNavigateable.swift */, @@ -1811,7 +1801,6 @@ DB51D171262832380062B7A1 /* BlurHashEncode.swift */, DB6180EC26391C6C0018D199 /* TransitioningMath.swift */, DB75BF1D263C1C1B00EDBF1F /* CustomScheduler.swift */, - DBAC649A267DF8C8007FE9FD /* ActivityIndicatorNode.swift */, DBF156E32702DB3F00EC00B7 /* HandleTapAction.swift */, DBF156E12702DA6900EC00B7 /* UIStatusBarManager+HandleTapAction.m */, DBF156E02702DA6800EC00B7 /* Mastodon-Bridging-Header.h */, @@ -1890,29 +1879,21 @@ 2D76319C25C151DE00929FB9 /* Diffiable */ = { isa = PBXGroup; children = ( - 2D76319D25C151F600929FB9 /* Section */, - 2D7631B125C159E700929FB9 /* Item */, + DB4F097826A039B400D62E92 /* Onboarding */, + DB0617FB27855B740030EE79 /* Account */, + DB0617F827855B170030EE79 /* User */, + DB0617F927855B460030EE79 /* Profile */, + DB4F097926A039C400D62E92 /* Status */, + DB0617F627855AF30030EE79 /* Poll */, + DB4F097626A0398000D62E92 /* Compose */, + DB0617F727855B010030EE79 /* Notification */, + DB4F097726A039A200D62E92 /* Search */, + DB0617FA27855B660030EE79 /* Settings */, DBCBED2226132E1D00B49291 /* FetchedResultsController */, - DBAC6490267DC84F007FE9FD /* DataSource */, ); path = Diffiable; sourceTree = ""; }; - 2D76319D25C151F600929FB9 /* Section */ = { - isa = PBXGroup; - children = ( - DB4F097926A039C400D62E92 /* Status */, - DB4F097826A039B400D62E92 /* Onboarding */, - DB4F097726A039A200D62E92 /* Search */, - DB4F097626A0398000D62E92 /* Compose */, - 2D4AD8A126316CD200613EFC /* SelectedAccountSection.swift */, - DB6D9F7C26358ED4008423CD /* SettingsSection.swift */, - DBA94433265CBB5300C537E1 /* ProfileFieldSection.swift */, - DB6B74FB272FF55800C70B6E /* UserSection.swift */, - ); - path = Section; - sourceTree = ""; - }; 2D7631A425C1532200929FB9 /* Share */ = { isa = PBXGroup; children = ( @@ -1938,7 +1919,6 @@ DB87D45C2609DE6600D12C0D /* TextField */, DB1D187125EF5BBD003F1F23 /* TableView */, 2D7631A625C1533800929FB9 /* TableviewCell */, - DBAC6486267D0FAC007FE9FD /* Node */, ); path = View; sourceTree = ""; @@ -1960,29 +1940,6 @@ path = TableviewCell; sourceTree = ""; }; - 2D7631B125C159E700929FB9 /* Item */ = { - isa = PBXGroup; - children = ( - 2D7631B225C159F700929FB9 /* Item.swift */, - DB6B74FD272FF59000C70B6E /* UserItem.swift */, - 2D198642261BF09500F0B013 /* SearchResultItem.swift */, - DB4F097C26A03A5B00D62E92 /* SearchHistoryItem.swift */, - 2D4AD8A726316D3500613EFC /* SelectedAccountItem.swift */, - 2D7867182625B77500211898 /* NotificationItem.swift */, - DB4481CB25EE2AFE00BEFB67 /* PollItem.swift */, - DB1E347725F519300079D7DF /* PickServerItem.swift */, - DB1FD45925F27898004CFCFC /* CategoryPickerItem.swift */, - DB66729B25F9F91F00D60309 /* ComposeStatusItem.swift */, - DB3667A0268ABB2E0027D07F /* ComposeStatusAttachmentItem.swift */, - DB3667A7268AE2900027D07F /* ComposeStatusPollItem.swift */, - DB44768A260B3F2100B66B82 /* CustomEmojiPickerItem.swift */, - DB6D9F8326358EEC008423CD /* SettingsItem.swift */, - DBBF1DCA2652539E00E5B703 /* AutoCompleteItem.swift */, - DBA94435265CBB7400C537E1 /* ProfileFieldItem.swift */, - ); - path = Item; - sourceTree = ""; - }; 2DA504672601ADBA008F4E6C /* Decoration */ = { isa = PBXGroup; children = ( @@ -2112,24 +2069,14 @@ DB68A03825E900CC00CFDF14 /* Share */, 0FAA0FDD25E0B5700017CCDE /* Welcome */, 0FAA102525E1125D0017CCDE /* PickServer */, - DBE0821A25CD382900FD6BBD /* Register */, DB72602125E36A2500235243 /* ServerRules */, + DBE0821A25CD382900FD6BBD /* Register */, 2D364F7025E66D5B00204FDC /* ResendEmail */, 2D59819925E4A55C000FB903 /* ConfirmEmail */, ); path = Onboarding; sourceTree = ""; }; - DB023296267F0ABE00031745 /* Status */ = { - isa = PBXGroup; - children = ( - DBAC6484267D0F9E007FE9FD /* StatusNode.swift */, - DBAC6496267DECCB007FE9FD /* TimelineMiddleLoaderNode.swift */, - DBAC6498267DF2C4007FE9FD /* TimelineBottomLoaderNode.swift */, - ); - path = Status; - sourceTree = ""; - }; DB03F7F1268990A2007B274C /* TableViewCell */ = { isa = PBXGroup; children = ( @@ -2140,6 +2087,87 @@ path = TableViewCell; sourceTree = ""; }; + DB0617F3278436360030EE79 /* Deprecated */ = { + isa = PBXGroup; + children = ( + 0FB3D33125E5F50E00AAD544 /* PickServerSearchCell.swift */, + 0FB3D30725E524C600AAD544 /* PickServerCategoriesCell.swift */, + ); + path = Deprecated; + sourceTree = ""; + }; + DB0617F627855AF30030EE79 /* Poll */ = { + isa = PBXGroup; + children = ( + DB4481C525EE2ADA00BEFB67 /* PollSection.swift */, + DB4481CB25EE2AFE00BEFB67 /* PollItem.swift */, + ); + path = Poll; + sourceTree = ""; + }; + DB0617F727855B010030EE79 /* Notification */ = { + isa = PBXGroup; + children = ( + 2D35237926256D920031AF25 /* NotificationSection.swift */, + 2D7867182625B77500211898 /* NotificationItem.swift */, + ); + path = Notification; + sourceTree = ""; + }; + DB0617F827855B170030EE79 /* User */ = { + isa = PBXGroup; + children = ( + DB6B74FB272FF55800C70B6E /* UserSection.swift */, + DB6B74FD272FF59000C70B6E /* UserItem.swift */, + ); + path = User; + sourceTree = ""; + }; + DB0617F927855B460030EE79 /* Profile */ = { + isa = PBXGroup; + children = ( + DBA94433265CBB5300C537E1 /* ProfileFieldSection.swift */, + DBA94435265CBB7400C537E1 /* ProfileFieldItem.swift */, + ); + path = Profile; + sourceTree = ""; + }; + DB0617FA27855B660030EE79 /* Settings */ = { + isa = PBXGroup; + children = ( + DB6D9F7C26358ED4008423CD /* SettingsSection.swift */, + DB6D9F8326358EEC008423CD /* SettingsItem.swift */, + ); + path = Settings; + sourceTree = ""; + }; + DB0617FB27855B740030EE79 /* Account */ = { + isa = PBXGroup; + children = ( + 2D4AD8A126316CD200613EFC /* SelectedAccountSection.swift */, + 2D4AD8A726316D3500613EFC /* SelectedAccountItem.swift */, + ); + path = Account; + sourceTree = ""; + }; + DB0618082785B2790030EE79 /* Cell */ = { + isa = PBXGroup; + children = ( + DB0618002785732C0030EE79 /* ServerRulesTableViewCell.swift */, + ); + path = Cell; + sourceTree = ""; + }; + DB06180B2785B2AF0030EE79 /* Cell */ = { + isa = PBXGroup; + children = ( + DB0618092785B2AB0030EE79 /* MastodonRegisterAvatarTableViewCell.swift */, + DB8481142788121200BBEABA /* MastodonRegisterTextFieldTableViewCell.swift */, + DB84811627883C2600BBEABA /* MastodonRegisterPasswordHintTableViewCell.swift */, + ); + path = Cell; + sourceTree = ""; + }; DB084B5125CBC56300F898ED /* CoreDataStack */ = { isa = PBXGroup; children = ( @@ -2271,6 +2299,7 @@ children = ( DB427DE325BAA00100D1B89D /* Info.plist */, DB89BA1025C10FF5008580ED /* Mastodon.entitlements */, + DB0617F3278436360030EE79 /* Deprecated */, 2D76319C25C151DE00929FB9 /* Diffiable */, DB8AF52A25C13561002E6C99 /* State */, 2D61335525C1886800CAE157 /* Service */, @@ -2390,10 +2419,15 @@ isa = PBXGroup; children = ( DB66729525F9F91600D60309 /* ComposeStatusSection.swift */, + DB66729B25F9F91F00D60309 /* ComposeStatusItem.swift */, DB36679E268ABAF20027D07F /* ComposeStatusAttachmentSection.swift */, + DB3667A0268ABB2E0027D07F /* ComposeStatusAttachmentItem.swift */, DB3667A5268AE2620027D07F /* ComposeStatusPollSection.swift */, + DB3667A7268AE2900027D07F /* ComposeStatusPollItem.swift */, DB447680260B3ED600B66B82 /* CustomEmojiPickerSection.swift */, + DB44768A260B3F2100B66B82 /* CustomEmojiPickerItem.swift */, DBBF1DC82652538500E5B703 /* AutoCompleteSection.swift */, + DBBF1DCA2652539E00E5B703 /* AutoCompleteItem.swift */, ); path = Compose; sourceTree = ""; @@ -2404,7 +2438,9 @@ 2DE0FAC02615F04D00CDF649 /* RecommendHashTagSection.swift */, 2DE0FACD2615F7AD00CDF649 /* RecommendAccountSection.swift */, 2D198648261C0B8500F0B013 /* SearchResultSection.swift */, + 2D198642261BF09500F0B013 /* SearchResultItem.swift */, DB4F097A26A039FF00D62E92 /* SearchHistorySection.swift */, + DB4F097C26A03A5B00D62E92 /* SearchHistoryItem.swift */, ); path = Search; sourceTree = ""; @@ -2413,7 +2449,13 @@ isa = PBXGroup; children = ( DB1FD44325F26CCC004CFCFC /* PickServerSection.swift */, + DB1E347725F519300079D7DF /* PickServerItem.swift */, DB1E346725F518E20079D7DF /* CategoryPickerSection.swift */, + DB1FD45925F27898004CFCFC /* CategoryPickerItem.swift */, + DB0617F427855AB90030EE79 /* ServerRuleSection.swift */, + DB0617FC27855BFE0030EE79 /* ServerRuleItem.swift */, + DB0618022785A7100030EE79 /* RegisterSection.swift */, + DB0618042785A73D0030EE79 /* RegisterItem.swift */, ); path = Onboarding; sourceTree = ""; @@ -2422,8 +2464,7 @@ isa = PBXGroup; children = ( 2D76319E25C1521200929FB9 /* StatusSection.swift */, - DB4481C525EE2ADA00BEFB67 /* PollSection.swift */, - 2D35237926256D920031AF25 /* NotificationSection.swift */, + 2D7631B225C159F700929FB9 /* Item.swift */, 5BB04FF4262F0E6D0043BFF6 /* ReportSection.swift */, ); path = Status; @@ -2563,9 +2604,12 @@ DB68A03825E900CC00CFDF14 /* Share */ = { isa = PBXGroup; children = ( - 2D82B9FE25E7863200E36F0F /* OnboardingViewControllerAppearance.swift */, DB2F073325E8ECF000957B2D /* AuthenticationViewModel.swift */, DB029E94266A20430062874E /* MastodonAuthenticationController.swift */, + 2D82B9FE25E7863200E36F0F /* OnboardingViewControllerAppearance.swift */, + DB0617EC277F02C50030EE79 /* OnboardingNavigationController.swift */, + 0FB3D2FD25E4CB6400AAD544 /* OnboardingHeadlineTableViewCell.swift */, + DB0617EE277F12720030EE79 /* NavigationActionView.swift */, ); path = Share; sourceTree = ""; @@ -2621,8 +2665,10 @@ DB72602125E36A2500235243 /* ServerRules */ = { isa = PBXGroup; children = ( + DB0618082785B2790030EE79 /* Cell */, DB72601B25E36A2100235243 /* MastodonServerRulesViewController.swift */, DB72602625E36A6F00235243 /* MastodonServerRulesViewModel.swift */, + DB0617FE27855D6C0030EE79 /* MastodonServerRulesViewModel+Diffable.swift */, ); path = ServerRules; sourceTree = ""; @@ -2825,7 +2871,6 @@ 2DF123A625C3B0210020F248 /* ActiveLabel.swift */, 5DF1056325F887CB00D6C0D4 /* AVPlayer.swift */, 0F20223826146553000C64BF /* Array.swift */, - DB44384E25E8C1FA008912A2 /* CALayer.swift */, 2D206B8525F5FB0900143C56 /* Double.swift */, DB97131E2666078B00BD1E90 /* Date.swift */, DBB3BA2926A81C020004F2D4 /* FLAnimatedImageView.swift */, @@ -3040,28 +3085,11 @@ children = ( DBABE3EB25ECAC4B00879EE5 /* WelcomeIllustrationView.swift */, DB4932B026F1FB5300EF46D4 /* WizardCardView.swift */, + DB0617EA277EF3820030EE79 /* GradientBorderView.swift */, ); path = View; sourceTree = ""; }; - DBAC6486267D0FAC007FE9FD /* Node */ = { - isa = PBXGroup; - children = ( - DB023296267F0ABE00031745 /* Status */, - DB023294267F0AB800031745 /* ASMetaEditableTextNode.swift */, - ); - path = Node; - sourceTree = ""; - }; - DBAC6490267DC84F007FE9FD /* DataSource */ = { - isa = PBXGroup; - children = ( - DBAC6487267D388B007FE9FD /* ASTableNode.swift */, - DBAC648E267DC84D007FE9FD /* TableNodeDiffableDataSource.swift */, - ); - path = DataSource; - sourceTree = ""; - }; DBAE3F742615DD63004B8251 /* UserProvider */ = { isa = PBXGroup; children = ( @@ -3187,21 +3215,6 @@ path = ShareActionExtension; sourceTree = ""; }; - DBCBCBFD2680ADBA000F5B51 /* AsyncHomeTimeline */ = { - isa = PBXGroup; - children = ( - DBCBCBFB2680ADB7000F5B51 /* AsyncHomeTimelineViewController.swift */, - DBCBCC022680AF6E000F5B51 /* AsyncHomeTimelineViewController+DebugAction.swift */, - DBCBCC042680AFB9000F5B51 /* AsyncHomeTimelineViewController+Provider.swift */, - DBCBCBFE2680AE98000F5B51 /* AsyncHomeTimelineViewModel.swift */, - DBCBCC002680AF2A000F5B51 /* AsyncHomeTimelineViewModel+Diffable.swift */, - DBCBCC062680AFEC000F5B51 /* AsyncHomeTimelineViewModel+LoadLatestState.swift */, - DBCBCC082680B01B000F5B51 /* AsyncHomeTimelineViewModel+LoadMiddleState.swift */, - DBCBCC0A2680B03F000F5B51 /* AsyncHomeTimelineViewModel+LoadOldestState.swift */, - ); - path = AsyncHomeTimeline; - sourceTree = ""; - }; DBCBED2226132E1D00B49291 /* FetchedResultsController */ = { isa = PBXGroup; children = ( @@ -3216,9 +3229,11 @@ DBE0821A25CD382900FD6BBD /* Register */ = { isa = PBXGroup; children = ( + DB06180B2785B2AF0030EE79 /* Cell */, DBE0821425CD382600FD6BBD /* MastodonRegisterViewController.swift */, 2D939AE725EE1CF80076FA61 /* MastodonRegisterViewController+Avatar.swift */, DBE0822325CD3F1E00FD6BBD /* MastodonRegisterViewModel.swift */, + DB0618062785A8880030EE79 /* MastodonRegisterViewModel+Diffable.swift */, ); path = Register; sourceTree = ""; @@ -3969,7 +3984,6 @@ 0FAA0FDF25E0B57E0017CCDE /* WelcomeViewController.swift in Sources */, 2D206B8C25F6015000143C56 /* AudioPlaybackService.swift in Sources */, 2D59819B25E4A581000FB903 /* MastodonConfirmEmailViewController.swift in Sources */, - DBCBCBFC2680ADB7000F5B51 /* AsyncHomeTimelineViewController.swift in Sources */, DB45FB1D25CA9D23005A8AC7 /* APIService+HomeTimeline.swift in Sources */, DBA94440265D137600C537E1 /* Mastodon+Entity+Field.swift in Sources */, DB49A61425FF2C5600B98345 /* EmojiService.swift in Sources */, @@ -3994,6 +4008,7 @@ DBB525502611ED6D002F1F29 /* ProfileHeaderView.swift in Sources */, 0FB3D33225E5F50E00AAD544 /* PickServerSearchCell.swift in Sources */, DB71FD5225F8CCAA00512AE1 /* APIService+Status.swift in Sources */, + DB8481152788121200BBEABA /* MastodonRegisterTextFieldTableViewCell.swift in Sources */, 5D0393962612D266007FE196 /* WebViewModel.swift in Sources */, 5B24BBDA262DB14800A9381B /* ReportViewModel.swift in Sources */, 2D5A3D3825CF8D9F002347D6 /* ScrollViewContainer.swift in Sources */, @@ -4015,13 +4030,12 @@ DB68A06325E905E000CFDF14 /* UIApplication.swift in Sources */, DB02CDAB26256A9500D0A2AF /* ThreadReplyLoaderTableViewCell.swift in Sources */, DB63BE7F268DD1070011D3F9 /* NotificationViewController+StatusProvider.swift in Sources */, + DB0617FF27855D6C0030EE79 /* MastodonServerRulesViewModel+Diffable.swift in Sources */, DBB5255E2611F07A002F1F29 /* ProfileViewModel.swift in Sources */, - DBAC648F267DC84D007FE9FD /* TableNodeDiffableDataSource.swift in Sources */, 2D8434FB25FF46B300EECE90 /* HomeTimelineNavigationBarTitleView.swift in Sources */, 0F1E2D0B2615C39400C38565 /* DoubleTitleLabelNavigationBarTitleView.swift in Sources */, DBA1DB80268F84F80052DB59 /* NotificationType.swift in Sources */, DB9A488A26034D40008B817C /* ComposeViewModel+PublishState.swift in Sources */, - DBAC649B267DF8C8007FE9FD /* ActivityIndicatorNode.swift in Sources */, DB45FAD725CA6C76005A8AC7 /* UIBarButtonItem.swift in Sources */, 2DA504692601ADE7008F4E6C /* SawToothView.swift in Sources */, DBA465952696E387002B41DB /* AppPreference.swift in Sources */, @@ -4041,7 +4055,9 @@ DB03A795272A981400EE37C5 /* ContentSplitViewController.swift in Sources */, 2D152A8C25C295CC009AA50C /* StatusView.swift in Sources */, DBBC24DE26A54BCB00398BB9 /* MastodonMetricFormatter.swift in Sources */, + DB06180A2785B2AB0030EE79 /* MastodonRegisterAvatarTableViewCell.swift in Sources */, DBB3BA2A26A81C020004F2D4 /* FLAnimatedImageView.swift in Sources */, + DB0618032785A7100030EE79 /* RegisterSection.swift in Sources */, 2DE0FAC82615F5F000CDF649 /* SearchRecommendAccountsCollectionViewCell.swift in Sources */, DBF1D251269DB01200C1C08A /* SearchHistoryViewController.swift in Sources */, 2D42FF8525C8224F004A627A /* HitTestExpandedButton.swift in Sources */, @@ -4049,8 +4065,11 @@ DBB5250E2611EBAF002F1F29 /* ProfileSegmentedViewController.swift in Sources */, 2D42FF8F25C8228A004A627A /* UIButton.swift in Sources */, DBF156DF2701B17600EC00B7 /* SidebarAddAccountCollectionViewCell.swift in Sources */, + DB0617F1278413D00030EE79 /* PickServerServerSectionTableHeaderView.swift in Sources */, DB789A0B25F9F2950071ACA0 /* ComposeViewController.swift in Sources */, DB938F0926240F3C00E5B6C1 /* RemoteThreadViewModel.swift in Sources */, + DB0617ED277F02C50030EE79 /* OnboardingNavigationController.swift in Sources */, + DB0617F527855AB90030EE79 /* ServerRuleSection.swift in Sources */, DBBC24AE26A53DC100398BB9 /* ReplicaStatusView.swift in Sources */, DB75BF1E263C1C1B00EDBF1F /* CustomScheduler.swift in Sources */, 0FAA102725E1126A0017CCDE /* MastodonPickServerViewController.swift in Sources */, @@ -4091,12 +4110,9 @@ 2D939AB525EDD8A90076FA61 /* String.swift in Sources */, DB4481B925EE289600BEFB67 /* UITableView.swift in Sources */, DBE3CDBB261C427900430CC6 /* TimelineHeaderTableViewCell.swift in Sources */, - DBCBCBFF2680AE98000F5B51 /* AsyncHomeTimelineViewModel.swift in Sources */, 0FAA101C25E10E760017CCDE /* UIFont.swift in Sources */, 2D38F1D525CD465300561493 /* HomeTimelineViewController.swift in Sources */, - DBAC6497267DECCB007FE9FD /* TimelineMiddleLoaderNode.swift in Sources */, DB97131F2666078B00BD1E90 /* Date.swift in Sources */, - DBAC6485267D0F9E007FE9FD /* StatusNode.swift in Sources */, DB98338825C945ED00AD9700 /* Assets.swift in Sources */, DB6180E926391BDF0018D199 /* MediaHostToMediaPreviewViewControllerAnimatedTransitioning.swift in Sources */, DB6B351E2601FAEE00DC1E11 /* ComposeStatusAttachmentCollectionViewCell.swift in Sources */, @@ -4156,7 +4172,6 @@ DBA5E7A5263BD28C004598BB /* ContextMenuImagePreviewViewModel.swift in Sources */, DB9D7C21269824B80054B3DF /* APIService+Filter.swift in Sources */, 2D38F1E525CD46C100561493 /* HomeTimelineViewModel.swift in Sources */, - DBCBCC012680AF2A000F5B51 /* AsyncHomeTimelineViewModel+Diffable.swift in Sources */, DBCC3B36261440BA0045B23D /* UINavigationController.swift in Sources */, DBB525852612D6DD002F1F29 /* ProfileStatusDashboardMeterView.swift in Sources */, 2D84350525FF858100EECE90 /* UIScrollView.swift in Sources */, @@ -4166,7 +4181,7 @@ DB71FD3C25F8A1C500512AE1 /* APIService+Persist+PersistCache.swift in Sources */, 2DA6055125F74407006356F9 /* AudioContainerViewModel.swift in Sources */, DB4F0968269ED8AD00D62E92 /* SearchHistoryTableHeaderView.swift in Sources */, - 0FB3D2FE25E4CB6400AAD544 /* PickServerTitleCell.swift in Sources */, + 0FB3D2FE25E4CB6400AAD544 /* OnboardingHeadlineTableViewCell.swift in Sources */, 5DA732CC2629CEF500A92342 /* UIView+Remove.swift in Sources */, DBAEDE5C267A058D00D25FF5 /* BlurhashImageCacheService.swift in Sources */, 2D38F1DF25CD46A400561493 /* HomeTimelineViewController+Provider.swift in Sources */, @@ -4185,7 +4200,6 @@ 2D694A7425F9EB4E0038ADDC /* ContentWarningOverlayView.swift in Sources */, DB4932B126F1FB5300EF46D4 /* WizardCardView.swift in Sources */, DBAE3F682615DD60004B8251 /* UserProvider.swift in Sources */, - DBAC6488267D388B007FE9FD /* ASTableNode.swift in Sources */, DB6D9F76263587C7008423CD /* SettingFetchedResultController.swift in Sources */, DB9A486C26032AC1008B817C /* AttachmentContainerView+EmptyStateView.swift in Sources */, 5D0393902612D259007FE196 /* WebViewController.swift in Sources */, @@ -4201,6 +4215,7 @@ 2DF75BA125D0E29D00694EC8 /* StatusProvider+StatusTableViewCellDelegate.swift in Sources */, 5DF1056425F887CB00D6C0D4 /* AVPlayer.swift in Sources */, DBBF1DCB2652539E00E5B703 /* AutoCompleteItem.swift in Sources */, + DB84811727883C2600BBEABA /* MastodonRegisterPasswordHintTableViewCell.swift in Sources */, 2DA6054725F716A2006356F9 /* PlaybackState.swift in Sources */, DB35FC1F2612F1D9006193C9 /* ProfileRelationshipActionButton.swift in Sources */, DBC7A672260C897100E57475 /* StatusContentWarningEditorView.swift in Sources */, @@ -4212,7 +4227,9 @@ 2D34D9D126148D9E0081BFC0 /* APIService+Recommend.swift in Sources */, DBB525562611EDCA002F1F29 /* UserTimelineViewModel.swift in Sources */, 2D42FF7E25C82218004A627A /* ActionToolBarContainer.swift in Sources */, + DB0618012785732C0030EE79 /* ServerRulesTableViewCell.swift in Sources */, DB221B16260C395900AEFE46 /* CustomEmojiPickerInputViewModel.swift in Sources */, + DB0617EF277F12720030EE79 /* NavigationActionView.swift in Sources */, DB1FD43625F26899004CFCFC /* MastodonPickServerViewModel+LoadIndexedServerState.swift in Sources */, 2D939AE825EE1CF80076FA61 /* MastodonRegisterViewController+Avatar.swift in Sources */, 2D084B8D26258EA3003AA3AF /* NotificationViewModel+Diffable.swift in Sources */, @@ -4220,7 +4237,6 @@ DB1D186C25EF5BA7003F1F23 /* PollTableView.swift in Sources */, DBA94434265CBB5300C537E1 /* ProfileFieldSection.swift in Sources */, DBF156E42702DB3F00EC00B7 /* HandleTapAction.swift in Sources */, - DB023295267F0AB800031745 /* ASMetaEditableTextNode.swift in Sources */, 2D5981A125E4A593000FB903 /* MastodonConfirmEmailViewModel.swift in Sources */, DB6B74F6272FBCDB00C70B6E /* FollowerListViewModel+State.swift in Sources */, DB4F096C269EFA2000D62E92 /* SearchResultViewController+StatusProvider.swift in Sources */, @@ -4237,6 +4253,7 @@ DB6C8C0F25F0A6AE00AAA452 /* Mastodon+Entity+Error.swift in Sources */, DB1E346825F518E20079D7DF /* CategoryPickerSection.swift in Sources */, DB7274F4273BB9B200577D95 /* ListBatchFetchViewModel.swift in Sources */, + DB0618052785A73D0030EE79 /* RegisterItem.swift in Sources */, 2D61254D262547C200299647 /* APIService+Notification.swift in Sources */, DB040ED126538E3D00BEE9D8 /* Trie.swift in Sources */, DB73BF4B27140C0800781945 /* UITableViewDiffableDataSource.swift in Sources */, @@ -4248,7 +4265,6 @@ DB938EED2623F79B00E5B6C1 /* ThreadViewModel.swift in Sources */, DBBC24AC26A53D9300398BB9 /* ComposeStatusContentTableViewCell.swift in Sources */, DBC7A67C260DFADE00E57475 /* StatusPublishService.swift in Sources */, - DBCBCC092680B01B000F5B51 /* AsyncHomeTimelineViewModel+LoadMiddleState.swift in Sources */, 2DCB73FD2615C13900EC03D4 /* SearchRecommendCollectionHeader.swift in Sources */, DB852D1C26FB021500FC9D81 /* RootSplitViewController.swift in Sources */, DB02CDBF2625AE5000D0A2AF /* AdaptiveUserInterfaceStyleBarButtonItem.swift in Sources */, @@ -4264,7 +4280,6 @@ 0FB3D33825E6401400AAD544 /* PickServerCell.swift in Sources */, DB6D9F8426358EEC008423CD /* SettingsItem.swift in Sources */, 2D364F7825E66D8300204FDC /* MastodonResendEmailViewModel.swift in Sources */, - DBCBCC052680AFB9000F5B51 /* AsyncHomeTimelineViewController+Provider.swift in Sources */, DBA465932696B495002B41DB /* APIService+WebFinger.swift in Sources */, DB8AF54525C13647002E6C99 /* NeedsDependency.swift in Sources */, DB9D6BF825E4F5690051B173 /* NotificationViewController.swift in Sources */, @@ -4284,7 +4299,6 @@ DB6180F826391D660018D199 /* MediaPreviewingViewController.swift in Sources */, DB0140CF25C42AEE00F9F3CF /* OSLog.swift in Sources */, DB6B75022730060700C70B6E /* UserProviderFacade+UITableViewDelegate.swift in Sources */, - DB44384F25E8C1FA008912A2 /* CALayer.swift in Sources */, 2D34D9CB261489930081BFC0 /* SearchViewController+Recommend.swift in Sources */, DB71C7CB271D5A0300BE3819 /* LineChartView.swift in Sources */, DB938F1526241FDF00E5B6C1 /* APIService+Thread.swift in Sources */, @@ -4301,7 +4315,6 @@ 2DB72C8C262D764300CE6173 /* Mastodon+Entity+Notification+Type.swift in Sources */, 2D35237A26256D920031AF25 /* NotificationSection.swift in Sources */, DB084B5725CBC56C00F898ED /* Status.swift in Sources */, - DBCBCC072680AFEC000F5B51 /* AsyncHomeTimelineViewModel+LoadLatestState.swift in Sources */, 2D4AD89C263165B500613EFC /* SuggestionAccountCollectionViewCell.swift in Sources */, DB447691260B406600B66B82 /* CustomEmojiPickerItemCollectionViewCell.swift in Sources */, DB9282B225F3222800823B15 /* PickServerEmptyStateView.swift in Sources */, @@ -4326,7 +4339,6 @@ DB0C947226A7D2D70088FB11 /* AvatarButton.swift in Sources */, DB98336B25C9420100AD9700 /* APIService+App.swift in Sources */, DBFEF07B26A6BCE8006D7ED1 /* APIService+Status+Publish.swift in Sources */, - DBCBCC032680AF6E000F5B51 /* AsyncHomeTimelineViewController+DebugAction.swift in Sources */, DBA0A11325FB3FC10079C110 /* ComposeToolbarView.swift in Sources */, 2D32EADA25CBCC3300C9ED86 /* PublicTimelineViewModel+LoadMiddleState.swift in Sources */, 5B90C48526259BF10002E742 /* APIService+Subscriptions.swift in Sources */, @@ -4349,6 +4361,7 @@ DBE54AC62636C89F004E7C0B /* NotificationPreference.swift in Sources */, 2D5A3D2825CF8BC9002347D6 /* HomeTimelineViewModel+Diffable.swift in Sources */, DB98339C25C96DE600AD9700 /* APIService+Account.swift in Sources */, + DB0618072785A8880030EE79 /* MastodonRegisterViewModel+Diffable.swift in Sources */, DB6B74FC272FF55800C70B6E /* UserSection.swift in Sources */, 2DF75BA725D10E1000694EC8 /* APIService+Favorite.swift in Sources */, DB9D6C3825E508BE0051B173 /* Attachment.swift in Sources */, @@ -4359,7 +4372,6 @@ DB71C7CD271D7F4300BE3819 /* CurveAlgorithm.swift in Sources */, DBD376B2269302A4007FEC24 /* UITableViewCell.swift in Sources */, DB4F0966269ED52200D62E92 /* SearchResultViewModel.swift in Sources */, - DBAC6499267DF2C4007FE9FD /* TimelineBottomLoaderNode.swift in Sources */, 2D76318325C14E8F00929FB9 /* PublicTimelineViewModel+Diffable.swift in Sources */, DBBF1DBF2652401B00E5B703 /* AutoCompleteViewModel.swift in Sources */, DB6180FA26391F2E0018D199 /* MediaPreviewViewModel.swift in Sources */, @@ -4390,7 +4402,6 @@ DB4F097B26A039FF00D62E92 /* SearchHistorySection.swift in Sources */, DBB525302611EBF3002F1F29 /* ProfilePagingViewModel.swift in Sources */, DB9F58EC26EF435000E7BBE9 /* AccountViewController.swift in Sources */, - DBCBCC0B2680B03F000F5B51 /* AsyncHomeTimelineViewModel+LoadOldestState.swift in Sources */, 2D5A3D6225CFD9CB002347D6 /* HomeTimelineViewController+DebugAction.swift in Sources */, DB49A62525FF334C00B98345 /* EmojiService+CustomEmojiViewModel+LoadState.swift in Sources */, DB4924E226312AB200E9DB22 /* NotificationService.swift in Sources */, @@ -4402,6 +4413,7 @@ DB4932B326F2054200EF46D4 /* CircleAvatarButton.swift in Sources */, 0FB3D30825E524C600AAD544 /* PickServerCategoriesCell.swift in Sources */, 2D4AD8A226316CD200613EFC /* SelectedAccountSection.swift in Sources */, + DB0617EB277EF3820030EE79 /* GradientBorderView.swift in Sources */, DB789A1225F9F2CC0071ACA0 /* ComposeViewModel.swift in Sources */, DBB525362611ECEB002F1F29 /* UserTimelineViewController.swift in Sources */, DB6D1B3D2636857500ACB481 /* AppearancePreference.swift in Sources */, @@ -4409,7 +4421,6 @@ DB3667A4268AE2370027D07F /* ComposeStatusPollTableViewCell.swift in Sources */, DBBF1DC226524D2900E5B703 /* AutoCompleteTableViewCell.swift in Sources */, 2D38F1FE25CD481700561493 /* StatusProvider.swift in Sources */, - DB1EE7B2267F9525000CC337 /* StatusProvider+StatusNodeDelegate.swift in Sources */, 5B24BBE2262DB19100A9381B /* APIService+Report.swift in Sources */, DBF3B7412733EB9400E21627 /* MastodonLocalCode.swift in Sources */, DB4F096A269EDAD200D62E92 /* SearchResultViewModel+State.swift in Sources */, @@ -4430,6 +4441,7 @@ DBAFB7352645463500371D5F /* Emojis.swift in Sources */, DBCCC71E25F73297007E1AB6 /* APIService+Reblog.swift in Sources */, DBE3CE13261D7D4200430CC6 /* StatusTableViewControllerAspect.swift in Sources */, + DB0617FD27855BFE0030EE79 /* ServerRuleItem.swift in Sources */, 5BB04FD5262E7AFF0043BFF6 /* ReportViewController.swift in Sources */, DBAE3F942616E28B004B8251 /* APIService+Follow.swift in Sources */, ); @@ -4919,7 +4931,7 @@ CLANG_ENABLE_MODULES = YES; CODE_SIGN_ENTITLEMENTS = Mastodon/Mastodon.entitlements; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 88; + CURRENT_PROJECT_VERSION = 90; DEVELOPMENT_ASSET_PATHS = "Mastodon/Resources/Preview\\ Assets.xcassets"; DEVELOPMENT_TEAM = 5Z4GVSS33P; INFOPLIST_FILE = Mastodon/Info.plist; @@ -4934,7 +4946,7 @@ SWIFT_OBJC_BRIDGING_HEADER = "Mastodon/Vender/Mastodon-Bridging-Header.h"; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; SWIFT_VERSION = 5.0; - TARGETED_DEVICE_FAMILY = 1; + TARGETED_DEVICE_FAMILY = "1,2"; VERSIONING_SYSTEM = "apple-generic"; }; name = Debug; @@ -4948,7 +4960,7 @@ CLANG_ENABLE_MODULES = YES; CODE_SIGN_ENTITLEMENTS = Mastodon/Mastodon.entitlements; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 88; + CURRENT_PROJECT_VERSION = 90; DEVELOPMENT_ASSET_PATHS = "Mastodon/Resources/Preview\\ Assets.xcassets"; DEVELOPMENT_TEAM = 5Z4GVSS33P; INFOPLIST_FILE = Mastodon/Info.plist; @@ -4962,7 +4974,7 @@ PROVISIONING_PROFILE_SPECIFIER = ""; SWIFT_OBJC_BRIDGING_HEADER = "Mastodon/Vender/Mastodon-Bridging-Header.h"; SWIFT_VERSION = 5.0; - TARGETED_DEVICE_FAMILY = 1; + TARGETED_DEVICE_FAMILY = "1,2"; VERSIONING_SYSTEM = "apple-generic"; }; name = Release; @@ -5056,11 +5068,11 @@ APPLICATION_EXTENSION_API_ONLY = YES; CLANG_ENABLE_MODULES = YES; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 88; + CURRENT_PROJECT_VERSION = 90; DEFINES_MODULE = YES; DEVELOPMENT_TEAM = 5Z4GVSS33P; DYLIB_COMPATIBILITY_VERSION = 1; - DYLIB_CURRENT_VERSION = 88; + DYLIB_CURRENT_VERSION = 90; DYLIB_INSTALL_NAME_BASE = "@rpath"; INFOPLIST_FILE = AppShared/Info.plist; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; @@ -5087,11 +5099,11 @@ APPLICATION_EXTENSION_API_ONLY = YES; CLANG_ENABLE_MODULES = YES; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 88; + CURRENT_PROJECT_VERSION = 90; DEFINES_MODULE = YES; DEVELOPMENT_TEAM = 5Z4GVSS33P; DYLIB_COMPATIBILITY_VERSION = 1; - DYLIB_CURRENT_VERSION = 88; + DYLIB_CURRENT_VERSION = 90; DYLIB_INSTALL_NAME_BASE = "@rpath"; INFOPLIST_FILE = AppShared/Info.plist; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; @@ -5116,11 +5128,11 @@ APPLICATION_EXTENSION_API_ONLY = YES; CLANG_ENABLE_MODULES = YES; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 88; + CURRENT_PROJECT_VERSION = 90; DEFINES_MODULE = YES; DEVELOPMENT_TEAM = 5Z4GVSS33P; DYLIB_COMPATIBILITY_VERSION = 1; - DYLIB_CURRENT_VERSION = 88; + DYLIB_CURRENT_VERSION = 90; DYLIB_INSTALL_NAME_BASE = "@rpath"; INFOPLIST_FILE = CoreDataStack/Info.plist; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; @@ -5146,11 +5158,11 @@ APPLICATION_EXTENSION_API_ONLY = YES; CLANG_ENABLE_MODULES = YES; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 88; + CURRENT_PROJECT_VERSION = 90; DEFINES_MODULE = YES; DEVELOPMENT_TEAM = 5Z4GVSS33P; DYLIB_COMPATIBILITY_VERSION = 1; - DYLIB_CURRENT_VERSION = 88; + DYLIB_CURRENT_VERSION = 90; DYLIB_INSTALL_NAME_BASE = "@rpath"; INFOPLIST_FILE = CoreDataStack/Info.plist; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; @@ -5213,7 +5225,7 @@ buildSettings = { CODE_SIGN_ENTITLEMENTS = MastodonIntent/MastodonIntent.entitlements; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 88; + CURRENT_PROJECT_VERSION = 90; DEVELOPMENT_TEAM = 5Z4GVSS33P; INFOPLIST_FILE = MastodonIntent/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( @@ -5227,68 +5239,18 @@ SKIP_INSTALL = YES; SWIFT_ACTIVE_COMPILATION_CONDITIONS = "APP_EXTENSION $(inherited)"; SWIFT_VERSION = 5.0; - TARGETED_DEVICE_FAMILY = 1; + TARGETED_DEVICE_FAMILY = "1,2"; VERSIONING_SYSTEM = "apple-generic"; }; name = Debug; }; - DB8FABD126AEC7B2008E5AF4 /* ASDK - Debug */ = { - isa = XCBuildConfiguration; - baseConfigurationReference = C3789232A52F43529CA67E95 /* Pods-MastodonIntent.asdk - debug.xcconfig */; - buildSettings = { - CODE_SIGN_ENTITLEMENTS = MastodonIntent/MastodonIntent.entitlements; - CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 88; - DEVELOPMENT_TEAM = 5Z4GVSS33P; - INFOPLIST_FILE = MastodonIntent/Info.plist; - LD_RUNPATH_SEARCH_PATHS = ( - "$(inherited)", - "@executable_path/Frameworks", - "@executable_path/../../Frameworks", - ); - MARKETING_VERSION = 1.0.7; - PRODUCT_BUNDLE_IDENTIFIER = org.joinmastodon.app.MastodonIntent; - PRODUCT_NAME = "$(TARGET_NAME)"; - SKIP_INSTALL = YES; - SWIFT_ACTIVE_COMPILATION_CONDITIONS = "APP_EXTENSION $(inherited)"; - SWIFT_VERSION = 5.0; - TARGETED_DEVICE_FAMILY = 1; - VERSIONING_SYSTEM = "apple-generic"; - }; - name = "ASDK - Debug"; - }; - DB8FABD226AEC7B2008E5AF4 /* ASDK - Release */ = { - isa = XCBuildConfiguration; - baseConfigurationReference = F920AD4EC23B0D00F5CCA58E /* Pods-MastodonIntent.asdk - release.xcconfig */; - buildSettings = { - CODE_SIGN_ENTITLEMENTS = MastodonIntent/MastodonIntent.entitlements; - CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 88; - DEVELOPMENT_TEAM = 5Z4GVSS33P; - INFOPLIST_FILE = MastodonIntent/Info.plist; - LD_RUNPATH_SEARCH_PATHS = ( - "$(inherited)", - "@executable_path/Frameworks", - "@executable_path/../../Frameworks", - ); - MARKETING_VERSION = 1.0.7; - PRODUCT_BUNDLE_IDENTIFIER = org.joinmastodon.app.MastodonIntent; - PRODUCT_NAME = "$(TARGET_NAME)"; - SKIP_INSTALL = YES; - SWIFT_ACTIVE_COMPILATION_CONDITIONS = "APP_EXTENSION $(inherited)"; - SWIFT_VERSION = 5.0; - TARGETED_DEVICE_FAMILY = 1; - VERSIONING_SYSTEM = "apple-generic"; - }; - name = "ASDK - Release"; - }; DB8FABD326AEC7B2008E5AF4 /* Release */ = { isa = XCBuildConfiguration; baseConfigurationReference = 159AC43EFE0A1F95FCB358A4 /* Pods-MastodonIntent.release.xcconfig */; buildSettings = { CODE_SIGN_ENTITLEMENTS = MastodonIntent/MastodonIntent.entitlements; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 88; + CURRENT_PROJECT_VERSION = 90; DEVELOPMENT_TEAM = 5Z4GVSS33P; INFOPLIST_FILE = MastodonIntent/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( @@ -5302,7 +5264,7 @@ SKIP_INSTALL = YES; SWIFT_ACTIVE_COMPILATION_CONDITIONS = "APP_EXTENSION $(inherited)"; SWIFT_VERSION = 5.0; - TARGETED_DEVICE_FAMILY = 1; + TARGETED_DEVICE_FAMILY = "1,2"; VERSIONING_SYSTEM = "apple-generic"; }; name = Release; @@ -5313,7 +5275,7 @@ buildSettings = { CODE_SIGN_ENTITLEMENTS = ShareActionExtension/ShareActionExtension.entitlements; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 88; + CURRENT_PROJECT_VERSION = 90; DEVELOPMENT_TEAM = 5Z4GVSS33P; INFOPLIST_FILE = ShareActionExtension/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( @@ -5327,68 +5289,18 @@ SKIP_INSTALL = YES; SWIFT_ACTIVE_COMPILATION_CONDITIONS = "APP_EXTENSION $(inherited)"; SWIFT_VERSION = 5.0; - TARGETED_DEVICE_FAMILY = 1; + TARGETED_DEVICE_FAMILY = "1,2"; VERSIONING_SYSTEM = "apple-generic"; }; name = Debug; }; - DBC6461E26A170AB00B0E31B /* ASDK - Debug */ = { - isa = XCBuildConfiguration; - baseConfigurationReference = 6130CBE4B26E3C976ACC1688 /* Pods-ShareActionExtension.asdk - debug.xcconfig */; - buildSettings = { - CODE_SIGN_ENTITLEMENTS = ShareActionExtension/ShareActionExtension.entitlements; - CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 88; - DEVELOPMENT_TEAM = 5Z4GVSS33P; - INFOPLIST_FILE = ShareActionExtension/Info.plist; - LD_RUNPATH_SEARCH_PATHS = ( - "$(inherited)", - "@executable_path/Frameworks", - "@executable_path/../../Frameworks", - ); - MARKETING_VERSION = 1.0.7; - PRODUCT_BUNDLE_IDENTIFIER = org.joinmastodon.app.ShareActionExtension; - PRODUCT_NAME = "$(TARGET_NAME)"; - SKIP_INSTALL = YES; - SWIFT_ACTIVE_COMPILATION_CONDITIONS = "APP_EXTENSION $(inherited)"; - SWIFT_VERSION = 5.0; - TARGETED_DEVICE_FAMILY = 1; - VERSIONING_SYSTEM = "apple-generic"; - }; - name = "ASDK - Debug"; - }; - DBC6461F26A170AB00B0E31B /* ASDK - Release */ = { - isa = XCBuildConfiguration; - baseConfigurationReference = 5CE45680252519F42FEA2D13 /* Pods-ShareActionExtension.asdk - release.xcconfig */; - buildSettings = { - CODE_SIGN_ENTITLEMENTS = ShareActionExtension/ShareActionExtension.entitlements; - CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 88; - DEVELOPMENT_TEAM = 5Z4GVSS33P; - INFOPLIST_FILE = ShareActionExtension/Info.plist; - LD_RUNPATH_SEARCH_PATHS = ( - "$(inherited)", - "@executable_path/Frameworks", - "@executable_path/../../Frameworks", - ); - MARKETING_VERSION = 1.0.7; - PRODUCT_BUNDLE_IDENTIFIER = org.joinmastodon.app.ShareActionExtension; - PRODUCT_NAME = "$(TARGET_NAME)"; - SKIP_INSTALL = YES; - SWIFT_ACTIVE_COMPILATION_CONDITIONS = "APP_EXTENSION $(inherited)"; - SWIFT_VERSION = 5.0; - TARGETED_DEVICE_FAMILY = 1; - VERSIONING_SYSTEM = "apple-generic"; - }; - name = "ASDK - Release"; - }; DBC6462026A170AB00B0E31B /* Release */ = { isa = XCBuildConfiguration; baseConfigurationReference = 95AD0663479892A2109EEFD0 /* Pods-ShareActionExtension.release.xcconfig */; buildSettings = { CODE_SIGN_ENTITLEMENTS = ShareActionExtension/ShareActionExtension.entitlements; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 88; + CURRENT_PROJECT_VERSION = 90; DEVELOPMENT_TEAM = 5Z4GVSS33P; INFOPLIST_FILE = ShareActionExtension/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( @@ -5402,492 +5314,18 @@ SKIP_INSTALL = YES; SWIFT_ACTIVE_COMPILATION_CONDITIONS = "APP_EXTENSION $(inherited)"; SWIFT_VERSION = 5.0; - TARGETED_DEVICE_FAMILY = 1; + TARGETED_DEVICE_FAMILY = "1,2"; VERSIONING_SYSTEM = "apple-generic"; }; name = Release; }; - DBCBCC0E2680BE3E000F5B51 /* ASDK - Release */ = { - isa = XCBuildConfiguration; - buildSettings = { - ALWAYS_SEARCH_USER_PATHS = NO; - CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES; - CLANG_ANALYZER_NONNULL = YES; - CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; - CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; - CLANG_CXX_LIBRARY = "libc++"; - CLANG_ENABLE_MODULES = YES; - CLANG_ENABLE_OBJC_ARC = YES; - CLANG_ENABLE_OBJC_WEAK = YES; - CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; - CLANG_WARN_BOOL_CONVERSION = YES; - CLANG_WARN_COMMA = YES; - CLANG_WARN_CONSTANT_CONVERSION = YES; - CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; - CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; - CLANG_WARN_DOCUMENTATION_COMMENTS = YES; - CLANG_WARN_EMPTY_BODY = YES; - CLANG_WARN_ENUM_CONVERSION = YES; - CLANG_WARN_INFINITE_RECURSION = YES; - CLANG_WARN_INT_CONVERSION = YES; - CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; - CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; - CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; - CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; - CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; - CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; - CLANG_WARN_STRICT_PROTOTYPES = YES; - CLANG_WARN_SUSPICIOUS_MOVE = YES; - CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; - CLANG_WARN_UNREACHABLE_CODE = YES; - CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; - COPY_PHASE_STRIP = NO; - DEBUG_INFORMATION_FORMAT = dwarf; - ENABLE_STRICT_OBJC_MSGSEND = YES; - ENABLE_TESTABILITY = YES; - GCC_C_LANGUAGE_STANDARD = gnu11; - GCC_DYNAMIC_NO_PIC = NO; - GCC_NO_COMMON_BLOCKS = YES; - GCC_OPTIMIZATION_LEVEL = 0; - GCC_PREPROCESSOR_DEFINITIONS = ( - "DEBUG=1", - "$(inherited)", - ); - GCC_WARN_64_TO_32_BIT_CONVERSION = YES; - GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; - GCC_WARN_UNDECLARED_SELECTOR = YES; - GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; - GCC_WARN_UNUSED_FUNCTION = YES; - GCC_WARN_UNUSED_VARIABLE = YES; - INTENTS_CODEGEN_LANGUAGE = Swift; - IPHONEOS_DEPLOYMENT_TARGET = 14.0; - MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; - MTL_FAST_MATH = YES; - ONLY_ACTIVE_ARCH = YES; - SDKROOT = iphoneos; - SWIFT_ACTIVE_COMPILATION_CONDITIONS = ASDK; - SWIFT_OPTIMIZATION_LEVEL = "-Onone"; - }; - name = "ASDK - Release"; - }; - DBCBCC0F2680BE3E000F5B51 /* ASDK - Release */ = { - isa = XCBuildConfiguration; - baseConfigurationReference = BD7598A87F4497045EDEF252 /* Pods-Mastodon.asdk - release.xcconfig */; - buildSettings = { - ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; - ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; - CLANG_ENABLE_MODULES = YES; - CODE_SIGN_ENTITLEMENTS = Mastodon/Mastodon.entitlements; - CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 88; - DEVELOPMENT_ASSET_PATHS = "Mastodon/Resources/Preview\\ Assets.xcassets"; - DEVELOPMENT_TEAM = 5Z4GVSS33P; - INFOPLIST_FILE = Mastodon/Info.plist; - LD_RUNPATH_SEARCH_PATHS = ( - "$(inherited)", - "@executable_path/Frameworks", - ); - MARKETING_VERSION = 1.0.7; - PRODUCT_BUNDLE_IDENTIFIER = org.joinmastodon.app; - PRODUCT_NAME = "$(TARGET_NAME)"; - PROVISIONING_PROFILE_SPECIFIER = ""; - SWIFT_OBJC_BRIDGING_HEADER = "Mastodon/Vender/Mastodon-Bridging-Header.h"; - SWIFT_OPTIMIZATION_LEVEL = "-Onone"; - SWIFT_VERSION = 5.0; - TARGETED_DEVICE_FAMILY = 1; - VERSIONING_SYSTEM = "apple-generic"; - }; - name = "ASDK - Release"; - }; - DBCBCC102680BE3E000F5B51 /* ASDK - Release */ = { - isa = XCBuildConfiguration; - baseConfigurationReference = 46DAB0EBDDFB678347CD96FF /* Pods-MastodonTests.asdk - release.xcconfig */; - buildSettings = { - BUNDLE_LOADER = "$(TEST_HOST)"; - CODE_SIGN_STYLE = Automatic; - DEVELOPMENT_TEAM = 5Z4GVSS33P; - INFOPLIST_FILE = MastodonTests/Info.plist; - LD_RUNPATH_SEARCH_PATHS = ( - "$(inherited)", - "@executable_path/Frameworks", - "@loader_path/Frameworks", - ); - PRODUCT_BUNDLE_IDENTIFIER = org.joinmastodon.MastodonTests; - PRODUCT_NAME = "$(TARGET_NAME)"; - SWIFT_VERSION = 5.0; - TARGETED_DEVICE_FAMILY = "1,2"; - TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Mastodon.app/Mastodon"; - }; - name = "ASDK - Release"; - }; - DBCBCC112680BE3E000F5B51 /* ASDK - Release */ = { - isa = XCBuildConfiguration; - baseConfigurationReference = 8850E70A1D5FF51432E43653 /* Pods-Mastodon-MastodonUITests.asdk - release.xcconfig */; - buildSettings = { - CODE_SIGN_STYLE = Automatic; - DEVELOPMENT_TEAM = 5Z4GVSS33P; - INFOPLIST_FILE = MastodonUITests/Info.plist; - LD_RUNPATH_SEARCH_PATHS = ( - "$(inherited)", - "@executable_path/Frameworks", - "@loader_path/Frameworks", - ); - PRODUCT_BUNDLE_IDENTIFIER = org.joinmastodon.MastodonUITests; - PRODUCT_NAME = "$(TARGET_NAME)"; - SWIFT_VERSION = 5.0; - TARGETED_DEVICE_FAMILY = "1,2"; - TEST_TARGET_NAME = Mastodon; - }; - name = "ASDK - Release"; - }; - DBCBCC122680BE3E000F5B51 /* ASDK - Release */ = { - isa = XCBuildConfiguration; - buildSettings = { - APPLICATION_EXTENSION_API_ONLY = YES; - CLANG_ENABLE_MODULES = YES; - CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 88; - DEFINES_MODULE = YES; - DEVELOPMENT_TEAM = 5Z4GVSS33P; - DYLIB_COMPATIBILITY_VERSION = 1; - DYLIB_CURRENT_VERSION = 88; - DYLIB_INSTALL_NAME_BASE = "@rpath"; - INFOPLIST_FILE = CoreDataStack/Info.plist; - INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; - LD_RUNPATH_SEARCH_PATHS = ( - "$(inherited)", - "@executable_path/Frameworks", - "@loader_path/Frameworks", - ); - PRODUCT_BUNDLE_IDENTIFIER = org.joinmastodon.CoreDataStack; - PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; - SKIP_INSTALL = YES; - SWIFT_OPTIMIZATION_LEVEL = "-Onone"; - SWIFT_VERSION = 5.0; - TARGETED_DEVICE_FAMILY = "1,2"; - VERSIONING_SYSTEM = "apple-generic"; - VERSION_INFO_PREFIX = ""; - }; - name = "ASDK - Release"; - }; - DBCBCC132680BE3E000F5B51 /* ASDK - Release */ = { - isa = XCBuildConfiguration; - buildSettings = { - CODE_SIGN_STYLE = Automatic; - DEVELOPMENT_TEAM = 5Z4GVSS33P; - INFOPLIST_FILE = CoreDataStackTests/Info.plist; - LD_RUNPATH_SEARCH_PATHS = ( - "$(inherited)", - "@executable_path/Frameworks", - "@loader_path/Frameworks", - ); - PRODUCT_BUNDLE_IDENTIFIER = org.joinmastodon.CoreDataStackTests; - PRODUCT_NAME = "$(TARGET_NAME)"; - SWIFT_VERSION = 5.0; - TARGETED_DEVICE_FAMILY = "1,2"; - TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Mastodon.app/Mastodon"; - }; - name = "ASDK - Release"; - }; - DBCBCC142680BE3E000F5B51 /* ASDK - Release */ = { - isa = XCBuildConfiguration; - baseConfigurationReference = 9CFF58FD900AC059428700E7 /* Pods-NotificationService.asdk - release.xcconfig */; - buildSettings = { - CODE_SIGN_ENTITLEMENTS = NotificationService/NotificationService.entitlements; - CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 88; - DEVELOPMENT_TEAM = 5Z4GVSS33P; - INFOPLIST_FILE = NotificationService/Info.plist; - LD_RUNPATH_SEARCH_PATHS = ( - "$(inherited)", - "@executable_path/Frameworks", - "@executable_path/../../Frameworks", - ); - MARKETING_VERSION = 1.0.7; - PRODUCT_BUNDLE_IDENTIFIER = org.joinmastodon.app.NotificationService; - PRODUCT_NAME = "$(TARGET_NAME)"; - SKIP_INSTALL = YES; - SWIFT_VERSION = 5.0; - TARGETED_DEVICE_FAMILY = 1; - VERSIONING_SYSTEM = "apple-generic"; - }; - name = "ASDK - Release"; - }; - DBCBCC152680BE3E000F5B51 /* ASDK - Release */ = { - isa = XCBuildConfiguration; - baseConfigurationReference = DDB1B139FA8EA26F510D58B6 /* Pods-AppShared.asdk - release.xcconfig */; - buildSettings = { - APPLICATION_EXTENSION_API_ONLY = YES; - CLANG_ENABLE_MODULES = YES; - CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 88; - DEFINES_MODULE = YES; - DEVELOPMENT_TEAM = 5Z4GVSS33P; - DYLIB_COMPATIBILITY_VERSION = 1; - DYLIB_CURRENT_VERSION = 88; - DYLIB_INSTALL_NAME_BASE = "@rpath"; - INFOPLIST_FILE = AppShared/Info.plist; - INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; - LD_RUNPATH_SEARCH_PATHS = ( - "$(inherited)", - "@executable_path/Frameworks", - "@loader_path/Frameworks", - ); - PRODUCT_BUNDLE_IDENTIFIER = org.joinmastodon.app.AppShared; - PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; - SKIP_INSTALL = YES; - SWIFT_OPTIMIZATION_LEVEL = "-Onone"; - SWIFT_VERSION = 5.0; - TARGETED_DEVICE_FAMILY = "1,2"; - VERSIONING_SYSTEM = "apple-generic"; - VERSION_INFO_PREFIX = ""; - }; - name = "ASDK - Release"; - }; - DBCBCC1E26818F6F000F5B51 /* ASDK - Debug */ = { - isa = XCBuildConfiguration; - buildSettings = { - ALWAYS_SEARCH_USER_PATHS = NO; - CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES; - CLANG_ANALYZER_NONNULL = YES; - CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; - CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; - CLANG_CXX_LIBRARY = "libc++"; - CLANG_ENABLE_MODULES = YES; - CLANG_ENABLE_OBJC_ARC = YES; - CLANG_ENABLE_OBJC_WEAK = YES; - CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; - CLANG_WARN_BOOL_CONVERSION = YES; - CLANG_WARN_COMMA = YES; - CLANG_WARN_CONSTANT_CONVERSION = YES; - CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; - CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; - CLANG_WARN_DOCUMENTATION_COMMENTS = YES; - CLANG_WARN_EMPTY_BODY = YES; - CLANG_WARN_ENUM_CONVERSION = YES; - CLANG_WARN_INFINITE_RECURSION = YES; - CLANG_WARN_INT_CONVERSION = YES; - CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; - CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; - CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; - CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; - CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; - CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; - CLANG_WARN_STRICT_PROTOTYPES = YES; - CLANG_WARN_SUSPICIOUS_MOVE = YES; - CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; - CLANG_WARN_UNREACHABLE_CODE = YES; - CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; - COPY_PHASE_STRIP = NO; - DEBUG_INFORMATION_FORMAT = dwarf; - ENABLE_STRICT_OBJC_MSGSEND = YES; - ENABLE_TESTABILITY = YES; - GCC_C_LANGUAGE_STANDARD = gnu11; - GCC_DYNAMIC_NO_PIC = NO; - GCC_NO_COMMON_BLOCKS = YES; - GCC_OPTIMIZATION_LEVEL = 0; - GCC_PREPROCESSOR_DEFINITIONS = ( - "DEBUG=1", - "$(inherited)", - ); - GCC_WARN_64_TO_32_BIT_CONVERSION = YES; - GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; - GCC_WARN_UNDECLARED_SELECTOR = YES; - GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; - GCC_WARN_UNUSED_FUNCTION = YES; - GCC_WARN_UNUSED_VARIABLE = YES; - INTENTS_CODEGEN_LANGUAGE = Swift; - IPHONEOS_DEPLOYMENT_TARGET = 14.0; - MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; - MTL_FAST_MATH = YES; - ONLY_ACTIVE_ARCH = YES; - SDKROOT = iphoneos; - SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG ASDK"; - SWIFT_OPTIMIZATION_LEVEL = "-Onone"; - }; - name = "ASDK - Debug"; - }; - DBCBCC1F26818F6F000F5B51 /* ASDK - Debug */ = { - isa = XCBuildConfiguration; - baseConfigurationReference = 1D6D967E77A5357E2C6110D9 /* Pods-Mastodon.asdk - debug.xcconfig */; - buildSettings = { - ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; - ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; - CLANG_ENABLE_MODULES = YES; - CODE_SIGN_ENTITLEMENTS = Mastodon/Mastodon.entitlements; - CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 88; - DEVELOPMENT_ASSET_PATHS = "Mastodon/Resources/Preview\\ Assets.xcassets"; - DEVELOPMENT_TEAM = 5Z4GVSS33P; - INFOPLIST_FILE = Mastodon/Info.plist; - LD_RUNPATH_SEARCH_PATHS = ( - "$(inherited)", - "@executable_path/Frameworks", - ); - MARKETING_VERSION = 1.0.7; - PRODUCT_BUNDLE_IDENTIFIER = org.joinmastodon.app; - PRODUCT_NAME = "$(TARGET_NAME)"; - PROVISIONING_PROFILE_SPECIFIER = ""; - SWIFT_OBJC_BRIDGING_HEADER = "Mastodon/Vender/Mastodon-Bridging-Header.h"; - SWIFT_OPTIMIZATION_LEVEL = "-Onone"; - SWIFT_VERSION = 5.0; - TARGETED_DEVICE_FAMILY = 1; - VERSIONING_SYSTEM = "apple-generic"; - }; - name = "ASDK - Debug"; - }; - DBCBCC2026818F6F000F5B51 /* ASDK - Debug */ = { - isa = XCBuildConfiguration; - baseConfigurationReference = 7CEFFAE9AF9284B13C0A758D /* Pods-MastodonTests.asdk - debug.xcconfig */; - buildSettings = { - BUNDLE_LOADER = "$(TEST_HOST)"; - CODE_SIGN_STYLE = Automatic; - DEVELOPMENT_TEAM = 5Z4GVSS33P; - INFOPLIST_FILE = MastodonTests/Info.plist; - LD_RUNPATH_SEARCH_PATHS = ( - "$(inherited)", - "@executable_path/Frameworks", - "@loader_path/Frameworks", - ); - PRODUCT_BUNDLE_IDENTIFIER = org.joinmastodon.MastodonTests; - PRODUCT_NAME = "$(TARGET_NAME)"; - SWIFT_VERSION = 5.0; - TARGETED_DEVICE_FAMILY = "1,2"; - TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Mastodon.app/Mastodon"; - }; - name = "ASDK - Debug"; - }; - DBCBCC2126818F6F000F5B51 /* ASDK - Debug */ = { - isa = XCBuildConfiguration; - baseConfigurationReference = E5C7236E58D14A0322FE00F2 /* Pods-Mastodon-MastodonUITests.asdk - debug.xcconfig */; - buildSettings = { - CODE_SIGN_STYLE = Automatic; - DEVELOPMENT_TEAM = 5Z4GVSS33P; - INFOPLIST_FILE = MastodonUITests/Info.plist; - LD_RUNPATH_SEARCH_PATHS = ( - "$(inherited)", - "@executable_path/Frameworks", - "@loader_path/Frameworks", - ); - PRODUCT_BUNDLE_IDENTIFIER = org.joinmastodon.MastodonUITests; - PRODUCT_NAME = "$(TARGET_NAME)"; - SWIFT_VERSION = 5.0; - TARGETED_DEVICE_FAMILY = "1,2"; - TEST_TARGET_NAME = Mastodon; - }; - name = "ASDK - Debug"; - }; - DBCBCC2226818F6F000F5B51 /* ASDK - Debug */ = { - isa = XCBuildConfiguration; - buildSettings = { - APPLICATION_EXTENSION_API_ONLY = YES; - CLANG_ENABLE_MODULES = YES; - CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 88; - DEFINES_MODULE = YES; - DEVELOPMENT_TEAM = 5Z4GVSS33P; - DYLIB_COMPATIBILITY_VERSION = 1; - DYLIB_CURRENT_VERSION = 88; - DYLIB_INSTALL_NAME_BASE = "@rpath"; - INFOPLIST_FILE = CoreDataStack/Info.plist; - INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; - LD_RUNPATH_SEARCH_PATHS = ( - "$(inherited)", - "@executable_path/Frameworks", - "@loader_path/Frameworks", - ); - PRODUCT_BUNDLE_IDENTIFIER = org.joinmastodon.CoreDataStack; - PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; - SKIP_INSTALL = YES; - SWIFT_OPTIMIZATION_LEVEL = "-Onone"; - SWIFT_VERSION = 5.0; - TARGETED_DEVICE_FAMILY = "1,2"; - VERSIONING_SYSTEM = "apple-generic"; - VERSION_INFO_PREFIX = ""; - }; - name = "ASDK - Debug"; - }; - DBCBCC2326818F6F000F5B51 /* ASDK - Debug */ = { - isa = XCBuildConfiguration; - buildSettings = { - CODE_SIGN_STYLE = Automatic; - DEVELOPMENT_TEAM = 5Z4GVSS33P; - INFOPLIST_FILE = CoreDataStackTests/Info.plist; - LD_RUNPATH_SEARCH_PATHS = ( - "$(inherited)", - "@executable_path/Frameworks", - "@loader_path/Frameworks", - ); - PRODUCT_BUNDLE_IDENTIFIER = org.joinmastodon.CoreDataStackTests; - PRODUCT_NAME = "$(TARGET_NAME)"; - SWIFT_VERSION = 5.0; - TARGETED_DEVICE_FAMILY = "1,2"; - TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Mastodon.app/Mastodon"; - }; - name = "ASDK - Debug"; - }; - DBCBCC2426818F6F000F5B51 /* ASDK - Debug */ = { - isa = XCBuildConfiguration; - baseConfigurationReference = 3B7FD8F28DDA8FBCE5562B78 /* Pods-NotificationService.asdk - debug.xcconfig */; - buildSettings = { - CODE_SIGN_ENTITLEMENTS = NotificationService/NotificationService.entitlements; - CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 88; - DEVELOPMENT_TEAM = 5Z4GVSS33P; - INFOPLIST_FILE = NotificationService/Info.plist; - LD_RUNPATH_SEARCH_PATHS = ( - "$(inherited)", - "@executable_path/Frameworks", - "@executable_path/../../Frameworks", - ); - MARKETING_VERSION = 1.0.7; - PRODUCT_BUNDLE_IDENTIFIER = org.joinmastodon.app.NotificationService; - PRODUCT_NAME = "$(TARGET_NAME)"; - SKIP_INSTALL = YES; - SWIFT_VERSION = 5.0; - TARGETED_DEVICE_FAMILY = 1; - VERSIONING_SYSTEM = "apple-generic"; - }; - name = "ASDK - Debug"; - }; - DBCBCC2526818F6F000F5B51 /* ASDK - Debug */ = { - isa = XCBuildConfiguration; - baseConfigurationReference = A9B1FB898DFD6063B044298C /* Pods-AppShared.asdk - debug.xcconfig */; - buildSettings = { - APPLICATION_EXTENSION_API_ONLY = YES; - CLANG_ENABLE_MODULES = YES; - CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 88; - DEFINES_MODULE = YES; - DEVELOPMENT_TEAM = 5Z4GVSS33P; - DYLIB_COMPATIBILITY_VERSION = 1; - DYLIB_CURRENT_VERSION = 88; - DYLIB_INSTALL_NAME_BASE = "@rpath"; - INFOPLIST_FILE = AppShared/Info.plist; - INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; - LD_RUNPATH_SEARCH_PATHS = ( - "$(inherited)", - "@executable_path/Frameworks", - "@loader_path/Frameworks", - ); - PRODUCT_BUNDLE_IDENTIFIER = org.joinmastodon.app.AppShared; - PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; - SKIP_INSTALL = YES; - SWIFT_OPTIMIZATION_LEVEL = "-Onone"; - SWIFT_VERSION = 5.0; - TARGETED_DEVICE_FAMILY = "1,2"; - VERSIONING_SYSTEM = "apple-generic"; - VERSION_INFO_PREFIX = ""; - }; - name = "ASDK - Debug"; - }; DBF8AE1C263293E400C9C23C /* Debug */ = { isa = XCBuildConfiguration; baseConfigurationReference = 9553C689FFA9EBC880CAB78D /* Pods-NotificationService.debug.xcconfig */; buildSettings = { CODE_SIGN_ENTITLEMENTS = NotificationService/NotificationService.entitlements; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 88; + CURRENT_PROJECT_VERSION = 90; DEVELOPMENT_TEAM = 5Z4GVSS33P; INFOPLIST_FILE = NotificationService/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( @@ -5900,7 +5338,7 @@ PRODUCT_NAME = "$(TARGET_NAME)"; SKIP_INSTALL = YES; SWIFT_VERSION = 5.0; - TARGETED_DEVICE_FAMILY = 1; + TARGETED_DEVICE_FAMILY = "1,2"; VERSIONING_SYSTEM = "apple-generic"; }; name = Debug; @@ -5911,7 +5349,7 @@ buildSettings = { CODE_SIGN_ENTITLEMENTS = NotificationService/NotificationService.entitlements; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 88; + CURRENT_PROJECT_VERSION = 90; DEVELOPMENT_TEAM = 5Z4GVSS33P; INFOPLIST_FILE = NotificationService/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( @@ -5924,7 +5362,7 @@ PRODUCT_NAME = "$(TARGET_NAME)"; SKIP_INSTALL = YES; SWIFT_VERSION = 5.0; - TARGETED_DEVICE_FAMILY = 1; + TARGETED_DEVICE_FAMILY = "1,2"; VERSIONING_SYSTEM = "apple-generic"; }; name = Release; @@ -5936,8 +5374,6 @@ isa = XCConfigurationList; buildConfigurations = ( DB427DFA25BAA00100D1B89D /* Debug */, - DBCBCC1E26818F6F000F5B51 /* ASDK - Debug */, - DBCBCC0E2680BE3E000F5B51 /* ASDK - Release */, DB427DFB25BAA00100D1B89D /* Release */, ); defaultConfigurationIsVisible = 0; @@ -5947,8 +5383,6 @@ isa = XCConfigurationList; buildConfigurations = ( DB427DFD25BAA00100D1B89D /* Debug */, - DBCBCC1F26818F6F000F5B51 /* ASDK - Debug */, - DBCBCC0F2680BE3E000F5B51 /* ASDK - Release */, DB427DFE25BAA00100D1B89D /* Release */, ); defaultConfigurationIsVisible = 0; @@ -5958,8 +5392,6 @@ isa = XCConfigurationList; buildConfigurations = ( DB427E0025BAA00100D1B89D /* Debug */, - DBCBCC2026818F6F000F5B51 /* ASDK - Debug */, - DBCBCC102680BE3E000F5B51 /* ASDK - Release */, DB427E0125BAA00100D1B89D /* Release */, ); defaultConfigurationIsVisible = 0; @@ -5969,8 +5401,6 @@ isa = XCConfigurationList; buildConfigurations = ( DB427E0325BAA00100D1B89D /* Debug */, - DBCBCC2126818F6F000F5B51 /* ASDK - Debug */, - DBCBCC112680BE3E000F5B51 /* ASDK - Release */, DB427E0425BAA00100D1B89D /* Release */, ); defaultConfigurationIsVisible = 0; @@ -5980,8 +5410,6 @@ isa = XCConfigurationList; buildConfigurations = ( DB6804892637CD4C00430867 /* Debug */, - DBCBCC2526818F6F000F5B51 /* ASDK - Debug */, - DBCBCC152680BE3E000F5B51 /* ASDK - Release */, DB68048A2637CD4C00430867 /* Release */, ); defaultConfigurationIsVisible = 0; @@ -5991,8 +5419,6 @@ isa = XCConfigurationList; buildConfigurations = ( DB89BA0625C10FD0008580ED /* Debug */, - DBCBCC2226818F6F000F5B51 /* ASDK - Debug */, - DBCBCC122680BE3E000F5B51 /* ASDK - Release */, DB89BA0725C10FD0008580ED /* Release */, ); defaultConfigurationIsVisible = 0; @@ -6002,8 +5428,6 @@ isa = XCConfigurationList; buildConfigurations = ( DB89BA0A25C10FD0008580ED /* Debug */, - DBCBCC2326818F6F000F5B51 /* ASDK - Debug */, - DBCBCC132680BE3E000F5B51 /* ASDK - Release */, DB89BA0B25C10FD0008580ED /* Release */, ); defaultConfigurationIsVisible = 0; @@ -6013,8 +5437,6 @@ isa = XCConfigurationList; buildConfigurations = ( DB8FABD026AEC7B2008E5AF4 /* Debug */, - DB8FABD126AEC7B2008E5AF4 /* ASDK - Debug */, - DB8FABD226AEC7B2008E5AF4 /* ASDK - Release */, DB8FABD326AEC7B2008E5AF4 /* Release */, ); defaultConfigurationIsVisible = 0; @@ -6024,8 +5446,6 @@ isa = XCConfigurationList; buildConfigurations = ( DBC6461D26A170AB00B0E31B /* Debug */, - DBC6461E26A170AB00B0E31B /* ASDK - Debug */, - DBC6461F26A170AB00B0E31B /* ASDK - Release */, DBC6462026A170AB00B0E31B /* Release */, ); defaultConfigurationIsVisible = 0; @@ -6035,8 +5455,6 @@ isa = XCConfigurationList; buildConfigurations = ( DBF8AE1C263293E400C9C23C /* Debug */, - DBCBCC2426818F6F000F5B51 /* ASDK - Debug */, - DBCBCC142680BE3E000F5B51 /* ASDK - Release */, DBF8AE1D263293E400C9C23C /* Release */, ); defaultConfigurationIsVisible = 0; diff --git a/Mastodon.xcodeproj/xcuserdata/mainasuk.xcuserdatad/xcschemes/xcschememanagement.plist b/Mastodon.xcodeproj/xcuserdata/mainasuk.xcuserdatad/xcschemes/xcschememanagement.plist index 5c99e944..56e26925 100644 --- a/Mastodon.xcodeproj/xcuserdata/mainasuk.xcuserdatad/xcschemes/xcschememanagement.plist +++ b/Mastodon.xcodeproj/xcuserdata/mainasuk.xcuserdatad/xcschemes/xcschememanagement.plist @@ -7,17 +7,17 @@ AppShared.xcscheme_^#shared#^_ orderHint - 44 + 26 CoreDataStack.xcscheme_^#shared#^_ orderHint - 45 + 27 Mastodon - ASDK.xcscheme_^#shared#^_ orderHint - 4 + 2 Mastodon - RTL.xcscheme_^#shared#^_ @@ -27,7 +27,7 @@ Mastodon - Release.xcscheme_^#shared#^_ orderHint - 3 + 1 Mastodon - ar.xcscheme_^#shared#^_ @@ -102,7 +102,7 @@ MastodonIntent.xcscheme_^#shared#^_ orderHint - 43 + 25 MastodonIntents.xcscheme_^#shared#^_ @@ -117,12 +117,12 @@ NotificationService.xcscheme_^#shared#^_ orderHint - 7 + 3 ShareActionExtension.xcscheme_^#shared#^_ orderHint - 42 + 24 SuppressBuildableAutocreation diff --git a/Mastodon.xcworkspace/xcshareddata/swiftpm/Package.resolved b/Mastodon.xcworkspace/xcshareddata/swiftpm/Package.resolved index 11dde726..e52bb1d9 100644 --- a/Mastodon.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/Mastodon.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -6,8 +6,8 @@ "repositoryURL": "https://github.com/Alamofire/Alamofire.git", "state": { "branch": null, - "revision": "d120af1e8638c7da36c8481fd61a66c0c08dc4fc", - "version": "5.4.4" + "revision": "f82c23a8a7ef8dc1a49a8bfc6a96883e79121864", + "version": "5.5.0" } }, { @@ -141,8 +141,8 @@ "repositoryURL": "https://github.com/SDWebImage/SDWebImage.git", "state": { "branch": null, - "revision": "a72df4849408da7e5d3c1b586797b7c601c41d1b", - "version": "5.12.1" + "revision": "0fff0d7505b5306348263ea64fcc561253bbeb21", + "version": "5.12.2" } }, { diff --git a/Mastodon/Coordinator/SceneCoordinator.swift b/Mastodon/Coordinator/SceneCoordinator.swift index 9fbb2b77..49504fd1 100644 --- a/Mastodon/Coordinator/SceneCoordinator.swift +++ b/Mastodon/Coordinator/SceneCoordinator.swift @@ -157,11 +157,6 @@ extension SceneCoordinator { case mastodonConfirmEmail(viewModel: MastodonConfirmEmailViewModel) case mastodonResendEmail(viewModel: MastodonResendEmailViewModel) case mastodonWebView(viewModel:WebViewModel) - - #if ASDK - // ASDK - case asyncHome - #endif // search case searchDetail(viewModel: SearchDetailViewModel) @@ -260,7 +255,7 @@ extension SceneCoordinator { DispatchQueue.main.async { self.present( scene: .welcome, - from: nil, + from: self.sceneDelegate.window?.rootViewController, transition: .modal(animated: animated, completion: nil) ) } @@ -311,7 +306,7 @@ extension SceneCoordinator { case .modal(let animated, let completion): let modalNavigationController: UINavigationController = { if scene.isOnboarding { - return AdaptiveStatusBarStyleNavigationController(rootViewController: viewController) + return OnboardingNavigationController(rootViewController: viewController) } else { return UINavigationController(rootViewController: viewController) } @@ -412,11 +407,6 @@ private extension SceneCoordinator { let _viewController = WebViewController() _viewController.viewModel = viewModel viewController = _viewController - #if ASDK - case .asyncHome: - let _viewController = AsyncHomeTimelineViewController() - viewController = _viewController - #endif case .searchDetail(let viewModel): let _viewController = SearchDetailViewController() _viewController.viewModel = viewModel diff --git a/Mastodon/Deprecated/PickServerCategoriesCell.swift b/Mastodon/Deprecated/PickServerCategoriesCell.swift new file mode 100644 index 00000000..b2ca1cc7 --- /dev/null +++ b/Mastodon/Deprecated/PickServerCategoriesCell.swift @@ -0,0 +1,145 @@ +// +// PickServerCategoriesCell.swift +// Mastodon +// +// Created by BradGao on 2021/2/23. +// + +//import os.log +//import UIKit +//import MastodonSDK +// +//protocol PickServerCategoriesCellDelegate: AnyObject { +// func pickServerCategoriesCell(_ cell: PickServerCategoriesCell, collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) +//} +// +//final class PickServerCategoriesCell: UITableViewCell { +// +// weak var delegate: PickServerCategoriesCellDelegate? +// +// var diffableDataSource: UICollectionViewDiffableDataSource? +// +// let metricView = UIView() +// +// let collectionView: UICollectionView = { +// let flowLayout = UICollectionViewFlowLayout() +// flowLayout.scrollDirection = .horizontal +// let view = ControlContainableCollectionView(frame: .zero, collectionViewLayout: flowLayout) +// view.register(PickServerCategoryCollectionViewCell.self, forCellWithReuseIdentifier: String(describing: PickServerCategoryCollectionViewCell.self)) +// view.backgroundColor = .clear +// view.showsHorizontalScrollIndicator = false +// view.showsVerticalScrollIndicator = false +// view.layer.masksToBounds = false +// view.translatesAutoresizingMaskIntoConstraints = false +// return view +// }() +// +// override func prepareForReuse() { +// super.prepareForReuse() +// +// delegate = nil +// } +// +// override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { +// super.init(style: style, reuseIdentifier: reuseIdentifier) +// _init() +// } +// +// required init?(coder: NSCoder) { +// super.init(coder: coder) +// _init() +// } +//} +// +//extension PickServerCategoriesCell { +// +// private func _init() { +// selectionStyle = .none +// backgroundColor = Asset.Theme.Mastodon.systemGroupedBackground.color +// configureMargin() +// +// metricView.translatesAutoresizingMaskIntoConstraints = false +// contentView.addSubview(metricView) +// NSLayoutConstraint.activate([ +// metricView.leadingAnchor.constraint(equalTo: contentView.layoutMarginsGuide.leadingAnchor), +// metricView.trailingAnchor.constraint(equalTo: contentView.layoutMarginsGuide.trailingAnchor), +// metricView.topAnchor.constraint(equalTo: contentView.topAnchor), +// metricView.bottomAnchor.constraint(equalTo: contentView.bottomAnchor), +// metricView.heightAnchor.constraint(equalToConstant: 80).priority(.defaultHigh), +// ]) +// +// contentView.addSubview(collectionView) +// NSLayoutConstraint.activate([ +// collectionView.leadingAnchor.constraint(equalTo: contentView.leadingAnchor), +// collectionView.trailingAnchor.constraint(equalTo: contentView.trailingAnchor), +// collectionView.topAnchor.constraint(equalTo: contentView.topAnchor, constant: 10), +// contentView.bottomAnchor.constraint(equalTo: collectionView.bottomAnchor, constant: 20), +// collectionView.heightAnchor.constraint(equalToConstant: 80).priority(.defaultHigh), +// ]) +// +// collectionView.delegate = self +// } +// +// override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) { +// super.traitCollectionDidChange(previousTraitCollection) +// +// configureMargin() +// } +// +// override func layoutSubviews() { +// super.layoutSubviews() +// +// collectionView.collectionViewLayout.invalidateLayout() +// } +// +//} +// +//extension PickServerCategoriesCell { +// private func configureMargin() { +// switch traitCollection.horizontalSizeClass { +// case .regular: +// let margin = MastodonPickServerViewController.viewEdgeMargin +// contentView.layoutMargins = UIEdgeInsets(top: 0, left: margin, bottom: 0, right: margin) +// default: +// contentView.layoutMargins = .zero +// } +// } +//} +// +//// MARK: - UICollectionViewDelegateFlowLayout +//extension PickServerCategoriesCell: UICollectionViewDelegateFlowLayout { +// +// func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) { +// os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s: indexPath: %s", ((#file as NSString).lastPathComponent), #line, #function, indexPath.debugDescription) +// collectionView.selectItem(at: indexPath, animated: true, scrollPosition: .centeredHorizontally) +// delegate?.pickServerCategoriesCell(self, collectionView: collectionView, didSelectItemAt: indexPath) +// } +// +// func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, insetForSectionAt section: Int) -> UIEdgeInsets { +// layoutIfNeeded() +// return UIEdgeInsets(top: 0, left: metricView.frame.minX - collectionView.frame.minX, bottom: 0, right: collectionView.frame.maxX - metricView.frame.maxX) +// } +// +// func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, minimumInteritemSpacingForSectionAt section: Int) -> CGFloat { +// return 16 +// } +// +// func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize { +// return CGSize(width: 60, height: 80) +// } +// +//} +// +//extension PickServerCategoriesCell { +// +// override func accessibilityElementCount() -> Int { +// guard let diffableDataSource = diffableDataSource else { return 0 } +// return diffableDataSource.snapshot().itemIdentifiers.count +// } +// +// override func accessibilityElement(at index: Int) -> Any? { +// guard let item = collectionView.cellForItem(at: IndexPath(item: index, section: 0)) else { return nil } +// return item +// } +// +//} diff --git a/Mastodon/Deprecated/PickServerSearchCell.swift b/Mastodon/Deprecated/PickServerSearchCell.swift new file mode 100644 index 00000000..465e7ae2 --- /dev/null +++ b/Mastodon/Deprecated/PickServerSearchCell.swift @@ -0,0 +1,171 @@ +// +// PickServerSearchCell.swift +// Mastodon +// +// Created by BradGao on 2021/2/24. +// + +import UIKit + +//protocol PickServerSearchCellDelegate: AnyObject { +// func pickServerSearchCell(_ cell: PickServerSearchCell, searchTextDidChange searchText: String?) +//} +// +//class PickServerSearchCell: UITableViewCell { +// +// weak var delegate: PickServerSearchCellDelegate? +// +// private var bgView: UIView = { +// let view = UIView() +// view.backgroundColor = Asset.Theme.Mastodon.secondaryGroupedSystemBackground.color +// view.translatesAutoresizingMaskIntoConstraints = false +// view.layer.maskedCorners = [ +// .layerMinXMinYCorner, +// .layerMaxXMinYCorner +// ] +// view.layer.cornerCurve = .continuous +// view.layer.cornerRadius = MastodonPickServerAppearance.tableViewCornerRadius +// return view +// }() +// +// private var textFieldBgView: UIView = { +// let view = UIView() +// view.backgroundColor = Asset.Colors.TextField.background.color +// view.translatesAutoresizingMaskIntoConstraints = false +// view.layer.masksToBounds = true +// view.layer.cornerRadius = 6 +// view.layer.cornerCurve = .continuous +// return view +// }() +// +// let searchTextField: UITextField = { +// let textField = UITextField() +// textField.translatesAutoresizingMaskIntoConstraints = false +// textField.leftView = { +// let imageView = UIImageView( +// image: UIImage( +// systemName: "magnifyingglass", +// withConfiguration: UIImage.SymbolConfiguration(pointSize: 15, weight: .regular) +// ) +// ) +// imageView.tintColor = Asset.Colors.Label.secondary.color.withAlphaComponent(0.6) +// +// let containerView = UIView() +// imageView.translatesAutoresizingMaskIntoConstraints = false +// containerView.addSubview(imageView) +// NSLayoutConstraint.activate([ +// imageView.topAnchor.constraint(equalTo: containerView.topAnchor), +// imageView.leadingAnchor.constraint(equalTo: containerView.leadingAnchor), +// imageView.bottomAnchor.constraint(equalTo: containerView.bottomAnchor), +// ]) +// +// let paddingView = UIView() +// paddingView.translatesAutoresizingMaskIntoConstraints = false +// containerView.addSubview(paddingView) +// NSLayoutConstraint.activate([ +// paddingView.topAnchor.constraint(equalTo: containerView.topAnchor), +// paddingView.leadingAnchor.constraint(equalTo: imageView.trailingAnchor), +// paddingView.trailingAnchor.constraint(equalTo: containerView.trailingAnchor), +// paddingView.bottomAnchor.constraint(equalTo: containerView.bottomAnchor), +// paddingView.widthAnchor.constraint(equalToConstant: 4).priority(.defaultHigh), +// ]) +// return containerView +// }() +// textField.leftViewMode = .always +// textField.font = .systemFont(ofSize: 15, weight: .regular) +// textField.tintColor = Asset.Colors.Label.primary.color +// textField.textColor = Asset.Colors.Label.primary.color +// textField.adjustsFontForContentSizeCategory = true +// textField.attributedPlaceholder = +// NSAttributedString(string: L10n.Scene.ServerPicker.Input.placeholder, +// attributes: [.font: UIFont.systemFont(ofSize: 15, weight: .regular), +// .foregroundColor: Asset.Colors.Label.secondary.color.withAlphaComponent(0.6)]) +// textField.clearButtonMode = .whileEditing +// textField.autocapitalizationType = .none +// textField.autocorrectionType = .no +// textField.returnKeyType = .done +// textField.keyboardType = .URL +// return textField +// }() +// +// override func prepareForReuse() { +// super.prepareForReuse() +// +// delegate = nil +// } +// +// override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { +// super.init(style: style, reuseIdentifier: reuseIdentifier) +// _init() +// } +// +// required init?(coder: NSCoder) { +// super.init(coder: coder) +// _init() +// } +//} +// +//extension PickServerSearchCell { +// private func _init() { +// selectionStyle = .none +// backgroundColor = Asset.Theme.Mastodon.systemGroupedBackground.color +// configureMargin() +// +// searchTextField.addTarget(self, action: #selector(textFieldDidChange(_:)), for: .editingChanged) +// searchTextField.delegate = self +// +// contentView.addSubview(bgView) +// contentView.addSubview(textFieldBgView) +// contentView.addSubview(searchTextField) +// +// NSLayoutConstraint.activate([ +// bgView.leadingAnchor.constraint(equalTo: contentView.layoutMarginsGuide.leadingAnchor), +// bgView.topAnchor.constraint(equalTo: contentView.topAnchor), +// bgView.trailingAnchor.constraint(equalTo: contentView.layoutMarginsGuide.trailingAnchor), +// bgView.bottomAnchor.constraint(equalTo: contentView.bottomAnchor), +// +// textFieldBgView.leadingAnchor.constraint(equalTo: bgView.leadingAnchor, constant: 14), +// textFieldBgView.topAnchor.constraint(equalTo: bgView.topAnchor, constant: 12), +// bgView.trailingAnchor.constraint(equalTo: textFieldBgView.trailingAnchor, constant: 14), +// bgView.bottomAnchor.constraint(equalTo: textFieldBgView.bottomAnchor, constant: 13), +// +// searchTextField.leadingAnchor.constraint(equalTo: textFieldBgView.leadingAnchor, constant: 11), +// searchTextField.topAnchor.constraint(equalTo: textFieldBgView.topAnchor, constant: 4), +// textFieldBgView.trailingAnchor.constraint(equalTo: searchTextField.trailingAnchor, constant: 11), +// textFieldBgView.bottomAnchor.constraint(equalTo: searchTextField.bottomAnchor, constant: 4), +// ]) +// } +// +// override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) { +// super.traitCollectionDidChange(previousTraitCollection) +// +// configureMargin() +// } +//} +// +//extension PickServerSearchCell { +// private func configureMargin() { +// switch traitCollection.horizontalSizeClass { +// case .regular: +// let margin = MastodonPickServerViewController.viewEdgeMargin +// contentView.layoutMargins = UIEdgeInsets(top: 0, left: margin, bottom: 0, right: margin) +// default: +// contentView.layoutMargins = .zero +// } +// } +//} +// +//extension PickServerSearchCell { +// @objc private func textFieldDidChange(_ textField: UITextField) { +// delegate?.pickServerSearchCell(self, searchTextDidChange: textField.text) +// } +//} +// +//// MARK: - UITextFieldDelegate +//extension PickServerSearchCell: UITextFieldDelegate { +// +// func textFieldShouldReturn(_ textField: UITextField) -> Bool { +// textField.resignFirstResponder() +// return false +// } +//} diff --git a/Mastodon/Diffiable/Item/SelectedAccountItem.swift b/Mastodon/Diffiable/Account/SelectedAccountItem.swift similarity index 100% rename from Mastodon/Diffiable/Item/SelectedAccountItem.swift rename to Mastodon/Diffiable/Account/SelectedAccountItem.swift diff --git a/Mastodon/Diffiable/Section/SelectedAccountSection.swift b/Mastodon/Diffiable/Account/SelectedAccountSection.swift similarity index 100% rename from Mastodon/Diffiable/Section/SelectedAccountSection.swift rename to Mastodon/Diffiable/Account/SelectedAccountSection.swift diff --git a/Mastodon/Diffiable/Item/AutoCompleteItem.swift b/Mastodon/Diffiable/Compose/AutoCompleteItem.swift similarity index 100% rename from Mastodon/Diffiable/Item/AutoCompleteItem.swift rename to Mastodon/Diffiable/Compose/AutoCompleteItem.swift diff --git a/Mastodon/Diffiable/Section/Compose/AutoCompleteSection.swift b/Mastodon/Diffiable/Compose/AutoCompleteSection.swift similarity index 100% rename from Mastodon/Diffiable/Section/Compose/AutoCompleteSection.swift rename to Mastodon/Diffiable/Compose/AutoCompleteSection.swift diff --git a/Mastodon/Diffiable/Item/ComposeStatusAttachmentItem.swift b/Mastodon/Diffiable/Compose/ComposeStatusAttachmentItem.swift similarity index 100% rename from Mastodon/Diffiable/Item/ComposeStatusAttachmentItem.swift rename to Mastodon/Diffiable/Compose/ComposeStatusAttachmentItem.swift diff --git a/Mastodon/Diffiable/Section/Compose/ComposeStatusAttachmentSection.swift b/Mastodon/Diffiable/Compose/ComposeStatusAttachmentSection.swift similarity index 100% rename from Mastodon/Diffiable/Section/Compose/ComposeStatusAttachmentSection.swift rename to Mastodon/Diffiable/Compose/ComposeStatusAttachmentSection.swift diff --git a/Mastodon/Diffiable/Item/ComposeStatusItem.swift b/Mastodon/Diffiable/Compose/ComposeStatusItem.swift similarity index 100% rename from Mastodon/Diffiable/Item/ComposeStatusItem.swift rename to Mastodon/Diffiable/Compose/ComposeStatusItem.swift diff --git a/Mastodon/Diffiable/Item/ComposeStatusPollItem.swift b/Mastodon/Diffiable/Compose/ComposeStatusPollItem.swift similarity index 100% rename from Mastodon/Diffiable/Item/ComposeStatusPollItem.swift rename to Mastodon/Diffiable/Compose/ComposeStatusPollItem.swift diff --git a/Mastodon/Diffiable/Section/Compose/ComposeStatusPollSection.swift b/Mastodon/Diffiable/Compose/ComposeStatusPollSection.swift similarity index 100% rename from Mastodon/Diffiable/Section/Compose/ComposeStatusPollSection.swift rename to Mastodon/Diffiable/Compose/ComposeStatusPollSection.swift diff --git a/Mastodon/Diffiable/Section/Compose/ComposeStatusSection.swift b/Mastodon/Diffiable/Compose/ComposeStatusSection.swift similarity index 100% rename from Mastodon/Diffiable/Section/Compose/ComposeStatusSection.swift rename to Mastodon/Diffiable/Compose/ComposeStatusSection.swift diff --git a/Mastodon/Diffiable/Item/CustomEmojiPickerItem.swift b/Mastodon/Diffiable/Compose/CustomEmojiPickerItem.swift similarity index 100% rename from Mastodon/Diffiable/Item/CustomEmojiPickerItem.swift rename to Mastodon/Diffiable/Compose/CustomEmojiPickerItem.swift diff --git a/Mastodon/Diffiable/Section/Compose/CustomEmojiPickerSection.swift b/Mastodon/Diffiable/Compose/CustomEmojiPickerSection.swift similarity index 100% rename from Mastodon/Diffiable/Section/Compose/CustomEmojiPickerSection.swift rename to Mastodon/Diffiable/Compose/CustomEmojiPickerSection.swift diff --git a/Mastodon/Diffiable/DataSource/ASTableNode.swift b/Mastodon/Diffiable/DataSource/ASTableNode.swift deleted file mode 100644 index 36ff1fb0..00000000 --- a/Mastodon/Diffiable/DataSource/ASTableNode.swift +++ /dev/null @@ -1,85 +0,0 @@ -// -// ASTableNode.swift -// Mastodon -// -// Created by Cirno MainasuK on 2021-6-19. -// - -#if ASDK - -import UIKit -import AsyncDisplayKit -import DifferenceKit -import DiffableDataSources - -extension ASTableNode: ReloadableTableView { - public func reload( - using stagedChangeset: StagedChangeset, - deleteSectionsAnimation: @autoclosure () -> UITableView.RowAnimation, - insertSectionsAnimation: @autoclosure () -> UITableView.RowAnimation, - reloadSectionsAnimation: @autoclosure () -> UITableView.RowAnimation, - deleteRowsAnimation: @autoclosure () -> UITableView.RowAnimation, - insertRowsAnimation: @autoclosure () -> UITableView.RowAnimation, - reloadRowsAnimation: @autoclosure () -> UITableView.RowAnimation, - interrupt: ((Changeset) -> Bool)? = nil, - setData: (C) -> Void - ) { - if case .none = view.window, let data = stagedChangeset.last?.data { - setData(data) - return reloadData() - } - - for changeset in stagedChangeset { - if let interrupt = interrupt, interrupt(changeset), let data = stagedChangeset.last?.data { - setData(data) - return reloadData() - } - - func updates() { - setData(changeset.data) - - if !changeset.sectionDeleted.isEmpty { - deleteSections(IndexSet(changeset.sectionDeleted), with: deleteSectionsAnimation()) - } - - if !changeset.sectionInserted.isEmpty { - insertSections(IndexSet(changeset.sectionInserted), with: insertSectionsAnimation()) - } - - if !changeset.sectionUpdated.isEmpty { - reloadSections(IndexSet(changeset.sectionUpdated), with: reloadSectionsAnimation()) - } - - for (source, target) in changeset.sectionMoved { - moveSection(source, toSection: target) - } - - if !changeset.elementDeleted.isEmpty { - deleteRows(at: changeset.elementDeleted.map { IndexPath(row: $0.element, section: $0.section) }, with: deleteRowsAnimation()) - } - - if !changeset.elementInserted.isEmpty { - insertRows(at: changeset.elementInserted.map { IndexPath(row: $0.element, section: $0.section) }, with: insertRowsAnimation()) - } - - if !changeset.elementUpdated.isEmpty { - reloadRows(at: changeset.elementUpdated.map { IndexPath(row: $0.element, section: $0.section) }, with: reloadRowsAnimation()) - } - - for (source, target) in changeset.elementMoved { - moveRow(at: IndexPath(row: source.element, section: source.section), to: IndexPath(row: target.element, section: target.section)) - } - } - - if isNodeLoaded { - view.beginUpdates() - updates() - view.endUpdates(animated: false, completion: nil) - } else { - updates() - } - } - } -} - -#endif diff --git a/Mastodon/Diffiable/DataSource/TableNodeDiffableDataSource.swift b/Mastodon/Diffiable/DataSource/TableNodeDiffableDataSource.swift deleted file mode 100644 index 54ab22a4..00000000 --- a/Mastodon/Diffiable/DataSource/TableNodeDiffableDataSource.swift +++ /dev/null @@ -1,115 +0,0 @@ -// -// TableNodeDiffableDataSource.swift -// Mastodon -// -// Created by Cirno MainasuK on 2021-6-19. -// - -#if ASDK - -import UIKit -import AsyncDisplayKit -import DiffableDataSources - -open class TableNodeDiffableDataSource: NSObject, ASTableDataSource { - /// The type of closure providing the cell. - public typealias CellProvider = (ASTableNode, IndexPath, ItemIdentifierType) -> ASCellNodeBlock? - - /// The default animation to updating the views. - public var defaultRowAnimation: UITableView.RowAnimation = .automatic - - private weak var tableNode: ASTableNode? - private let cellProvider: CellProvider - private let core = DiffableDataSourceCore() - - /// Creates a new data source. - /// - /// - Parameters: - /// - tableView: A table view instance to be managed. - /// - cellProvider: A closure to dequeue the cell for rows. - public init(tableNode: ASTableNode, cellProvider: @escaping CellProvider) { - self.tableNode = tableNode - self.cellProvider = cellProvider - super.init() - - tableNode.delegate = self - } - - /// Applies given snapshot to perform automatic diffing update. - /// - /// - Parameters: - /// - snapshot: A snapshot object to be applied to data model. - /// - animatingDifferences: A Boolean value indicating whether to update with - /// diffing animation. - /// - completion: An optional completion block which is called when the complete - /// performing updates. - public func apply(_ snapshot: DiffableDataSourceSnapshot, animatingDifferences: Bool = true, completion: (() -> Void)? = nil) { - core.apply(snapshot, view: tableNode, animatingDifferences: animatingDifferences, completion: completion) - } - - /// Returns a new snapshot object of current state. - /// - /// - Returns: A new snapshot object of current state. - public func snapshot() -> DiffableDataSourceSnapshot { - return core.snapshot() - } - - /// Returns an item identifier for given index path. - /// - /// - Parameters: - /// - indexPath: An index path for the item identifier. - /// - /// - Returns: An item identifier for given index path. - public func itemIdentifier(for indexPath: IndexPath) -> ItemIdentifierType? { - return core.itemIdentifier(for: indexPath) - } - - /// Returns an index path for given item identifier. - /// - /// - Parameters: - /// - itemIdentifier: An identifier of item. - /// - /// - Returns: An index path for given item identifier. - public func indexPath(for itemIdentifier: ItemIdentifierType) -> IndexPath? { - return core.indexPath(for: itemIdentifier) - } - - /// Returns the number of sections in the data source. - /// - /// - Parameters: - /// - tableNode: A table node instance managed by `self`. - /// - /// - Returns: The number of sections in the data source. - public func numberOfSections(in tableNode: ASTableNode) -> Int { - return core.numberOfSections() - } - - /// Returns the number of items in the specified section. - /// - /// - Parameters: - /// - tableNode: A table node instance managed by `self`. - /// - section: An index of section. - /// - /// - Returns: The number of items in the specified section. - public func tableNode(_ tableNode: ASTableNode, numberOfRowsInSection section: Int) -> Int { - return core.numberOfItems(inSection: section) - } - - /// Returns a cell for row at specified index path. - /// - /// - Parameters: - /// - tableView: A table view instance managed by `self`. - /// - indexPath: An index path for cell. - /// - /// - Returns: A cell for row at specified index path. - open func tableNode(_ tableNode: ASTableNode, nodeBlockForRowAt indexPath: IndexPath) -> ASCellNodeBlock { - let itemIdentifier = core.unsafeItemIdentifier(for: indexPath) - guard let block = cellProvider(tableNode, indexPath, itemIdentifier) else { - fatalError("UITableView dataSource returned a nil cell for row at index path: \(indexPath), tableNode: \(tableNode), itemIdentifier: \(itemIdentifier)") - } - - return block - } -} - -#endif diff --git a/Mastodon/Diffiable/Item/NotificationItem.swift b/Mastodon/Diffiable/Notification/NotificationItem.swift similarity index 100% rename from Mastodon/Diffiable/Item/NotificationItem.swift rename to Mastodon/Diffiable/Notification/NotificationItem.swift diff --git a/Mastodon/Diffiable/Section/Status/NotificationSection.swift b/Mastodon/Diffiable/Notification/NotificationSection.swift similarity index 100% rename from Mastodon/Diffiable/Section/Status/NotificationSection.swift rename to Mastodon/Diffiable/Notification/NotificationSection.swift diff --git a/Mastodon/Diffiable/Item/CategoryPickerItem.swift b/Mastodon/Diffiable/Onboarding/CategoryPickerItem.swift similarity index 69% rename from Mastodon/Diffiable/Item/CategoryPickerItem.swift rename to Mastodon/Diffiable/Onboarding/CategoryPickerItem.swift index 0f2cdcc2..53f9c9ab 100644 --- a/Mastodon/Diffiable/Item/CategoryPickerItem.swift +++ b/Mastodon/Diffiable/Onboarding/CategoryPickerItem.swift @@ -15,10 +15,11 @@ enum CategoryPickerItem { } extension CategoryPickerItem { - var title: String { + + var emoji: String { switch self { case .all: - return L10n.Scene.ServerPicker.Button.Category.all + return "💬" case .category(let category): switch category.category { case .academia: @@ -32,7 +33,7 @@ extension CategoryPickerItem { case .games: return "🕹" case .general: - return "💬" + return "🐘" case .journalism: return "📰" case .lgbt: @@ -50,6 +51,41 @@ extension CategoryPickerItem { } } } + var title: String { + switch self { + case .all: + return L10n.Scene.ServerPicker.Button.Category.all + case .category(let category): + switch category.category { + case .academia: + return L10n.Scene.ServerPicker.Button.Category.academia + case .activism: + return L10n.Scene.ServerPicker.Button.Category.activism + case .food: + return L10n.Scene.ServerPicker.Button.Category.food + case .furry: + return L10n.Scene.ServerPicker.Button.Category.furry + case .games: + return L10n.Scene.ServerPicker.Button.Category.games + case .general: + return L10n.Scene.ServerPicker.Button.Category.general + case .journalism: + return L10n.Scene.ServerPicker.Button.Category.journalism + case .lgbt: + return L10n.Scene.ServerPicker.Button.Category.lgbt + case .regional: + return L10n.Scene.ServerPicker.Button.Category.regional + case .art: + return L10n.Scene.ServerPicker.Button.Category.art + case .music: + return L10n.Scene.ServerPicker.Button.Category.music + case .tech: + return L10n.Scene.ServerPicker.Button.Category.tech + case ._other: + return "-" // FIXME: + } + } + } var accessibilityDescription: String { switch self { @@ -82,7 +118,7 @@ extension CategoryPickerItem { case .tech: return L10n.Scene.ServerPicker.Button.Category.tech case ._other: - return "❓" // FIXME: + return "-" // FIXME: } } } diff --git a/Mastodon/Diffiable/Section/Onboarding/CategoryPickerSection.swift b/Mastodon/Diffiable/Onboarding/CategoryPickerSection.swift similarity index 52% rename from Mastodon/Diffiable/Section/Onboarding/CategoryPickerSection.swift rename to Mastodon/Diffiable/Onboarding/CategoryPickerSection.swift index 732813c0..525d7720 100644 --- a/Mastodon/Diffiable/Section/Onboarding/CategoryPickerSection.swift +++ b/Mastodon/Diffiable/Onboarding/CategoryPickerSection.swift @@ -19,27 +19,11 @@ extension CategoryPickerSection { UICollectionViewDiffableDataSource(collectionView: collectionView) { [weak dependency] collectionView, indexPath, item -> UICollectionViewCell? in guard let _ = dependency else { return nil } let cell = collectionView.dequeueReusableCell(withReuseIdentifier: String(describing: PickServerCategoryCollectionViewCell.self), for: indexPath) as! PickServerCategoryCollectionViewCell - switch item { - case .all: - cell.categoryView.titleLabel.font = .systemFont(ofSize: 17) - case .category: - cell.categoryView.titleLabel.font = .systemFont(ofSize: 28) - } + cell.categoryView.emojiLabel.text = item.emoji cell.categoryView.titleLabel.text = item.title cell.observe(\.isSelected, options: [.initial, .new]) { cell, _ in - if cell.isSelected { - cell.categoryView.bgView.backgroundColor = Asset.Colors.brandBlue.color - cell.categoryView.bgView.applyShadow(color: Asset.Colors.brandBlue.color, alpha: 1, x: 0, y: 0, blur: 4.0) - if case .all = item { - cell.categoryView.titleLabel.textColor = .white - } - } else { - cell.categoryView.bgView.backgroundColor = Asset.Theme.Mastodon.secondaryGroupedSystemBackground.color - cell.categoryView.bgView.applyShadow(color: Asset.Colors.brandBlue.color, alpha: 0, x: 0, y: 0, blur: 0.0) - if case .all = item { - cell.categoryView.titleLabel.textColor = Asset.Colors.brandBlue.color - } - } + cell.categoryView.highlightedIndicatorView.alpha = cell.isSelected ? 1 : 0 + cell.categoryView.titleLabel.textColor = cell.isSelected ? Asset.Colors.Label.primary.color : Asset.Colors.Label.secondary.color } .store(in: &cell.observations) diff --git a/Mastodon/Diffiable/Item/PickServerItem.swift b/Mastodon/Diffiable/Onboarding/PickServerItem.swift similarity index 85% rename from Mastodon/Diffiable/Item/PickServerItem.swift rename to Mastodon/Diffiable/Onboarding/PickServerItem.swift index 7db2c958..ba693ad7 100644 --- a/Mastodon/Diffiable/Item/PickServerItem.swift +++ b/Mastodon/Diffiable/Onboarding/PickServerItem.swift @@ -12,8 +12,6 @@ import MastodonSDK /// Note: update Equatable when change case enum PickServerItem { case header - case categoryPicker(items: [CategoryPickerItem]) - case search case server(server: Mastodon.Entity.Server, attribute: ServerItemAttribute) case loader(attribute: LoaderItemAttribute) } @@ -63,10 +61,6 @@ extension PickServerItem: Equatable { switch (lhs, rhs) { case (.header, .header): return true - case (.categoryPicker(let itemsLeft), .categoryPicker(let itemsRight)): - return itemsLeft == itemsRight - case (.search, .search): - return true case (.server(let serverLeft, _), .server(let serverRight, _)): return serverLeft.domain == serverRight.domain case (.loader(let attributeLeft), loader(let attributeRight)): @@ -82,10 +76,6 @@ extension PickServerItem: Hashable { switch self { case .header: hasher.combine(String(describing: PickServerItem.header.self)) - case .categoryPicker(let items): - hasher.combine(items) - case .search: - hasher.combine(String(describing: PickServerItem.search.self)) case .server(let server, _): hasher.combine(server.domain) case .loader(let attribute): diff --git a/Mastodon/Diffiable/Section/Onboarding/PickServerSection.swift b/Mastodon/Diffiable/Onboarding/PickServerSection.swift similarity index 51% rename from Mastodon/Diffiable/Section/Onboarding/PickServerSection.swift rename to Mastodon/Diffiable/Onboarding/PickServerSection.swift index 28b1ded3..9f74bad5 100644 --- a/Mastodon/Diffiable/Section/Onboarding/PickServerSection.swift +++ b/Mastodon/Diffiable/Onboarding/PickServerSection.swift @@ -12,8 +12,6 @@ import AlamofireImage enum PickServerSection: Equatable, Hashable { case header - case category - case search case servers } @@ -21,36 +19,16 @@ extension PickServerSection { static func tableViewDiffableDataSource( for tableView: UITableView, dependency: NeedsDependency, - pickServerCategoriesCellDelegate: PickServerCategoriesCellDelegate, - pickServerSearchCellDelegate: PickServerSearchCellDelegate, pickServerCellDelegate: PickServerCellDelegate ) -> UITableViewDiffableDataSource { UITableViewDiffableDataSource(tableView: tableView) { [ weak dependency, - weak pickServerCategoriesCellDelegate, - weak pickServerSearchCellDelegate, weak pickServerCellDelegate ] tableView, indexPath, item -> UITableViewCell? in guard let dependency = dependency else { return nil } switch item { case .header: - let cell = tableView.dequeueReusableCell(withIdentifier: String(describing: PickServerTitleCell.self), for: indexPath) as! PickServerTitleCell - return cell - case .categoryPicker(let items): - let cell = tableView.dequeueReusableCell(withIdentifier: String(describing: PickServerCategoriesCell.self), for: indexPath) as! PickServerCategoriesCell - cell.delegate = pickServerCategoriesCellDelegate - cell.diffableDataSource = CategoryPickerSection.collectionViewDiffableDataSource( - for: cell.collectionView, - dependency: dependency - ) - var snapshot = NSDiffableDataSourceSnapshot() - snapshot.appendSections([.main]) - snapshot.appendItems(items, toSection: .main) - cell.diffableDataSource?.apply(snapshot, animatingDifferences: false, completion: nil) - return cell - case .search: - let cell = tableView.dequeueReusableCell(withIdentifier: String(describing: PickServerSearchCell.self), for: indexPath) as! PickServerSearchCell - cell.delegate = pickServerSearchCellDelegate + let cell = tableView.dequeueReusableCell(withIdentifier: String(describing: OnboardingHeadlineTableViewCell.self), for: indexPath) as! OnboardingHeadlineTableViewCell return cell case .server(let server, let attribute): let cell = tableView.dequeueReusableCell(withIdentifier: String(describing: PickServerCell.self), for: indexPath) as! PickServerCell @@ -70,19 +48,63 @@ extension PickServerSection { static func configure(cell: PickServerCell, server: Mastodon.Entity.Server, attribute: PickServerItem.ServerItemAttribute) { cell.domainLabel.text = server.domain - cell.descriptionLabel.text = { - guard let html = try? HTML(html: server.description, encoding: .utf8) else { - return server.description - } + cell.descriptionLabel.attributedText = { + let content: String = { + guard let html = try? HTML(html: server.description, encoding: .utf8) else { + return server.description + } + return html.text ?? server.description + }() - return html.text ?? server.description + let paragraphStyle = NSMutableParagraphStyle() + paragraphStyle.lineHeightMultiple = 1.16 + + return NSAttributedString( + string: content, + attributes: [ + .paragraphStyle: paragraphStyle + ] + ) }() - cell.langValueLabel.text = server.language.uppercased() - cell.usersValueLabel.text = parseUsersCount(server.totalUsers) - cell.categoryValueLabel.text = server.category.uppercased() - - cell.updateExpandMode(mode: attribute.isExpand ? .expand : .collapse) - + cell.usersValueLabel.attributedText = { + let attributedString = NSMutableAttributedString() + let attachment = NSTextAttachment(image: UIImage(systemName: "person.2.fill")!) + let attachmentAttributedString = NSAttributedString(attachment: attachment) + attributedString.append(attachmentAttributedString) + attributedString.append(NSAttributedString(string: " ")) + + let paragraphStyle = NSMutableParagraphStyle() + paragraphStyle.lineHeightMultiple = 1.12 + let valueAttributedString = NSAttributedString( + string: parseUsersCount(server.totalUsers), + attributes: [ + .paragraphStyle: paragraphStyle + ] + ) + attributedString.append(valueAttributedString) + + return attributedString + }() + cell.langValueLabel.attributedText = { + let attributedString = NSMutableAttributedString() + let attachment = NSTextAttachment(image: UIImage(systemName: "text.bubble.fill")!) + let attachmentAttributedString = NSAttributedString(attachment: attachment) + attributedString.append(attachmentAttributedString) + attributedString.append(NSAttributedString(string: " ")) + + let paragraphStyle = NSMutableParagraphStyle() + paragraphStyle.lineHeightMultiple = 1.12 + let valueAttributedString = NSAttributedString( + string: server.language.uppercased(), + attributes: [ + .paragraphStyle: paragraphStyle + ] + ) + attributedString.append(valueAttributedString) + + return attributedString + }() + attribute.isLast .receive(on: DispatchQueue.main) .sink { [weak cell] isLast in @@ -101,41 +123,6 @@ extension PickServerSection { } } .store(in: &cell.disposeBag) - - cell.expandMode - .receive(on: DispatchQueue.main) - .sink { mode in - switch mode { - case .collapse: - // do nothing - break - case .expand: - let placeholderImage = UIImage.placeholder(size: cell.thumbnailImageView.frame.size, color: .systemFill) - .af.imageRounded(withCornerRadius: 3.0, divideRadiusByImageScale: false) - guard let proxiedThumbnail = server.proxiedThumbnail, - let url = URL(string: proxiedThumbnail) else { - cell.thumbnailImageView.image = placeholderImage - cell.thumbnailActivityIndicator.stopAnimating() - return - } - cell.thumbnailImageView.isHidden = false - cell.thumbnailActivityIndicator.startAnimating() - - cell.thumbnailImageView.af.setImage( - withURL: url, - placeholderImage: placeholderImage, - filter: AspectScaledToFillSizeWithRoundedCornersFilter(size: cell.thumbnailImageView.frame.size, radius: 3), - imageTransition: .crossDissolve(0.33), - completion: { [weak cell] response in - switch response.result { - case .success, .failure: - cell?.thumbnailActivityIndicator.stopAnimating() - } - } - ) - } - } - .store(in: &cell.disposeBag) } private static func parseUsersCount(_ usersCount: Int) -> String { diff --git a/Mastodon/Diffiable/Onboarding/RegisterItem.swift b/Mastodon/Diffiable/Onboarding/RegisterItem.swift new file mode 100644 index 00000000..0fb0aead --- /dev/null +++ b/Mastodon/Diffiable/Onboarding/RegisterItem.swift @@ -0,0 +1,19 @@ +// +// RegisterItem.swift +// Mastodon +// +// Created by MainasuK on 2022-1-5. +// + +import Foundation + +enum RegisterItem: Hashable { + case header + case avatar + case name + case username + case email + case password + case hint + case reason +} diff --git a/Mastodon/Diffiable/Onboarding/RegisterSection.swift b/Mastodon/Diffiable/Onboarding/RegisterSection.swift new file mode 100644 index 00000000..efb67f69 --- /dev/null +++ b/Mastodon/Diffiable/Onboarding/RegisterSection.swift @@ -0,0 +1,12 @@ +// +// RegisterSection.swift +// Mastodon +// +// Created by MainasuK on 2022-1-5. +// + +import UIKit + +enum RegisterSection: Hashable { + case main +} diff --git a/Mastodon/Diffiable/Onboarding/ServerRuleItem.swift b/Mastodon/Diffiable/Onboarding/ServerRuleItem.swift new file mode 100644 index 00000000..37d8b6ee --- /dev/null +++ b/Mastodon/Diffiable/Onboarding/ServerRuleItem.swift @@ -0,0 +1,21 @@ +// +// ServerRuleItem.swift +// Mastodon +// +// Created by MainasuK on 2022-1-5. +// + +import Foundation +import MastodonSDK + +enum ServerRuleItem: Hashable { + case header(domain: String) + case rule(RuleContext) +} + +extension ServerRuleItem { + struct RuleContext: Hashable { + let index: Int + let rule: Mastodon.Entity.Instance.Rule + } +} diff --git a/Mastodon/Diffiable/Onboarding/ServerRuleSection.swift b/Mastodon/Diffiable/Onboarding/ServerRuleSection.swift new file mode 100644 index 00000000..66abec44 --- /dev/null +++ b/Mastodon/Diffiable/Onboarding/ServerRuleSection.swift @@ -0,0 +1,34 @@ +// +// ServerRuleSection.swift +// Mastodon +// +// Created by MainasuK on 2022-1-5. +// + +import UIKit + +enum ServerRuleSection: Hashable { + case header + case rules +} + +extension ServerRuleSection { + static func tableViewDiffableDataSource( + tableView: UITableView + ) -> UITableViewDiffableDataSource { + return UITableViewDiffableDataSource(tableView: tableView) { tableView, indexPath, item in + switch item { + case .header(let domain): + let cell = tableView.dequeueReusableCell(withIdentifier: String(describing: OnboardingHeadlineTableViewCell.self), for: indexPath) as! OnboardingHeadlineTableViewCell + cell.titleLabel.text = L10n.Scene.ServerRules.title + cell.subTitleLabel.text = L10n.Scene.ServerRules.subtitle(domain) + return cell + case .rule(let ruleContext): + let cell = tableView.dequeueReusableCell(withIdentifier: String(describing: ServerRulesTableViewCell.self), for: indexPath) as! ServerRulesTableViewCell + cell.indexImageView.image = UIImage(systemName: "\(ruleContext.index + 1).circle.fill") ?? UIImage(systemName: "questionmark.circle.fill") + cell.ruleLabel.text = ruleContext.rule.text + return cell + } + } + } +} diff --git a/Mastodon/Diffiable/Item/PollItem.swift b/Mastodon/Diffiable/Poll/PollItem.swift similarity index 100% rename from Mastodon/Diffiable/Item/PollItem.swift rename to Mastodon/Diffiable/Poll/PollItem.swift diff --git a/Mastodon/Diffiable/Section/Status/PollSection.swift b/Mastodon/Diffiable/Poll/PollSection.swift similarity index 100% rename from Mastodon/Diffiable/Section/Status/PollSection.swift rename to Mastodon/Diffiable/Poll/PollSection.swift diff --git a/Mastodon/Diffiable/Item/ProfileFieldItem.swift b/Mastodon/Diffiable/Profile/ProfileFieldItem.swift similarity index 100% rename from Mastodon/Diffiable/Item/ProfileFieldItem.swift rename to Mastodon/Diffiable/Profile/ProfileFieldItem.swift diff --git a/Mastodon/Diffiable/Section/ProfileFieldSection.swift b/Mastodon/Diffiable/Profile/ProfileFieldSection.swift similarity index 100% rename from Mastodon/Diffiable/Section/ProfileFieldSection.swift rename to Mastodon/Diffiable/Profile/ProfileFieldSection.swift diff --git a/Mastodon/Diffiable/Section/Search/RecommendAccountSection.swift b/Mastodon/Diffiable/Search/RecommendAccountSection.swift similarity index 100% rename from Mastodon/Diffiable/Section/Search/RecommendAccountSection.swift rename to Mastodon/Diffiable/Search/RecommendAccountSection.swift diff --git a/Mastodon/Diffiable/Section/Search/RecommendHashTagSection.swift b/Mastodon/Diffiable/Search/RecommendHashTagSection.swift similarity index 100% rename from Mastodon/Diffiable/Section/Search/RecommendHashTagSection.swift rename to Mastodon/Diffiable/Search/RecommendHashTagSection.swift diff --git a/Mastodon/Diffiable/Item/SearchHistoryItem.swift b/Mastodon/Diffiable/Search/SearchHistoryItem.swift similarity index 100% rename from Mastodon/Diffiable/Item/SearchHistoryItem.swift rename to Mastodon/Diffiable/Search/SearchHistoryItem.swift diff --git a/Mastodon/Diffiable/Section/Search/SearchHistorySection.swift b/Mastodon/Diffiable/Search/SearchHistorySection.swift similarity index 100% rename from Mastodon/Diffiable/Section/Search/SearchHistorySection.swift rename to Mastodon/Diffiable/Search/SearchHistorySection.swift diff --git a/Mastodon/Diffiable/Item/SearchResultItem.swift b/Mastodon/Diffiable/Search/SearchResultItem.swift similarity index 100% rename from Mastodon/Diffiable/Item/SearchResultItem.swift rename to Mastodon/Diffiable/Search/SearchResultItem.swift diff --git a/Mastodon/Diffiable/Section/Search/SearchResultSection.swift b/Mastodon/Diffiable/Search/SearchResultSection.swift similarity index 100% rename from Mastodon/Diffiable/Section/Search/SearchResultSection.swift rename to Mastodon/Diffiable/Search/SearchResultSection.swift diff --git a/Mastodon/Diffiable/Item/SettingsItem.swift b/Mastodon/Diffiable/Settings/SettingsItem.swift similarity index 100% rename from Mastodon/Diffiable/Item/SettingsItem.swift rename to Mastodon/Diffiable/Settings/SettingsItem.swift diff --git a/Mastodon/Diffiable/Section/SettingsSection.swift b/Mastodon/Diffiable/Settings/SettingsSection.swift similarity index 100% rename from Mastodon/Diffiable/Section/SettingsSection.swift rename to Mastodon/Diffiable/Settings/SettingsSection.swift diff --git a/Mastodon/Diffiable/Item/Item.swift b/Mastodon/Diffiable/Status/Item.swift similarity index 100% rename from Mastodon/Diffiable/Item/Item.swift rename to Mastodon/Diffiable/Status/Item.swift diff --git a/Mastodon/Diffiable/Section/Status/ReportSection.swift b/Mastodon/Diffiable/Status/ReportSection.swift similarity index 100% rename from Mastodon/Diffiable/Section/Status/ReportSection.swift rename to Mastodon/Diffiable/Status/ReportSection.swift diff --git a/Mastodon/Diffiable/Section/Status/StatusSection.swift b/Mastodon/Diffiable/Status/StatusSection.swift similarity index 98% rename from Mastodon/Diffiable/Section/Status/StatusSection.swift rename to Mastodon/Diffiable/Status/StatusSection.swift index 61217c79..918b8b45 100644 --- a/Mastodon/Diffiable/Section/Status/StatusSection.swift +++ b/Mastodon/Diffiable/Status/StatusSection.swift @@ -18,10 +18,6 @@ import NaturalLanguage // import LinkPresentation -#if ASDK -import AsyncDisplayKit -#endif - protocol StatusCell: DisposeBagCollectable { var statusView: StatusView { get } var isFiltered: Bool { get set } @@ -32,33 +28,6 @@ enum StatusSection: Equatable, Hashable { } extension StatusSection { - #if ASDK - static func tableNodeDiffableDataSource( - tableNode: ASTableNode, - managedObjectContext: NSManagedObjectContext - ) -> TableNodeDiffableDataSource { - TableNodeDiffableDataSource(tableNode: tableNode) { tableNode, indexPath, item in - switch item { - case .homeTimelineIndex(let objectID, let attribute): - guard let homeTimelineIndex = try? managedObjectContext.existingObject(with: objectID) as? HomeTimelineIndex else { - return { ASCellNode() } - } - let status = homeTimelineIndex.status - - return { () -> ASCellNode in - let cellNode = StatusNode(status: status) - return cellNode - } - case .homeMiddleLoader: - return { TimelineMiddleLoaderNode() } - case .bottomLoader: - return { TimelineBottomLoaderNode() } - default: - return { ASCellNode() } - } - } - } - #endif static let logger = Logger(subsystem: "StatusSection", category: "logic") diff --git a/Mastodon/Diffiable/Item/UserItem.swift b/Mastodon/Diffiable/User/UserItem.swift similarity index 100% rename from Mastodon/Diffiable/Item/UserItem.swift rename to Mastodon/Diffiable/User/UserItem.swift diff --git a/Mastodon/Diffiable/Section/UserSection.swift b/Mastodon/Diffiable/User/UserSection.swift similarity index 100% rename from Mastodon/Diffiable/Section/UserSection.swift rename to Mastodon/Diffiable/User/UserSection.swift diff --git a/Mastodon/Generated/Assets.swift b/Mastodon/Generated/Assets.swift index 906dd74e..410d81a2 100644 --- a/Mastodon/Generated/Assets.swift +++ b/Mastodon/Generated/Assets.swift @@ -47,6 +47,7 @@ internal enum Asset { } internal enum Label { internal static let primary = ColorAsset(name: "Colors/Label/primary") + internal static let primaryReverse = ColorAsset(name: "Colors/Label/primary.reverse") internal static let secondary = ColorAsset(name: "Colors/Label/secondary") internal static let tertiary = ColorAsset(name: "Colors/Label/tertiary") } @@ -89,6 +90,16 @@ internal enum Asset { internal static let faceSmilingAdaptive = ImageAsset(name: "Human/face.smiling.adaptive") } internal enum Scene { + internal enum Onboarding { + internal static let avatarPlaceholder = ImageAsset(name: "Scene/Onboarding/avatar.placeholder") + internal static let navigationBackButtonBackground = ColorAsset(name: "Scene/Onboarding/navigation.back.button.background") + internal static let navigationBackButtonBackgroundHighlighted = ColorAsset(name: "Scene/Onboarding/navigation.back.button.background.highlighted") + internal static let navigationNextButtonBackground = ColorAsset(name: "Scene/Onboarding/navigation.next.button.background") + internal static let navigationNextButtonBackgroundHighlighted = ColorAsset(name: "Scene/Onboarding/navigation.next.button.background.highlighted") + internal static let onboardingBackground = ColorAsset(name: "Scene/Onboarding/onboarding.background") + internal static let searchBarBackground = ColorAsset(name: "Scene/Onboarding/search.bar.background") + internal static let textFieldBackground = ColorAsset(name: "Scene/Onboarding/textField.background") + } internal enum Profile { internal enum Banner { internal static let bioEditBackgroundGray = ColorAsset(name: "Scene/Profile/Banner/bio.edit.background.gray") @@ -102,8 +113,10 @@ internal enum Asset { internal enum Welcome { internal enum Illustration { internal static let backgroundCyan = ColorAsset(name: "Scene/Welcome/illustration/background.cyan") + internal static let cloudBaseExtend = ImageAsset(name: "Scene/Welcome/illustration/cloud.base.extend") internal static let cloudBase = ImageAsset(name: "Scene/Welcome/illustration/cloud.base") internal static let elephantOnAirplaneWithContrail = ImageAsset(name: "Scene/Welcome/illustration/elephant.on.airplane.with.contrail") + internal static let elephantThreeOnGrassExtend = ImageAsset(name: "Scene/Welcome/illustration/elephant.three.on.grass.extend") internal static let elephantThreeOnGrass = ImageAsset(name: "Scene/Welcome/illustration/elephant.three.on.grass") internal static let elephantThreeOnGrassWithTreeThree = ImageAsset(name: "Scene/Welcome/illustration/elephant.three.on.grass.with.tree.three") internal static let elephantThreeOnGrassWithTreeTwo = ImageAsset(name: "Scene/Welcome/illustration/elephant.three.on.grass.with.tree.two") @@ -112,6 +125,7 @@ internal enum Asset { internal static let mastodonLogoBlackLarge = ImageAsset(name: "Scene/Welcome/mastodon.logo.black.large") internal static let mastodonLogo = ImageAsset(name: "Scene/Welcome/mastodon.logo") internal static let mastodonLogoLarge = ImageAsset(name: "Scene/Welcome/mastodon.logo.large") + internal static let signInButtonBackground = ColorAsset(name: "Scene/Welcome/sign.in.button.background") } } internal enum Settings { diff --git a/Mastodon/Info.plist b/Mastodon/Info.plist index affa5b05..8f0b7211 100644 --- a/Mastodon/Info.plist +++ b/Mastodon/Info.plist @@ -17,7 +17,7 @@ CFBundlePackageType $(PRODUCT_BUNDLE_PACKAGE_TYPE) CFBundleShortVersionString - 1.2.0 + 1.3.0 CFBundleURLTypes @@ -30,7 +30,7 @@ CFBundleVersion - 88 + 90 ITSAppUsesNonExemptEncryption LSApplicationQueriesSchemes diff --git a/Mastodon/Protocol/StatusProvider/StatusProvider+StatusNodeDelegate.swift b/Mastodon/Protocol/StatusProvider/StatusProvider+StatusNodeDelegate.swift deleted file mode 100644 index 3c6d7da1..00000000 --- a/Mastodon/Protocol/StatusProvider/StatusProvider+StatusNodeDelegate.swift +++ /dev/null @@ -1,17 +0,0 @@ -// -// StatusProvider+StatusNodeDelegate.swift -// Mastodon -// -// Created by Cirno MainasuK on 2021-6-20. -// - -#if ASDK - -import Foundation - -// MARK: - StatusViewDelegate -extension StatusNodeDelegate where Self: StatusProvider { - -} - -#endif diff --git a/Mastodon/Protocol/StatusProvider/StatusProvider.swift b/Mastodon/Protocol/StatusProvider/StatusProvider.swift index 3497fd7a..2f13b8d5 100644 --- a/Mastodon/Protocol/StatusProvider/StatusProvider.swift +++ b/Mastodon/Protocol/StatusProvider/StatusProvider.swift @@ -10,10 +10,6 @@ import Combine import CoreData import CoreDataStack -#if ASDK -import AsyncDisplayKit -#endif - protocol StatusProvider: NeedsDependency & DisposeBagCollectable & UIViewController { // async func status() -> Future @@ -31,20 +27,8 @@ protocol StatusProvider: NeedsDependency & DisposeBagCollectable & UIViewControl func items(indexPaths: [IndexPath]) -> [Item] func statusObjectItems(indexPaths: [IndexPath]) -> [StatusObjectItem] - - #if ASDK - func status(node: ASCellNode?, indexPath: IndexPath?) -> Status? - #endif } -#if ASDK -extension StatusProvider { - func status(node: ASCellNode?, indexPath: IndexPath?) -> Status? { - fatalError("Needs implement this") - } -} -#endif - enum StatusObjectItem { case status(objectID: NSManagedObjectID) case homeTimelineIndex(objectID: NSManagedObjectID) diff --git a/Mastodon/Protocol/StatusProvider/StatusProviderFacade.swift b/Mastodon/Protocol/StatusProvider/StatusProviderFacade.swift index d11870ed..68987c30 100644 --- a/Mastodon/Protocol/StatusProvider/StatusProviderFacade.swift +++ b/Mastodon/Protocol/StatusProvider/StatusProviderFacade.swift @@ -14,10 +14,6 @@ import MastodonSDK import Meta import MetaTextKit -#if ASDK -import AsyncDisplayKit -#endif - enum StatusProviderFacade { } extension StatusProviderFacade { @@ -154,13 +150,6 @@ extension StatusProviderFacade { } } - #if ASDK - private static func coordinateToStatusMentionProfileScene(for target: Target, provider: StatusProvider, node: ASCellNode, mention: String) { - guard let status = provider.status(node: node, indexPath: nil) else { return } - coordinateToStatusMentionProfileScene(for: target, provider: provider, status: status, mention: mention, href: nil) - } - #endif - private static func coordinateToStatusMentionProfileScene(for target: Target, provider: StatusProvider, cell: UITableViewCell, mention: String, href: String?) { provider.status(for: cell, indexPath: nil) .sink { [weak provider] status in diff --git a/Mastodon/Resources/Assets.xcassets/Colors/Label/primary.colorset/Contents.json b/Mastodon/Resources/Assets.xcassets/Colors/Label/primary.colorset/Contents.json index 202a1c04..ee70bcc1 100644 --- a/Mastodon/Resources/Assets.xcassets/Colors/Label/primary.colorset/Contents.json +++ b/Mastodon/Resources/Assets.xcassets/Colors/Label/primary.colorset/Contents.json @@ -5,9 +5,9 @@ "color-space" : "srgb", "components" : { "alpha" : "1.000", - "blue" : "0x00", - "green" : "0x00", - "red" : "0x00" + "blue" : "0.216", + "green" : "0.173", + "red" : "0.157" } }, "idiom" : "universal" @@ -23,9 +23,9 @@ "color-space" : "srgb", "components" : { "alpha" : "1.000", - "blue" : "1.000", - "green" : "1.000", - "red" : "1.000" + "blue" : "0xEE", + "green" : "0xEE", + "red" : "0xEE" } }, "idiom" : "universal" diff --git a/Mastodon/Resources/Assets.xcassets/Colors/Label/primary.reverse.colorset/Contents.json b/Mastodon/Resources/Assets.xcassets/Colors/Label/primary.reverse.colorset/Contents.json new file mode 100644 index 00000000..8f42a585 --- /dev/null +++ b/Mastodon/Resources/Assets.xcassets/Colors/Label/primary.reverse.colorset/Contents.json @@ -0,0 +1,38 @@ +{ + "colors" : [ + { + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "0.933", + "green" : "0.933", + "red" : "0.933" + } + }, + "idiom" : "universal" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "dark" + } + ], + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "0.216", + "green" : "0.173", + "red" : "0.157" + } + }, + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Mastodon/Resources/Assets.xcassets/Colors/Label/secondary.colorset/Contents.json b/Mastodon/Resources/Assets.xcassets/Colors/Label/secondary.colorset/Contents.json index 70b1446d..104dfd02 100644 --- a/Mastodon/Resources/Assets.xcassets/Colors/Label/secondary.colorset/Contents.json +++ b/Mastodon/Resources/Assets.xcassets/Colors/Label/secondary.colorset/Contents.json @@ -22,10 +22,10 @@ "color" : { "color-space" : "srgb", "components" : { - "alpha" : "0.600", - "blue" : "0xF5", - "green" : "0xEB", - "red" : "0xEB" + "alpha" : "1.000", + "blue" : "0xAD", + "green" : "0x9D", + "red" : "0x97" } }, "idiom" : "universal" diff --git a/Mastodon/Resources/Assets.xcassets/Scene/Onboarding/Contents.json b/Mastodon/Resources/Assets.xcassets/Scene/Onboarding/Contents.json new file mode 100644 index 00000000..6e965652 --- /dev/null +++ b/Mastodon/Resources/Assets.xcassets/Scene/Onboarding/Contents.json @@ -0,0 +1,9 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "provides-namespace" : true + } +} diff --git a/Mastodon/Resources/Assets.xcassets/Scene/Onboarding/avatar.placeholder.imageset/Contents.json b/Mastodon/Resources/Assets.xcassets/Scene/Onboarding/avatar.placeholder.imageset/Contents.json new file mode 100644 index 00000000..2b84d06b --- /dev/null +++ b/Mastodon/Resources/Assets.xcassets/Scene/Onboarding/avatar.placeholder.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "filename" : "Frame 82.jpg", + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "Frame 82@2x.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "Frame 82@3x.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Mastodon/Resources/Assets.xcassets/Scene/Onboarding/avatar.placeholder.imageset/Frame 82.jpg b/Mastodon/Resources/Assets.xcassets/Scene/Onboarding/avatar.placeholder.imageset/Frame 82.jpg new file mode 100644 index 00000000..7819c97b Binary files /dev/null and b/Mastodon/Resources/Assets.xcassets/Scene/Onboarding/avatar.placeholder.imageset/Frame 82.jpg differ diff --git a/Mastodon/Resources/Assets.xcassets/Scene/Onboarding/avatar.placeholder.imageset/Frame 82@2x.png b/Mastodon/Resources/Assets.xcassets/Scene/Onboarding/avatar.placeholder.imageset/Frame 82@2x.png new file mode 100644 index 00000000..31f1bdf6 Binary files /dev/null and b/Mastodon/Resources/Assets.xcassets/Scene/Onboarding/avatar.placeholder.imageset/Frame 82@2x.png differ diff --git a/Mastodon/Resources/Assets.xcassets/Scene/Onboarding/avatar.placeholder.imageset/Frame 82@3x.png b/Mastodon/Resources/Assets.xcassets/Scene/Onboarding/avatar.placeholder.imageset/Frame 82@3x.png new file mode 100644 index 00000000..68603b22 Binary files /dev/null and b/Mastodon/Resources/Assets.xcassets/Scene/Onboarding/avatar.placeholder.imageset/Frame 82@3x.png differ diff --git a/Mastodon/Resources/Assets.xcassets/Scene/Onboarding/navigation.back.button.background.colorset/Contents.json b/Mastodon/Resources/Assets.xcassets/Scene/Onboarding/navigation.back.button.background.colorset/Contents.json new file mode 100644 index 00000000..b7d63ece --- /dev/null +++ b/Mastodon/Resources/Assets.xcassets/Scene/Onboarding/navigation.back.button.background.colorset/Contents.json @@ -0,0 +1,38 @@ +{ + "colors" : [ + { + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "0xFF", + "green" : "0xFF", + "red" : "0xFF" + } + }, + "idiom" : "universal" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "dark" + } + ], + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "0.200", + "blue" : "0x80", + "green" : "0x78", + "red" : "0x78" + } + }, + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Mastodon/Resources/Assets.xcassets/Scene/Onboarding/navigation.back.button.background.highlighted.colorset/Contents.json b/Mastodon/Resources/Assets.xcassets/Scene/Onboarding/navigation.back.button.background.highlighted.colorset/Contents.json new file mode 100644 index 00000000..7136040b --- /dev/null +++ b/Mastodon/Resources/Assets.xcassets/Scene/Onboarding/navigation.back.button.background.highlighted.colorset/Contents.json @@ -0,0 +1,38 @@ +{ + "colors" : [ + { + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "0xE5", + "green" : "0xE5", + "red" : "0xE5" + } + }, + "idiom" : "universal" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "dark" + } + ], + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "0.400", + "blue" : "0x80", + "green" : "0x78", + "red" : "0x78" + } + }, + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Mastodon/Resources/Assets.xcassets/Scene/Onboarding/navigation.next.button.background.colorset/Contents.json b/Mastodon/Resources/Assets.xcassets/Scene/Onboarding/navigation.next.button.background.colorset/Contents.json new file mode 100644 index 00000000..17ed9364 --- /dev/null +++ b/Mastodon/Resources/Assets.xcassets/Scene/Onboarding/navigation.next.button.background.colorset/Contents.json @@ -0,0 +1,38 @@ +{ + "colors" : [ + { + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "0x37", + "green" : "0x2C", + "red" : "0x28" + } + }, + "idiom" : "universal" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "dark" + } + ], + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "0xEE", + "green" : "0xEE", + "red" : "0xEE" + } + }, + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Mastodon/Resources/Assets.xcassets/Scene/Onboarding/navigation.next.button.background.highlighted.colorset/Contents.json b/Mastodon/Resources/Assets.xcassets/Scene/Onboarding/navigation.next.button.background.highlighted.colorset/Contents.json new file mode 100644 index 00000000..706cd755 --- /dev/null +++ b/Mastodon/Resources/Assets.xcassets/Scene/Onboarding/navigation.next.button.background.highlighted.colorset/Contents.json @@ -0,0 +1,38 @@ +{ + "colors" : [ + { + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "0x1B", + "green" : "0x15", + "red" : "0x13" + } + }, + "idiom" : "universal" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "dark" + } + ], + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "0xBA", + "green" : "0xBA", + "red" : "0xBA" + } + }, + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Mastodon/Resources/Assets.xcassets/Scene/Onboarding/onboarding.background.colorset/Contents.json b/Mastodon/Resources/Assets.xcassets/Scene/Onboarding/onboarding.background.colorset/Contents.json new file mode 100644 index 00000000..0b219c90 --- /dev/null +++ b/Mastodon/Resources/Assets.xcassets/Scene/Onboarding/onboarding.background.colorset/Contents.json @@ -0,0 +1,38 @@ +{ + "colors" : [ + { + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "0xF7", + "green" : "0xF2", + "red" : "0xF2" + } + }, + "idiom" : "universal" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "dark" + } + ], + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "0x21", + "green" : "0x1B", + "red" : "0x19" + } + }, + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Mastodon/Resources/Assets.xcassets/Scene/Onboarding/search.bar.background.colorset/Contents.json b/Mastodon/Resources/Assets.xcassets/Scene/Onboarding/search.bar.background.colorset/Contents.json new file mode 100644 index 00000000..f16bb02f --- /dev/null +++ b/Mastodon/Resources/Assets.xcassets/Scene/Onboarding/search.bar.background.colorset/Contents.json @@ -0,0 +1,38 @@ +{ + "colors" : [ + { + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "0.200", + "blue" : "0x80", + "green" : "0x78", + "red" : "0x78" + } + }, + "idiom" : "universal" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "dark" + } + ], + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "0.240", + "blue" : "0x80", + "green" : "0x76", + "red" : "0x76" + } + }, + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Mastodon/Resources/Assets.xcassets/Scene/Onboarding/textField.background.colorset/Contents.json b/Mastodon/Resources/Assets.xcassets/Scene/Onboarding/textField.background.colorset/Contents.json new file mode 100644 index 00000000..147cca83 --- /dev/null +++ b/Mastodon/Resources/Assets.xcassets/Scene/Onboarding/textField.background.colorset/Contents.json @@ -0,0 +1,38 @@ +{ + "colors" : [ + { + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "1.000", + "green" : "1.000", + "red" : "1.000" + } + }, + "idiom" : "universal" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "dark" + } + ], + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "0x37", + "green" : "0x2C", + "red" : "0x28" + } + }, + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Mastodon/Resources/Assets.xcassets/Scene/Welcome/illustration/cloud.base.extend.imageset/Contents.json b/Mastodon/Resources/Assets.xcassets/Scene/Welcome/illustration/cloud.base.extend.imageset/Contents.json new file mode 100644 index 00000000..421e01a3 --- /dev/null +++ b/Mastodon/Resources/Assets.xcassets/Scene/Welcome/illustration/cloud.base.extend.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "filename" : "cloud.base.extend.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "cloud.base.extend@2x.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "cloud.base.extend@3x.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Mastodon/Resources/Assets.xcassets/Scene/Welcome/illustration/cloud.base.extend.imageset/cloud.base.extend.png b/Mastodon/Resources/Assets.xcassets/Scene/Welcome/illustration/cloud.base.extend.imageset/cloud.base.extend.png new file mode 100644 index 00000000..3c8443c9 Binary files /dev/null and b/Mastodon/Resources/Assets.xcassets/Scene/Welcome/illustration/cloud.base.extend.imageset/cloud.base.extend.png differ diff --git a/Mastodon/Resources/Assets.xcassets/Scene/Welcome/illustration/cloud.base.extend.imageset/cloud.base.extend@2x.png b/Mastodon/Resources/Assets.xcassets/Scene/Welcome/illustration/cloud.base.extend.imageset/cloud.base.extend@2x.png new file mode 100644 index 00000000..b03b6720 Binary files /dev/null and b/Mastodon/Resources/Assets.xcassets/Scene/Welcome/illustration/cloud.base.extend.imageset/cloud.base.extend@2x.png differ diff --git a/Mastodon/Resources/Assets.xcassets/Scene/Welcome/illustration/cloud.base.extend.imageset/cloud.base.extend@3x.png b/Mastodon/Resources/Assets.xcassets/Scene/Welcome/illustration/cloud.base.extend.imageset/cloud.base.extend@3x.png new file mode 100644 index 00000000..f7747685 Binary files /dev/null and b/Mastodon/Resources/Assets.xcassets/Scene/Welcome/illustration/cloud.base.extend.imageset/cloud.base.extend@3x.png differ diff --git a/Mastodon/Resources/Assets.xcassets/Scene/Welcome/illustration/elephant.three.on.grass.extend.imageset/Contents.json b/Mastodon/Resources/Assets.xcassets/Scene/Welcome/illustration/elephant.three.on.grass.extend.imageset/Contents.json new file mode 100644 index 00000000..9c3ea2de --- /dev/null +++ b/Mastodon/Resources/Assets.xcassets/Scene/Welcome/illustration/elephant.three.on.grass.extend.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "filename" : "elephant.three.on.grass.extend.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "elephant.three.on.grass.extend@2x.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "elephant.three.on.grass.extend@3x.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Mastodon/Resources/Assets.xcassets/Scene/Welcome/illustration/elephant.three.on.grass.extend.imageset/elephant.three.on.grass.extend.png b/Mastodon/Resources/Assets.xcassets/Scene/Welcome/illustration/elephant.three.on.grass.extend.imageset/elephant.three.on.grass.extend.png new file mode 100644 index 00000000..97ef8df6 Binary files /dev/null and b/Mastodon/Resources/Assets.xcassets/Scene/Welcome/illustration/elephant.three.on.grass.extend.imageset/elephant.three.on.grass.extend.png differ diff --git a/Mastodon/Resources/Assets.xcassets/Scene/Welcome/illustration/elephant.three.on.grass.extend.imageset/elephant.three.on.grass.extend@2x.png b/Mastodon/Resources/Assets.xcassets/Scene/Welcome/illustration/elephant.three.on.grass.extend.imageset/elephant.three.on.grass.extend@2x.png new file mode 100644 index 00000000..63580f3c Binary files /dev/null and b/Mastodon/Resources/Assets.xcassets/Scene/Welcome/illustration/elephant.three.on.grass.extend.imageset/elephant.three.on.grass.extend@2x.png differ diff --git a/Mastodon/Resources/Assets.xcassets/Scene/Welcome/illustration/elephant.three.on.grass.extend.imageset/elephant.three.on.grass.extend@3x.png b/Mastodon/Resources/Assets.xcassets/Scene/Welcome/illustration/elephant.three.on.grass.extend.imageset/elephant.three.on.grass.extend@3x.png new file mode 100644 index 00000000..8799a731 Binary files /dev/null and b/Mastodon/Resources/Assets.xcassets/Scene/Welcome/illustration/elephant.three.on.grass.extend.imageset/elephant.three.on.grass.extend@3x.png differ diff --git a/Mastodon/Resources/Assets.xcassets/Scene/Welcome/sign.in.button.background.colorset/Contents.json b/Mastodon/Resources/Assets.xcassets/Scene/Welcome/sign.in.button.background.colorset/Contents.json new file mode 100644 index 00000000..7bf1f1e4 --- /dev/null +++ b/Mastodon/Resources/Assets.xcassets/Scene/Welcome/sign.in.button.background.colorset/Contents.json @@ -0,0 +1,20 @@ +{ + "colors" : [ + { + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "0x81", + "green" : "0xAC", + "red" : "0x58" + } + }, + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Mastodon/Resources/Assets.xcassets/Theme/system/Background/content.warning.overlay.background.colorset/Contents.json b/Mastodon/Resources/Assets.xcassets/Theme/system/Background/content.warning.overlay.background.colorset/Contents.json index d211d7df..c8aa45b5 100644 --- a/Mastodon/Resources/Assets.xcassets/Theme/system/Background/content.warning.overlay.background.colorset/Contents.json +++ b/Mastodon/Resources/Assets.xcassets/Theme/system/Background/content.warning.overlay.background.colorset/Contents.json @@ -5,9 +5,9 @@ "color-space" : "srgb", "components" : { "alpha" : "1.000", - "blue" : "0.922", - "green" : "0.898", - "red" : "0.867" + "blue" : "0xEB", + "green" : "0xE4", + "red" : "0xDD" } }, "idiom" : "universal" diff --git a/Mastodon/Resources/Assets.xcassets/Theme/system/Background/secondary.system.background.colorset/Contents.json b/Mastodon/Resources/Assets.xcassets/Theme/system/Background/secondary.system.background.colorset/Contents.json index 77d24b11..14441ef0 100644 --- a/Mastodon/Resources/Assets.xcassets/Theme/system/Background/secondary.system.background.colorset/Contents.json +++ b/Mastodon/Resources/Assets.xcassets/Theme/system/Background/secondary.system.background.colorset/Contents.json @@ -5,9 +5,9 @@ "color-space" : "srgb", "components" : { "alpha" : "1.000", - "blue" : "0.910", - "green" : "0.882", - "red" : "0.851" + "blue" : "0xE8", + "green" : "0xE0", + "red" : "0xD9" } }, "idiom" : "universal" diff --git a/Mastodon/Resources/Assets.xcassets/Theme/system/Background/system.grouped.background.colorset/Contents.json b/Mastodon/Resources/Assets.xcassets/Theme/system/Background/system.grouped.background.colorset/Contents.json index 370a745e..daac70e0 100644 --- a/Mastodon/Resources/Assets.xcassets/Theme/system/Background/system.grouped.background.colorset/Contents.json +++ b/Mastodon/Resources/Assets.xcassets/Theme/system/Background/system.grouped.background.colorset/Contents.json @@ -5,9 +5,9 @@ "color-space" : "srgb", "components" : { "alpha" : "1.000", - "blue" : "0.910", - "green" : "0.882", - "red" : "0.851" + "blue" : "0xE8", + "green" : "0xE0", + "red" : "0xD9" } }, "idiom" : "universal" diff --git a/Mastodon/Scene/HomeTimeline/AsyncHomeTimeline/AsyncHomeTimelineViewController+DebugAction.swift b/Mastodon/Scene/HomeTimeline/AsyncHomeTimeline/AsyncHomeTimelineViewController+DebugAction.swift deleted file mode 100644 index 19c3244c..00000000 --- a/Mastodon/Scene/HomeTimeline/AsyncHomeTimeline/AsyncHomeTimelineViewController+DebugAction.swift +++ /dev/null @@ -1,384 +0,0 @@ -// -// AsyncHomeTimelineViewController+DebugAction.swift -// Mastodon -// -// Created by MainasuK Cirno on 2021-6-21. -// - -#if ASDK && DEBUG - -import os.log -import UIKit -import CoreData -import CoreDataStack -import FLEX - -extension AsyncHomeTimelineViewController { - var debugMenu: UIMenu { - let menu = UIMenu( - title: "Debug Tools", - image: nil, - identifier: nil, - options: .displayInline, - children: [ - UIAction(title: "Show FLEX", image: nil, attributes: [], handler: { [weak self] action in - guard let self = self else { return } - self.showFLEXAction(action) - }), - moveMenu, - dropMenu, - UIAction(title: "Show Welcome", image: UIImage(systemName: "figure.walk"), attributes: []) { [weak self] action in - guard let self = self else { return } - self.showWelcomeAction(action) - }, - UIAction(title: "Show Or Remove EmptyView", image: UIImage(systemName: "clear"), attributes: []) { [weak self] action in - guard let self = self else { return } - if self.emptyView.superview != nil { - self.emptyView.removeFromSuperview() - } else { - self.showEmptyView() - } - }, - UIAction(title: "Show Public Timeline", image: UIImage(systemName: "list.dash"), attributes: []) { [weak self] action in - guard let self = self else { return } - self.showPublicTimelineAction(action) - }, - UIAction(title: "Show Profile", image: UIImage(systemName: "person.crop.circle"), attributes: []) { [weak self] action in - guard let self = self else { return } - self.showProfileAction(action) - }, - UIAction(title: "Show Thread", image: UIImage(systemName: "bubble.left.and.bubble.right"), attributes: []) { [weak self] action in - guard let self = self else { return } - self.showThreadAction(action) - }, - UIAction(title: "Settings", image: UIImage(systemName: "gear"), attributes: []) { [weak self] action in - guard let self = self else { return } - self.showSettings(action) - }, - UIAction(title: "Sign Out", image: UIImage(systemName: "escape"), attributes: .destructive) { [weak self] action in - guard let self = self else { return } - self.signOutAction(action) - } - ] - ) - return menu - } - - var moveMenu: UIMenu { - return UIMenu( - title: "Move to…", - image: UIImage(systemName: "arrow.forward.circle"), - identifier: nil, - options: [], - children: [ - UIAction(title: "First Gap", image: nil, attributes: [], handler: { [weak self] action in - guard let self = self else { return } - self.moveToTopGapAction(action) - }), - UIAction(title: "First Replied Status", image: nil, attributes: [], handler: { [weak self] action in - guard let self = self else { return } - self.moveToFirstRepliedStatus(action) - }), - UIAction(title: "First Reblog Status", image: nil, attributes: [], handler: { [weak self] action in - guard let self = self else { return } - self.moveToFirstReblogStatus(action) - }), - UIAction(title: "First Poll Status", image: nil, attributes: [], handler: { [weak self] action in - guard let self = self else { return } - self.moveToFirstPollStatus(action) - }), - UIAction(title: "First Audio Status", image: nil, attributes: [], handler: { [weak self] action in - guard let self = self else { return } - self.moveToFirstAudioStatus(action) - }), - UIAction(title: "First Video Status", image: nil, attributes: [], handler: { [weak self] action in - guard let self = self else { return } - self.moveToFirstVideoStatus(action) - }), - UIAction(title: "First GIF status", image: nil, attributes: [], handler: { [weak self] action in - guard let self = self else { return } - self.moveToFirstGIFStatus(action) - }), - ] - ) - } - - var dropMenu: UIMenu { - return UIMenu( - title: "Drop…", - image: UIImage(systemName: "minus.circle"), - identifier: nil, - options: [], - children: [10, 50, 100, 150, 200, 250, 300].map { count in - UIAction(title: "Drop Recent \(count) Statuses", image: nil, attributes: [], handler: { [weak self] action in - guard let self = self else { return } - self.dropRecentStatusAction(action, count: count) - }) - } - ) - } -} - -extension AsyncHomeTimelineViewController { - - @objc private func showFLEXAction(_ sender: UIAction) { - FLEXManager.shared.showExplorer() - } - - @objc private func moveToTopGapAction(_ sender: UIAction) { - guard let diffableDataSource = viewModel.diffableDataSource else { return } - let snapshotTransitioning = diffableDataSource.snapshot() - let item = snapshotTransitioning.itemIdentifiers.first(where: { item in - switch item { - case .homeMiddleLoader: return true - default: return false - } - }) - if let targetItem = item, let index = snapshotTransitioning.indexOfItem(targetItem) { - tableView.scrollToRow(at: IndexPath(row: index, section: 0), at: .middle, animated: true) - } - } - - @objc private func moveToFirstReblogStatus(_ sender: UIAction) { - guard let diffableDataSource = viewModel.diffableDataSource else { return } - let snapshotTransitioning = diffableDataSource.snapshot() - let item = snapshotTransitioning.itemIdentifiers.first(where: { item in - switch item { - case .homeTimelineIndex(let objectID, _): - let homeTimelineIndex = viewModel.fetchedResultsController.managedObjectContext.object(with: objectID) as! HomeTimelineIndex - return homeTimelineIndex.status.reblog != nil - default: - return false - } - }) - if let targetItem = item, let index = snapshotTransitioning.indexOfItem(targetItem) { - tableView.scrollToRow(at: IndexPath(row: index, section: 0), at: .middle, animated: true) - tableView.blinkRow(at: IndexPath(row: index, section: 0)) - } else { - print("Not found reblog status") - } - } - - @objc private func moveToFirstPollStatus(_ sender: UIAction) { - guard let diffableDataSource = viewModel.diffableDataSource else { return } - let snapshotTransitioning = diffableDataSource.snapshot() - let item = snapshotTransitioning.itemIdentifiers.first(where: { item in - switch item { - case .homeTimelineIndex(let objectID, _): - let homeTimelineIndex = viewModel.fetchedResultsController.managedObjectContext.object(with: objectID) as! HomeTimelineIndex - let post = homeTimelineIndex.status.reblog ?? homeTimelineIndex.status - return post.poll != nil - default: - return false - } - }) - if let targetItem = item, let index = snapshotTransitioning.indexOfItem(targetItem) { - tableView.scrollToRow(at: IndexPath(row: index, section: 0), at: .middle, animated: true) - tableView.blinkRow(at: IndexPath(row: index, section: 0)) - } else { - print("Not found poll status") - } - } - - @objc private func moveToFirstRepliedStatus(_ sender: UIAction) { - guard let diffableDataSource = viewModel.diffableDataSource else { return } - let snapshotTransitioning = diffableDataSource.snapshot() - let item = snapshotTransitioning.itemIdentifiers.first(where: { item in - switch item { - case .homeTimelineIndex(let objectID, _): - let homeTimelineIndex = viewModel.fetchedResultsController.managedObjectContext.object(with: objectID) as! HomeTimelineIndex - guard homeTimelineIndex.status.inReplyToID != nil else { - return false - } - return true - default: - return false - } - }) - if let targetItem = item, let index = snapshotTransitioning.indexOfItem(targetItem) { - tableView.scrollToRow(at: IndexPath(row: index, section: 0), at: .middle, animated: true) - tableView.blinkRow(at: IndexPath(row: index, section: 0)) - } else { - print("Not found replied status") - } - } - - @objc private func moveToFirstAudioStatus(_ sender: UIAction) { - guard let diffableDataSource = viewModel.diffableDataSource else { return } - let snapshotTransitioning = diffableDataSource.snapshot() - let item = snapshotTransitioning.itemIdentifiers.first(where: { item in - switch item { - case .homeTimelineIndex(let objectID, _): - let homeTimelineIndex = viewModel.fetchedResultsController.managedObjectContext.object(with: objectID) as! HomeTimelineIndex - let status = homeTimelineIndex.status.reblog ?? homeTimelineIndex.status - return status.mediaAttachments?.contains(where: { $0.type == .audio }) ?? false - default: - return false - } - }) - if let targetItem = item, let index = snapshotTransitioning.indexOfItem(targetItem) { - tableView.scrollToRow(at: IndexPath(row: index, section: 0), at: .middle, animated: true) - tableView.blinkRow(at: IndexPath(row: index, section: 0)) - } else { - print("Not found audio status") - } - } - - @objc private func moveToFirstVideoStatus(_ sender: UIAction) { - guard let diffableDataSource = viewModel.diffableDataSource else { return } - let snapshotTransitioning = diffableDataSource.snapshot() - let item = snapshotTransitioning.itemIdentifiers.first(where: { item in - switch item { - case .homeTimelineIndex(let objectID, _): - let homeTimelineIndex = viewModel.fetchedResultsController.managedObjectContext.object(with: objectID) as! HomeTimelineIndex - let status = homeTimelineIndex.status.reblog ?? homeTimelineIndex.status - return status.mediaAttachments?.contains(where: { $0.type == .video }) ?? false - default: - return false - } - }) - if let targetItem = item, let index = snapshotTransitioning.indexOfItem(targetItem) { - tableView.scrollToRow(at: IndexPath(row: index, section: 0), at: .middle, animated: true) - tableView.blinkRow(at: IndexPath(row: index, section: 0)) - } else { - print("Not found video status") - } - } - - @objc private func moveToFirstGIFStatus(_ sender: UIAction) { - guard let diffableDataSource = viewModel.diffableDataSource else { return } - let snapshotTransitioning = diffableDataSource.snapshot() - let item = snapshotTransitioning.itemIdentifiers.first(where: { item in - switch item { - case .homeTimelineIndex(let objectID, _): - let homeTimelineIndex = viewModel.fetchedResultsController.managedObjectContext.object(with: objectID) as! HomeTimelineIndex - let status = homeTimelineIndex.status.reblog ?? homeTimelineIndex.status - return status.mediaAttachments?.contains(where: { $0.type == .gifv }) ?? false - default: - return false - } - }) - if let targetItem = item, let index = snapshotTransitioning.indexOfItem(targetItem) { - tableView.scrollToRow(at: IndexPath(row: index, section: 0), at: .middle, animated: true) - tableView.blinkRow(at: IndexPath(row: index, section: 0)) - } else { - print("Not found GIF status") - } - } - - @objc private func dropRecentStatusAction(_ sender: UIAction, count: Int) { - guard let diffableDataSource = viewModel.diffableDataSource else { return } - let snapshotTransitioning = diffableDataSource.snapshot() - - let droppingObjectIDs = snapshotTransitioning.itemIdentifiers.prefix(count).compactMap { item -> NSManagedObjectID? in - switch item { - case .homeTimelineIndex(let objectID, _): return objectID - default: return nil - } - } - var droppingStatusObjectIDs: [NSManagedObjectID] = [] - context.apiService.backgroundManagedObjectContext.performChanges { [weak self] in - guard let self = self else { return } - for objectID in droppingObjectIDs { - guard let homeTimelineIndex = try? self.context.apiService.backgroundManagedObjectContext.existingObject(with: objectID) as? HomeTimelineIndex else { continue } - droppingStatusObjectIDs.append(homeTimelineIndex.status.objectID) - self.context.apiService.backgroundManagedObjectContext.delete(homeTimelineIndex) - } - } - .sink { [weak self] result in - guard let self = self else { return } - switch result { - case .success: - self.context.apiService.backgroundManagedObjectContext.performChanges { [weak self] in - guard let self = self else { return } - for objectID in droppingStatusObjectIDs { - guard let post = try? self.context.apiService.backgroundManagedObjectContext.existingObject(with: objectID) as? Status else { continue } - self.context.apiService.backgroundManagedObjectContext.delete(post) - } - } - .sink { _ in - // do nothing - } - .store(in: &self.disposeBag) - case .failure(let error): - assertionFailure(error.localizedDescription) - } - } - .store(in: &disposeBag) - } - - @objc private func showWelcomeAction(_ sender: UIAction) { - coordinator.present(scene: .welcome, from: self, transition: .modal(animated: true, completion: nil)) - } - - @objc private func showPublicTimelineAction(_ sender: UIAction) { - coordinator.present(scene: .publicTimeline, from: self, transition: .show) - } - - @objc private func showProfileAction(_ sender: UIAction) { - let alertController = UIAlertController(title: "Enter User ID", message: nil, preferredStyle: .alert) - alertController.addTextField() - let showAction = UIAlertAction(title: "Show", style: .default) { [weak self, weak alertController] _ in - guard let self = self else { return } - guard let textField = alertController?.textFields?.first else { return } - let profileViewModel = RemoteProfileViewModel(context: self.context, userID: textField.text ?? "") - self.coordinator.present(scene: .profile(viewModel: profileViewModel), from: self, transition: .show) - } - alertController.addAction(showAction) - let cancelAction = UIAlertAction(title: "Cancel", style: .cancel, handler: nil) - alertController.addAction(cancelAction) - coordinator.present(scene: .alertController(alertController: alertController), from: self, transition: .alertController(animated: true, completion: nil)) - } - - @objc private func showThreadAction(_ sender: UIAction) { - let alertController = UIAlertController(title: "Enter Status ID", message: nil, preferredStyle: .alert) - alertController.addTextField() - let showAction = UIAlertAction(title: "Show", style: .default) { [weak self, weak alertController] _ in - guard let self = self else { return } - guard let textField = alertController?.textFields?.first else { return } - let threadViewModel = RemoteThreadViewModel(context: self.context, statusID: textField.text ?? "") - self.coordinator.present(scene: .thread(viewModel: threadViewModel), from: self, transition: .show) - } - alertController.addAction(showAction) - let cancelAction = UIAlertAction(title: "Cancel", style: .cancel, handler: nil) - alertController.addAction(cancelAction) - coordinator.present(scene: .alertController(alertController: alertController), from: self, transition: .alertController(animated: true, completion: nil)) - } - - @objc private func showSettings(_ sender: UIAction) { - guard let currentSetting = context.settingService.currentSetting.value else { return } - let settingsViewModel = SettingsViewModel(context: context, setting: currentSetting) - coordinator.present( - scene: .settings(viewModel: settingsViewModel), - from: self, - transition: .modal(animated: true, completion: nil) - ) - } - - @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) - } -} - -#endif diff --git a/Mastodon/Scene/HomeTimeline/AsyncHomeTimeline/AsyncHomeTimelineViewController+Provider.swift b/Mastodon/Scene/HomeTimeline/AsyncHomeTimeline/AsyncHomeTimelineViewController+Provider.swift deleted file mode 100644 index 5f97ebea..00000000 --- a/Mastodon/Scene/HomeTimeline/AsyncHomeTimeline/AsyncHomeTimelineViewController+Provider.swift +++ /dev/null @@ -1,123 +0,0 @@ -// -// AsyncHomeTimelineViewController+Provider.swift -// Mastodon -// -// Created by MainasuK Cirno on 2021-6-21. -// - -#if ASDK - -import os.log -import UIKit -import Combine -import CoreData -import CoreDataStack -import AsyncDisplayKit - -// MARK: - StatusProvider -extension AsyncHomeTimelineViewController: StatusProvider { - - func status() -> Future { - return Future { promise in promise(.success(nil)) } - } - - func status(for cell: UITableViewCell?, indexPath: IndexPath?) -> Future { - return Future { promise in - guard let diffableDataSource = self.viewModel.diffableDataSource else { - assertionFailure() - promise(.success(nil)) - return - } - guard let indexPath = indexPath ?? cell.flatMap({ self.tableView.indexPath(for: $0) }), - let item = diffableDataSource.itemIdentifier(for: indexPath) else { - promise(.success(nil)) - return - } - - switch item { - case .homeTimelineIndex(let objectID, _): - let managedObjectContext = self.viewModel.fetchedResultsController.managedObjectContext - managedObjectContext.perform { - let timelineIndex = managedObjectContext.object(with: objectID) as? HomeTimelineIndex - promise(.success(timelineIndex?.status)) - } - default: - promise(.success(nil)) - } - } - } - - func status(for cell: UICollectionViewCell) -> Future { - return Future { promise in promise(.success(nil)) } - } - - var managedObjectContext: NSManagedObjectContext { - return viewModel.fetchedResultsController.managedObjectContext - } - - var tableViewDiffableDataSource: UITableViewDiffableDataSource? { - return nil - } - - func item(for cell: UITableViewCell?, indexPath: IndexPath?) -> Item? { - guard let diffableDataSource = self.viewModel.diffableDataSource else { - assertionFailure() - return nil - } - - guard let indexPath = indexPath ?? cell.flatMap({ self.tableView.indexPath(for: $0) }), - let item = diffableDataSource.itemIdentifier(for: indexPath) else { - return nil - } - - return item - } - - func items(indexPaths: [IndexPath]) -> [Item] { - guard let diffableDataSource = self.viewModel.diffableDataSource else { - assertionFailure() - return [] - } - - var items: [Item] = [] - for indexPath in indexPaths { - guard let item = diffableDataSource.itemIdentifier(for: indexPath) else { continue } - items.append(item) - } - return items - } - - func status(node: ASCellNode?, indexPath: IndexPath?) -> Status? { - guard let diffableDataSource = self.viewModel.diffableDataSource else { - assertionFailure() - return nil - } - - guard let indexPath = indexPath ?? node.flatMap({ self.node.indexPath(for: $0) }), - let item = diffableDataSource.itemIdentifier(for: indexPath) else { - return nil - } - - switch item { - case .homeTimelineIndex(let objectID, _): - guard let homeTimelineIndex = try? viewModel.fetchedResultsController.managedObjectContext.existingObject(with: objectID) as? HomeTimelineIndex else { - assertionFailure() - return nil - } - return homeTimelineIndex.status - default: - return nil - } - } - - func statusObjectItems(indexPaths: [IndexPath]) -> [StatusObjectItem] { - guard let diffableDataSource = self.viewModel.diffableDataSource else { return [] } - let items = indexPaths.compactMap { diffableDataSource.itemIdentifier(for: $0)?.statusObjectItem } - return items - } - -} - -extension AsyncHomeTimelineViewController: UserProvider {} - -#endif diff --git a/Mastodon/Scene/HomeTimeline/AsyncHomeTimeline/AsyncHomeTimelineViewController.swift b/Mastodon/Scene/HomeTimeline/AsyncHomeTimeline/AsyncHomeTimelineViewController.swift deleted file mode 100644 index c90b703e..00000000 --- a/Mastodon/Scene/HomeTimeline/AsyncHomeTimeline/AsyncHomeTimelineViewController.swift +++ /dev/null @@ -1,573 +0,0 @@ -// -// AsyncHomeTimelineViewController.swift -// Mastodon -// -// Created by MainasuK Cirno on 2021-6-21. -// - -#if ASDK - -import os.log -import UIKit -import AVKit -import Combine -import CoreData -import CoreDataStack -import GameplayKit -import MastodonSDK -import AlamofireImage -import AsyncDisplayKit - -final class AsyncHomeTimelineViewController: ASDKViewController, NeedsDependency, MediaPreviewableViewController { - - weak var context: AppContext! { willSet { precondition(!isViewLoaded) } } - weak var coordinator: SceneCoordinator! { willSet { precondition(!isViewLoaded) } } - - var disposeBag = Set() - private(set) lazy var viewModel = AsyncHomeTimelineViewModel(context: context) - - let mediaPreviewTransitionController = MediaPreviewTransitionController() - - lazy var emptyView: UIStackView = { - let emptyView = UIStackView() - emptyView.axis = .vertical - emptyView.distribution = .fill - emptyView.layoutMargins = UIEdgeInsets(top: 0, left: 20, bottom: 54, right: 20) - emptyView.isLayoutMarginsRelativeArrangement = true - return emptyView - }() - - let titleView = HomeTimelineNavigationBarTitleView() - - let settingBarButtonItem: UIBarButtonItem = { - let barButtonItem = UIBarButtonItem() - barButtonItem.tintColor = Asset.Colors.brandBlue.color - barButtonItem.image = UIImage(systemName: "gear")?.withRenderingMode(.alwaysTemplate) - return barButtonItem - }() - - let composeBarButtonItem: UIBarButtonItem = { - let barButtonItem = UIBarButtonItem() - barButtonItem.tintColor = Asset.Colors.brandBlue.color - barButtonItem.image = UIImage(systemName: "square.and.pencil")?.withRenderingMode(.alwaysTemplate) - return barButtonItem - }() - - var tableView: UITableView { node.view } - - let publishProgressView: UIProgressView = { - let progressView = UIProgressView(progressViewStyle: .bar) - progressView.alpha = 0 - return progressView - }() - - let refreshControl = UIRefreshControl() - - - override init() { - super.init(node: ASTableNode()) - } - - required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - deinit { - os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s:", ((#file as NSString).lastPathComponent), #line, #function) - } - -} - -extension AsyncHomeTimelineViewController { - - override func viewDidLoad() { - super.viewDidLoad() - - node.allowsSelection = true - - title = L10n.Scene.HomeTimeline.title - view.backgroundColor = ThemeService.shared.currentTheme.value.secondarySystemBackgroundColor - navigationItem.leftBarButtonItem = settingBarButtonItem - navigationItem.titleView = titleView - titleView.delegate = self - - viewModel.homeTimelineNavigationBarTitleViewModel.state - .removeDuplicates() - .receive(on: DispatchQueue.main) - .sink { [weak self] state in - guard let self = self else { return } - self.titleView.configure(state: state) - } - .store(in: &disposeBag) - - #if DEBUG - // long press to trigger debug menu - settingBarButtonItem.menu = debugMenu - #else - settingBarButtonItem.target = self - settingBarButtonItem.action = #selector(AsyncHomeTimelineViewController.settingBarButtonItemPressed(_:)) - #endif - - navigationItem.rightBarButtonItem = composeBarButtonItem - composeBarButtonItem.target = self - composeBarButtonItem.action = #selector(AsyncHomeTimelineViewController.composeBarButtonItemPressed(_:)) - - node.view.refreshControl = refreshControl - refreshControl.addTarget(self, action: #selector(AsyncHomeTimelineViewController.refreshControlValueChanged(_:)), for: .valueChanged) -// -// tableView.translatesAutoresizingMaskIntoConstraints = false -// view.addSubview(tableView) -// NSLayoutConstraint.activate([ -// tableView.topAnchor.constraint(equalTo: view.topAnchor), -// tableView.leadingAnchor.constraint(equalTo: view.leadingAnchor), -// tableView.trailingAnchor.constraint(equalTo: view.trailingAnchor), -// tableView.bottomAnchor.constraint(equalTo: view.bottomAnchor), -// ]) -// -// publishProgressView.translatesAutoresizingMaskIntoConstraints = false -// view.addSubview(publishProgressView) -// NSLayoutConstraint.activate([ -// publishProgressView.topAnchor.constraint(equalTo: view.layoutMarginsGuide.topAnchor), -// publishProgressView.leadingAnchor.constraint(equalTo: view.leadingAnchor), -// publishProgressView.trailingAnchor.constraint(equalTo: view.trailingAnchor), -// ]) -// -// viewModel.tableView = tableView - viewModel.tableNode = node - viewModel.contentOffsetAdjustableTimelineViewControllerDelegate = self - node.delegate = self - viewModel.setupDiffableDataSource( - tableNode: node, - dependency: self, - statusTableViewCellDelegate: self, - timelineMiddleLoaderTableViewCellDelegate: self - ) - - -// tableView.delegate = self -// tableView.prefetchDataSource = self - - // bind refresh control - viewModel.isFetchingLatestTimeline - .receive(on: DispatchQueue.main) - .sink { [weak self] isFetching in - guard let self = self else { return } - if !isFetching { - UIView.animate(withDuration: 0.5) { [weak self] in - guard let self = self else { return } - self.refreshControl.endRefreshing() - } completion: { _ in } - } - } - .store(in: &disposeBag) - -// viewModel.homeTimelineNavigationBarTitleViewModel.publishingProgress -// .receive(on: DispatchQueue.main) -// .sink { [weak self] progress in -// guard let self = self else { return } -// guard progress > 0 else { -// let dismissAnimator = UIViewPropertyAnimator(duration: 0.1, curve: .easeInOut) -// dismissAnimator.addAnimations { -// self.publishProgressView.alpha = 0 -// } -// dismissAnimator.addCompletion { _ in -// self.publishProgressView.setProgress(0, animated: false) -// } -// dismissAnimator.startAnimation() -// return -// } -// if self.publishProgressView.alpha == 0 { -// let progressAnimator = UIViewPropertyAnimator(duration: 0.1, curve: .easeOut) -// progressAnimator.addAnimations { -// self.publishProgressView.alpha = 1 -// } -// progressAnimator.startAnimation() -// } -// -// self.publishProgressView.setProgress(progress, animated: true) -// } -// .store(in: &disposeBag) -// -// viewModel.timelineIsEmpty -// .receive(on: DispatchQueue.main) -// .sink { [weak self] isEmpty in -// if isEmpty { -// self?.showEmptyView() -// } else { -// self?.emptyView.removeFromSuperview() -// } -// } -// .store(in: &disposeBag) - } - - override func viewWillAppear(_ animated: Bool) { - super.viewWillAppear(animated) - -// aspectViewWillAppear(animated) -// -// // needs trigger manually after onboarding dismiss -// setNeedsStatusBarAppearanceUpdate() -// -// if (viewModel.fetchedResultsController.fetchedObjects ?? []).isEmpty { -// viewModel.loadLatestStateMachine.enter(HomeTimelineViewModel.LoadLatestState.Loading.self) -// } - } - - override func viewDidAppear(_ animated: Bool) { - super.viewDidAppear(animated) - -// viewModel.viewDidAppear.send() -// -// DispatchQueue.main.async { [weak self] in -// guard let self = self else { return } -// if (self.viewModel.fetchedResultsController.fetchedObjects ?? []).count == 0 { -// self.viewModel.loadLatestStateMachine.enter(HomeTimelineViewModel.LoadLatestState.Loading.self) -// } -// } - } - - override func viewDidDisappear(_ animated: Bool) { - super.viewDidDisappear(animated) - -// aspectViewDidDisappear(animated) - } - - override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) { - super.viewWillTransition(to: size, with: coordinator) - -// coordinator.animate { _ in -// // do nothing -// } completion: { _ in -// // fix AutoLayout cell height not update after rotate issue -// self.viewModel.cellFrameCache.removeAllObjects() -// self.tableView.reloadData() -// } - } -} - -extension AsyncHomeTimelineViewController { - func showEmptyView() { - if emptyView.superview != nil { - return - } - view.addSubview(emptyView) - emptyView.translatesAutoresizingMaskIntoConstraints = false - NSLayoutConstraint.activate([ - emptyView.leadingAnchor.constraint(equalTo: view.readableContentGuide.leadingAnchor), - emptyView.trailingAnchor.constraint(equalTo: view.readableContentGuide.trailingAnchor), - emptyView.bottomAnchor.constraint(equalTo: view.readableContentGuide.bottomAnchor) - ]) - - if emptyView.arrangedSubviews.count > 0 { - return - } - let findPeopleButton: PrimaryActionButton = { - let button = PrimaryActionButton() - button.setTitle(L10n.Common.Controls.Actions.findPeople, for: .normal) - button.addTarget(self, action: #selector(AsyncHomeTimelineViewController.findPeopleButtonPressed(_:)), for: .touchUpInside) - return button - }() - NSLayoutConstraint.activate([ - findPeopleButton.heightAnchor.constraint(equalToConstant: 46) - ]) - - let manuallySearchButton: HighlightDimmableButton = { - let button = HighlightDimmableButton() - button.titleLabel?.font = UIFontMetrics(forTextStyle: .headline).scaledFont(for: .systemFont(ofSize: 15, weight: .semibold)) - button.setTitle(L10n.Common.Controls.Actions.manuallySearch, for: .normal) - button.setTitleColor(Asset.Colors.brandBlue.color, for: .normal) - button.addTarget(self, action: #selector(AsyncHomeTimelineViewController.manuallySearchButtonPressed(_:)), for: .touchUpInside) - return button - }() - - emptyView.addArrangedSubview(findPeopleButton) - emptyView.setCustomSpacing(17, after: findPeopleButton) - emptyView.addArrangedSubview(manuallySearchButton) - - } -} - -extension AsyncHomeTimelineViewController { - - @objc private func findPeopleButtonPressed(_ sender: PrimaryActionButton) { - let viewModel = SuggestionAccountViewModel(context: context) - viewModel.delegate = self.viewModel - coordinator.present(scene: .suggestionAccount(viewModel: viewModel), from: self, transition: .modal(animated: true, completion: nil)) - } - - @objc private func manuallySearchButtonPressed(_ sender: UIButton) { - coordinator.switchToTabBar(tab: .search) - } - - @objc private func settingBarButtonItemPressed(_ sender: UIBarButtonItem) { - os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s", ((#file as NSString).lastPathComponent), #line, #function) - guard let setting = context.settingService.currentSetting.value else { return } - let settingsViewModel = SettingsViewModel(context: context, setting: setting) - coordinator.present(scene: .settings(viewModel: settingsViewModel), from: self, transition: .modal(animated: true, completion: nil)) - } - - @objc private func composeBarButtonItemPressed(_ sender: UIBarButtonItem) { - os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s", ((#file as NSString).lastPathComponent), #line, #function) - let composeViewModel = ComposeViewModel(context: context, composeKind: .post) - coordinator.present(scene: .compose(viewModel: composeViewModel), from: self, transition: .modal(animated: true, completion: nil)) - } - - @objc private func refreshControlValueChanged(_ sender: UIRefreshControl) { - guard viewModel.loadLatestStateMachine.enter(HomeTimelineViewModel.LoadLatestState.Loading.self) else { - sender.endRefreshing() - return - } - } - -} - -// MARK: - StatusTableViewControllerAspect -//extension AsyncHomeTimelineViewController: StatusTableViewControllerAspect { } - -//extension AsyncHomeTimelineViewController: TableViewCellHeightCacheableContainer { -// var cellFrameCache: NSCache { return viewModel.cellFrameCache } -//} - -// MARK: - UIScrollViewDelegate -extension AsyncHomeTimelineViewController { - func scrollViewDidScroll(_ scrollView: UIScrollView) { - - //aspectScrollViewDidScroll(scrollView) - viewModel.homeTimelineNavigationBarTitleViewModel.handleScrollViewDidScroll(scrollView) - } -} - -//extension AsyncHomeTimelineViewController: LoadMoreConfigurableTableViewContainer { -// typealias BottomLoaderTableViewCell = TimelineBottomLoaderTableViewCell -// typealias LoadingState = HomeTimelineViewModel.LoadOldestState.Loading -// var loadMoreConfigurableTableView: UITableView { return tableView } -// var loadMoreConfigurableStateMachine: GKStateMachine { return viewModel.loadOldestStateMachine } -//} - -// MARK: - UITableViewDelegate -//extension AsyncHomeTimelineViewController: UITableViewDelegate { -// -// func tableView(_ tableView: UITableView, estimatedHeightForRowAt indexPath: IndexPath) -> CGFloat { -// aspectTableView(tableView, estimatedHeightForRowAt: indexPath) -// } -// -// func tableView(_ tableView: UITableView, willDisplay cell: UITableViewCell, forRowAt indexPath: IndexPath) { -// aspectTableView(tableView, willDisplay: cell, forRowAt: indexPath) -// } -// -// func tableView(_ tableView: UITableView, didEndDisplaying cell: UITableViewCell, forRowAt indexPath: IndexPath) { -// aspectTableView(tableView, didEndDisplaying: cell, forRowAt: indexPath) -// } -// -// func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { -// aspectTableView(tableView, didSelectRowAt: indexPath) -// } -// -// func tableView(_ tableView: UITableView, contextMenuConfigurationForRowAt indexPath: IndexPath, point: CGPoint) -> UIContextMenuConfiguration? { -// return aspectTableView(tableView, contextMenuConfigurationForRowAt: indexPath, point: point) -// } -// -// func tableView(_ tableView: UITableView, previewForHighlightingContextMenuWithConfiguration configuration: UIContextMenuConfiguration) -> UITargetedPreview? { -// return aspectTableView(tableView, previewForHighlightingContextMenuWithConfiguration: configuration) -// } -// -// func tableView(_ tableView: UITableView, previewForDismissingContextMenuWithConfiguration configuration: UIContextMenuConfiguration) -> UITargetedPreview? { -// return aspectTableView(tableView, previewForDismissingContextMenuWithConfiguration: configuration) -// } -// -// func tableView(_ tableView: UITableView, willPerformPreviewActionForMenuWith configuration: UIContextMenuConfiguration, animator: UIContextMenuInteractionCommitAnimating) { -// aspectTableView(tableView, willPerformPreviewActionForMenuWith: configuration, animator: animator) -// } -// -//} - -// MARK: - UITableViewDataSourcePrefetching -//extension AsyncHomeTimelineViewController: UITableViewDataSourcePrefetching { -// func tableView(_ tableView: UITableView, prefetchRowsAt indexPaths: [IndexPath]) { -// aspectTableView(tableView, prefetchRowsAt: indexPaths) -// } -//} - -// MARK: - ContentOffsetAdjustableTimelineViewControllerDelegate -extension AsyncHomeTimelineViewController: ContentOffsetAdjustableTimelineViewControllerDelegate { - func navigationBar() -> UINavigationBar? { - return navigationController?.navigationBar - } -} - -// MARK: - TimelineMiddleLoaderTableViewCellDelegate -extension AsyncHomeTimelineViewController: TimelineMiddleLoaderTableViewCellDelegate { - func configure(cell: TimelineMiddleLoaderTableViewCell, upperTimelineStatusID: String?, timelineIndexobjectID: NSManagedObjectID?) { - guard let upperTimelineIndexObjectID = timelineIndexobjectID else { - return - } - viewModel.loadMiddleSateMachineList - .receive(on: DispatchQueue.main) - .sink { [weak self] ids in - guard let _ = self else { return } - if let stateMachine = ids[upperTimelineIndexObjectID] { - guard let state = stateMachine.currentState else { - assertionFailure() - return - } - - // make success state same as loading due to snapshot updating delay - let isLoading = state is HomeTimelineViewModel.LoadMiddleState.Loading || state is HomeTimelineViewModel.LoadMiddleState.Success - if isLoading { - cell.startAnimating() - } else { - cell.stopAnimating() - } - } else { - cell.stopAnimating() - } - } - .store(in: &cell.disposeBag) - - var dict = viewModel.loadMiddleSateMachineList.value - if let _ = dict[upperTimelineIndexObjectID] { - // do nothing - } else { - let stateMachine = GKStateMachine(states: [ - AsyncHomeTimelineViewModel.LoadMiddleState.Initial(viewModel: viewModel, upperTimelineIndexObjectID: upperTimelineIndexObjectID), - AsyncHomeTimelineViewModel.LoadMiddleState.Loading(viewModel: viewModel, upperTimelineIndexObjectID: upperTimelineIndexObjectID), - AsyncHomeTimelineViewModel.LoadMiddleState.Fail(viewModel: viewModel, upperTimelineIndexObjectID: upperTimelineIndexObjectID), - AsyncHomeTimelineViewModel.LoadMiddleState.Success(viewModel: viewModel, upperTimelineIndexObjectID: upperTimelineIndexObjectID), - ]) - stateMachine.enter(HomeTimelineViewModel.LoadMiddleState.Initial.self) - dict[upperTimelineIndexObjectID] = stateMachine - viewModel.loadMiddleSateMachineList.value = dict - } - } - - func timelineMiddleLoaderTableViewCell(_ cell: TimelineMiddleLoaderTableViewCell, loadMoreButtonDidPressed button: UIButton) { - guard let diffableDataSource = viewModel.diffableDataSource else { return } - guard let indexPath = tableView.indexPath(for: cell) else { return } - guard let item = diffableDataSource.itemIdentifier(for: indexPath) else { return } - - switch item { - case .homeMiddleLoader(let upper): - guard let stateMachine = viewModel.loadMiddleSateMachineList.value[upper] else { - assertionFailure() - return - } - stateMachine.enter(HomeTimelineViewModel.LoadMiddleState.Loading.self) - default: - assertionFailure() - } - } -} - -// MARK: - ScrollViewContainer -extension AsyncHomeTimelineViewController: ScrollViewContainer { - - var scrollView: UIScrollView { return tableView } - - func scrollToTop(animated: Bool) { - if scrollView.contentOffset.y < scrollView.frame.height, - viewModel.loadLatestStateMachine.canEnterState(HomeTimelineViewModel.LoadLatestState.Loading.self), - (scrollView.contentOffset.y + scrollView.adjustedContentInset.top) == 0.0, - !refreshControl.isRefreshing { - scrollView.scrollRectToVisible(CGRect(origin: CGPoint(x: 0, y: -refreshControl.frame.height), size: CGSize(width: 1, height: 1)), animated: animated) - DispatchQueue.main.async { [weak self] in - guard let self = self else { return } - self.refreshControl.beginRefreshing() - self.refreshControl.sendActions(for: .valueChanged) - } - } else { - let indexPath = IndexPath(row: 0, section: 0) - guard viewModel.diffableDataSource?.itemIdentifier(for: indexPath) != nil else { return } - node.scrollToRow(at: indexPath, at: .top, animated: true) - } - } - -} - -// MARK: - AVPlayerViewControllerDelegate -extension AsyncHomeTimelineViewController: AVPlayerViewControllerDelegate { - - func playerViewController(_ playerViewController: AVPlayerViewController, willBeginFullScreenPresentationWithAnimationCoordinator coordinator: UIViewControllerTransitionCoordinator) { - handlePlayerViewController(playerViewController, willBeginFullScreenPresentationWithAnimationCoordinator: coordinator) - } - - func playerViewController(_ playerViewController: AVPlayerViewController, willEndFullScreenPresentationWithAnimationCoordinator coordinator: UIViewControllerTransitionCoordinator) { - handlePlayerViewController(playerViewController, willEndFullScreenPresentationWithAnimationCoordinator: coordinator) - } - -} - -// MARK: - StatusTableViewCellDelegate -extension AsyncHomeTimelineViewController: StatusTableViewCellDelegate { - weak var playerViewControllerDelegate: AVPlayerViewControllerDelegate? { return self } - func parent() -> UIViewController { return self } -} - -// MARK: - HomeTimelineNavigationBarTitleViewDelegate -extension AsyncHomeTimelineViewController: HomeTimelineNavigationBarTitleViewDelegate { - func homeTimelineNavigationBarTitleView(_ titleView: HomeTimelineNavigationBarTitleView, logoButtonDidPressed sender: UIButton) { - scrollToTop(animated: true) - } - - func homeTimelineNavigationBarTitleView(_ titleView: HomeTimelineNavigationBarTitleView, buttonDidPressed sender: UIButton) { - switch titleView.state { - case .newPostButton: - guard let diffableDataSource = viewModel.diffableDataSource else { return } - let indexPath = IndexPath(row: 0, section: 0) - guard diffableDataSource.itemIdentifier(for: indexPath) != nil else { return } - node.scrollToRow(at: indexPath, at: .top, animated: true) - case .offlineButton: - // TODO: retry - break - case .publishedButton: - break - default: - break - } - } -} - -extension AsyncHomeTimelineViewController { - override var keyCommands: [UIKeyCommand]? { - return navigationKeyCommands + statusNavigationKeyCommands - } -} - -// MARK: - StatusTableViewControllerNavigateable -extension AsyncHomeTimelineViewController: StatusTableViewControllerNavigateable { - @objc func navigateKeyCommandHandlerRelay(_ sender: UIKeyCommand) { - navigateKeyCommandHandler(sender) - } - - @objc func statusKeyCommandHandlerRelay(_ sender: UIKeyCommand) { - statusKeyCommandHandler(sender) - } -} - - -// MARK: - ASTableDelegate -extension AsyncHomeTimelineViewController: ASTableDelegate { - func shouldBatchFetch(for tableNode: ASTableNode) -> Bool { - switch viewModel.loadLatestStateMachine.currentState { - case is HomeTimelineViewModel.LoadOldestState.NoMore: - return false - default: - return true - } - } - - func tableNode(_ tableNode: ASTableNode, willBeginBatchFetchWith context: ASBatchContext) { - viewModel.loadLatestStateMachine.enter(HomeTimelineViewModel.LoadOldestState.Loading.self) - context.completeBatchFetching(true) - } - - func tableNode(_ tableNode: ASTableNode, willDisplayRowWith node: ASCellNode) { - if let statusNode = node as? StatusNode { - statusNode.delegate = self - } - } -} - -// MARK: - StatusNodeDelegate -extension AsyncHomeTimelineViewController: StatusNodeDelegate { } - -#endif diff --git a/Mastodon/Scene/HomeTimeline/AsyncHomeTimeline/AsyncHomeTimelineViewModel+Diffable.swift b/Mastodon/Scene/HomeTimeline/AsyncHomeTimeline/AsyncHomeTimelineViewModel+Diffable.swift deleted file mode 100644 index 7799c216..00000000 --- a/Mastodon/Scene/HomeTimeline/AsyncHomeTimeline/AsyncHomeTimelineViewModel+Diffable.swift +++ /dev/null @@ -1,159 +0,0 @@ -// -// AsyncHomeTimelineViewModel+Diffable.swift -// Mastodon -// -// Created by MainasuK Cirno on 2021-6-21. -// - -#if ASDK - -import os.log -import UIKit -import CoreData -import CoreDataStack -import AsyncDisplayKit -import DifferenceKit -import DiffableDataSources - -extension AsyncHomeTimelineViewModel { - - func setupDiffableDataSource( - tableNode: ASTableNode, - dependency: NeedsDependency, - statusTableViewCellDelegate: StatusTableViewCellDelegate, - timelineMiddleLoaderTableViewCellDelegate: TimelineMiddleLoaderTableViewCellDelegate - ) { - tableNode.automaticallyAdjustsContentOffset = true - - diffableDataSource = StatusSection.tableNodeDiffableDataSource( - tableNode: tableNode, - managedObjectContext: fetchedResultsController.managedObjectContext - ) - - var snapshot = DiffableDataSourceSnapshot() - snapshot.appendSections([.main]) - diffableDataSource?.apply(snapshot) - } - -} - -// MARK: - NSFetchedResultsControllerDelegate -extension AsyncHomeTimelineViewModel: NSFetchedResultsControllerDelegate { - - func controllerWillChangeContent(_ controller: NSFetchedResultsController) { - os_log("%{public}s[%{public}ld], %{public}s", ((#file as NSString).lastPathComponent), #line, #function) - } - - func controller(_ controller: NSFetchedResultsController, didChangeContentWith snapshot: NSDiffableDataSourceSnapshotReference) { - os_log("%{public}s[%{public}ld], %{public}s", ((#file as NSString).lastPathComponent), #line, #function) - - guard let diffableDataSource = self.diffableDataSource else { return } - let oldSnapshot = diffableDataSource.snapshot() - - let predicate = fetchedResultsController.fetchRequest.predicate - let parentManagedObjectContext = fetchedResultsController.managedObjectContext - let managedObjectContext = NSManagedObjectContext(concurrencyType: .privateQueueConcurrencyType) - managedObjectContext.parent = parentManagedObjectContext - - managedObjectContext.perform { - var shouldAddBottomLoader = false - - let timelineIndexes: [HomeTimelineIndex] = { - let request = HomeTimelineIndex.sortedFetchRequest - request.returnsObjectsAsFaults = false - request.predicate = predicate - do { - return try managedObjectContext.fetch(request) - } catch { - assertionFailure(error.localizedDescription) - return [] - } - }() - - // that's will be the most fastest fetch because of upstream just update and no modify needs consider - - var oldSnapshotAttributeDict: [NSManagedObjectID : Item.StatusAttribute] = [:] - - for item in oldSnapshot.itemIdentifiers { - guard case let .homeTimelineIndex(objectID, attribute) = item else { continue } - oldSnapshotAttributeDict[objectID] = attribute - } - - var newTimelineItems: [Item] = [] - - for (i, timelineIndex) in timelineIndexes.enumerated() { - let attribute = oldSnapshotAttributeDict[timelineIndex.objectID] ?? Item.StatusAttribute() - attribute.isSeparatorLineHidden = false - - // append new item into snapshot - newTimelineItems.append(.homeTimelineIndex(objectID: timelineIndex.objectID, attribute: attribute)) - - let isLast = i == timelineIndexes.count - 1 - switch (isLast, timelineIndex.hasMore) { - case (false, true): - newTimelineItems.append(.homeMiddleLoader(upperTimelineIndexAnchorObjectID: timelineIndex.objectID)) - attribute.isSeparatorLineHidden = true - case (true, true): - shouldAddBottomLoader = true - default: - break - } - } // end for - - var newSnapshot = DiffableDataSourceSnapshot() - newSnapshot.appendSections([.main]) - newSnapshot.appendItems(newTimelineItems, toSection: .main) - - let endSnapshot = CACurrentMediaTime() - - if shouldAddBottomLoader, !(self.loadLatestStateMachine.currentState is LoadOldestState.NoMore) { - newSnapshot.appendItems([.bottomLoader], toSection: .main) - } - - diffableDataSource.apply(newSnapshot, animatingDifferences: false) { - DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { - self.isFetchingLatestTimeline.value = false - } - - let end = CACurrentMediaTime() - os_log("%{public}s[%{public}ld], %{public}s: calculate home timeline layout cost %.2fs", ((#file as NSString).lastPathComponent), #line, #function, end - endSnapshot) - } - } // end perform - } - - private struct Difference { - let item: T - let sourceIndexPath: IndexPath - let targetIndexPath: IndexPath - let offset: CGFloat - } - - private func calculateReloadSnapshotDifference( - navigationBar: UINavigationBar, - tableView: UITableView, - oldSnapshot: DiffableDataSourceSnapshot, - newSnapshot: DiffableDataSourceSnapshot - ) -> Difference? { - guard oldSnapshot.numberOfItems != 0 else { return nil } - - // old snapshot not empty. set source index path to first item if not match - let sourceIndexPath = UIViewController.topVisibleTableViewCellIndexPath(in: tableView, navigationBar: navigationBar) ?? IndexPath(row: 0, section: 0) - - guard sourceIndexPath.row < oldSnapshot.itemIdentifiers(inSection: .main).count else { return nil } - - let timelineItem = oldSnapshot.itemIdentifiers(inSection: .main)[sourceIndexPath.row] - guard let itemIndex = newSnapshot.itemIdentifiers(inSection: .main).firstIndex(of: timelineItem) else { return nil } - let targetIndexPath = IndexPath(row: itemIndex, section: 0) - - let offset = UIViewController.tableViewCellOriginOffsetToWindowTop(in: tableView, at: sourceIndexPath, navigationBar: navigationBar) - return Difference( - item: timelineItem, - sourceIndexPath: sourceIndexPath, - targetIndexPath: targetIndexPath, - offset: offset - ) - } - -} - -#endif diff --git a/Mastodon/Scene/HomeTimeline/AsyncHomeTimeline/AsyncHomeTimelineViewModel+LoadLatestState.swift b/Mastodon/Scene/HomeTimeline/AsyncHomeTimeline/AsyncHomeTimelineViewModel+LoadLatestState.swift deleted file mode 100644 index 4d73eae5..00000000 --- a/Mastodon/Scene/HomeTimeline/AsyncHomeTimeline/AsyncHomeTimelineViewModel+LoadLatestState.swift +++ /dev/null @@ -1,134 +0,0 @@ -// -// AsyncHomeTimelineViewModel+LoadLatestState.swift -// Mastodon -// -// Created by MainasuK Cirno on 2021-6-21. -// -// - -#if ASDK - -import os.log -import func QuartzCore.CACurrentMediaTime -import Foundation -import CoreData -import CoreDataStack -import GameplayKit - -extension AsyncHomeTimelineViewModel { - class LoadLatestState: GKState { - weak var viewModel: AsyncHomeTimelineViewModel? - - init(viewModel: AsyncHomeTimelineViewModel) { - self.viewModel = viewModel - } - - override func didEnter(from previousState: GKState?) { - os_log("%{public}s[%{public}ld], %{public}s: enter %s, previous: %s", ((#file as NSString).lastPathComponent), #line, #function, self.debugDescription, previousState.debugDescription) - viewModel?.loadLatestStateMachinePublisher.send(self) - } - } -} - -extension AsyncHomeTimelineViewModel.LoadLatestState { - class Initial: AsyncHomeTimelineViewModel.LoadLatestState { - override func isValidNextState(_ stateClass: AnyClass) -> Bool { - return stateClass == Loading.self - } - } - - class Loading: AsyncHomeTimelineViewModel.LoadLatestState { - override func isValidNextState(_ stateClass: AnyClass) -> Bool { - return stateClass == Fail.self || stateClass == Idle.self - } - - override func didEnter(from previousState: GKState?) { - super.didEnter(from: previousState) - guard let viewModel = viewModel, let stateMachine = stateMachine else { return } - guard let activeMastodonAuthenticationBox = viewModel.context.authenticationService.activeMastodonAuthenticationBox.value else { - // sign out when loading will enter here - stateMachine.enter(Fail.self) - return - } - - let predicate = viewModel.fetchedResultsController.fetchRequest.predicate - let parentManagedObjectContext = viewModel.fetchedResultsController.managedObjectContext - let managedObjectContext = NSManagedObjectContext(concurrencyType: .privateQueueConcurrencyType) - managedObjectContext.parent = parentManagedObjectContext - - managedObjectContext.perform { - let start = CACurrentMediaTime() - let latestStatusIDs: [Status.ID] - let request = HomeTimelineIndex.sortedFetchRequest - request.returnsObjectsAsFaults = false - request.predicate = predicate - - do { - let timelineIndexes = try managedObjectContext.fetch(request) - let endFetch = CACurrentMediaTime() - os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s: collect timelineIndexes cost: %.2fs", ((#file as NSString).lastPathComponent), #line, #function, endFetch - start) - latestStatusIDs = timelineIndexes - .prefix(APIService.onceRequestStatusMaxCount) // avoid performance issue - .compactMap { timelineIndex in - timelineIndex.value(forKeyPath: #keyPath(HomeTimelineIndex.status.id)) as? Status.ID - } - } catch { - stateMachine.enter(Fail.self) - return - } - - let end = CACurrentMediaTime() - os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s: collect statuses id cost: %.2fs", ((#file as NSString).lastPathComponent), #line, #function, end - start) - - // TODO: only set large count when using Wi-Fi - viewModel.context.apiService.homeTimeline(domain: activeMastodonAuthenticationBox.domain, authorizationBox: activeMastodonAuthenticationBox) - .receive(on: DispatchQueue.main) - .sink { completion in - viewModel.homeTimelineNavigationBarTitleViewModel.receiveLoadingStateCompletion(completion) - switch completion { - case .failure(let error): - // TODO: handle error - viewModel.isFetchingLatestTimeline.value = false - os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s: fetch statuses failed. %s", ((#file as NSString).lastPathComponent), #line, #function, error.localizedDescription) - case .finished: - // handle isFetchingLatestTimeline in fetch controller delegate - break - } - - stateMachine.enter(Idle.self) - - } receiveValue: { response in - // stop refresher if no new statuses - let statuses = response.value - let newStatuses = statuses.filter { !latestStatusIDs.contains($0.id) } - os_log("%{public}s[%{public}ld], %{public}s: load %{public}ld new statuses", ((#file as NSString).lastPathComponent), #line, #function, newStatuses.count) - - if newStatuses.isEmpty { - viewModel.isFetchingLatestTimeline.value = false - } else { - if !latestStatusIDs.isEmpty { - viewModel.homeTimelineNavigationBarTitleViewModel.newPostsIncoming() - } - } - viewModel.timelineIsEmpty.value = latestStatusIDs.isEmpty && statuses.isEmpty - } - .store(in: &viewModel.disposeBag) - } - } - } - - class Fail: AsyncHomeTimelineViewModel.LoadLatestState { - override func isValidNextState(_ stateClass: AnyClass) -> Bool { - return stateClass == Loading.self || stateClass == Idle.self - } - } - - class Idle: AsyncHomeTimelineViewModel.LoadLatestState { - override func isValidNextState(_ stateClass: AnyClass) -> Bool { - return stateClass == Loading.self - } - } - -} - -#endif diff --git a/Mastodon/Scene/HomeTimeline/AsyncHomeTimeline/AsyncHomeTimelineViewModel+LoadMiddleState.swift b/Mastodon/Scene/HomeTimeline/AsyncHomeTimeline/AsyncHomeTimelineViewModel+LoadMiddleState.swift deleted file mode 100644 index f568a6aa..00000000 --- a/Mastodon/Scene/HomeTimeline/AsyncHomeTimeline/AsyncHomeTimelineViewModel+LoadMiddleState.swift +++ /dev/null @@ -1,112 +0,0 @@ -// -// AsyncHomeTimelineViewModel+LoadMiddleState.swift -// Mastodon -// -// Created by MainasuK Cirno on 2021-6-21. -// - -#if ASDK - -import os.log -import Foundation -import GameplayKit -import CoreData -import CoreDataStack - -extension AsyncHomeTimelineViewModel { - class LoadMiddleState: GKState { - weak var viewModel: AsyncHomeTimelineViewModel? - let upperTimelineIndexObjectID: NSManagedObjectID - - init(viewModel: AsyncHomeTimelineViewModel, upperTimelineIndexObjectID: NSManagedObjectID) { - self.viewModel = viewModel - self.upperTimelineIndexObjectID = upperTimelineIndexObjectID - } - - override func didEnter(from previousState: GKState?) { - os_log("%{public}s[%{public}ld], %{public}s: enter %s, previous: %s", ((#file as NSString).lastPathComponent), #line, #function, self.debugDescription, previousState.debugDescription) - guard let viewModel = viewModel, let stateMachine = stateMachine else { return } - var dict = viewModel.loadMiddleSateMachineList.value - dict[upperTimelineIndexObjectID] = stateMachine - viewModel.loadMiddleSateMachineList.value = dict // trigger value change - } - } -} - -extension AsyncHomeTimelineViewModel.LoadMiddleState { - - class Initial: AsyncHomeTimelineViewModel.LoadMiddleState { - override func isValidNextState(_ stateClass: AnyClass) -> Bool { - return stateClass == Loading.self - } - } - - class Loading: AsyncHomeTimelineViewModel.LoadMiddleState { - override func isValidNextState(_ stateClass: AnyClass) -> Bool { - // guard let viewModel = viewModel else { return false } - return stateClass == Success.self || stateClass == Fail.self - } - - override func didEnter(from previousState: GKState?) { - super.didEnter(from: previousState) - - guard let viewModel = viewModel, let stateMachine = stateMachine else { return } - guard let activeMastodonAuthenticationBox = viewModel.context.authenticationService.activeMastodonAuthenticationBox.value else { - stateMachine.enter(Fail.self) - return - } - - guard let timelineIndex = (viewModel.fetchedResultsController.fetchedObjects ?? []).first(where: { $0.objectID == upperTimelineIndexObjectID }) else { - stateMachine.enter(Fail.self) - return - } - let statusIDs = (viewModel.fetchedResultsController.fetchedObjects ?? []).compactMap { timelineIndex in - timelineIndex.status.id - } - - // TODO: only set large count when using Wi-Fi - let maxID = timelineIndex.status.id - viewModel.context.apiService.homeTimeline(domain: activeMastodonAuthenticationBox.domain,maxID: maxID, authorizationBox: activeMastodonAuthenticationBox) - .delay(for: .seconds(1), scheduler: DispatchQueue.main) - .receive(on: DispatchQueue.main) - .sink { completion in - viewModel.homeTimelineNavigationBarTitleViewModel.receiveLoadingStateCompletion(completion) - switch completion { - case .failure(let error): - // TODO: handle error - os_log("%{public}s[%{public}ld], %{public}s: fetch statuses failed. %s", ((#file as NSString).lastPathComponent), #line, #function, error.localizedDescription) - stateMachine.enter(Fail.self) - case .finished: - break - } - } receiveValue: { response in - let statuses = response.value - let newStatuses = statuses.filter { !statusIDs.contains($0.id) } - os_log("%{public}s[%{public}ld], %{public}s: load %{public}ld statuses, %{public}%ld new statuses", ((#file as NSString).lastPathComponent), #line, #function, statuses.count, newStatuses.count) - if newStatuses.isEmpty { - stateMachine.enter(Fail.self) - } else { - stateMachine.enter(Success.self) - } - } - .store(in: &viewModel.disposeBag) - } - } - - class Fail: AsyncHomeTimelineViewModel.LoadMiddleState { - override func isValidNextState(_ stateClass: AnyClass) -> Bool { - // guard let viewModel = viewModel else { return false } - return stateClass == Loading.self - } - } - - class Success: AsyncHomeTimelineViewModel.LoadMiddleState { - override func isValidNextState(_ stateClass: AnyClass) -> Bool { - // guard let viewModel = viewModel else { return false } - return false - } - } - -} - -#endif diff --git a/Mastodon/Scene/HomeTimeline/AsyncHomeTimeline/AsyncHomeTimelineViewModel+LoadOldestState.swift b/Mastodon/Scene/HomeTimeline/AsyncHomeTimeline/AsyncHomeTimelineViewModel+LoadOldestState.swift deleted file mode 100644 index 5743ab29..00000000 --- a/Mastodon/Scene/HomeTimeline/AsyncHomeTimeline/AsyncHomeTimelineViewModel+LoadOldestState.swift +++ /dev/null @@ -1,117 +0,0 @@ -// -// AsyncHomeTimelineViewModel+LoadOldestState.swift -// Mastodon -// -// Created by MainasuK Cirno on 2021-6-21. -// - -#if ASDK - -import os.log -import Foundation -import GameplayKit - -extension AsyncHomeTimelineViewModel { - class LoadOldestState: GKState { - weak var viewModel: AsyncHomeTimelineViewModel? - - init(viewModel: AsyncHomeTimelineViewModel) { - self.viewModel = viewModel - } - - override func didEnter(from previousState: GKState?) { - os_log("%{public}s[%{public}ld], %{public}s: enter %s, previous: %s", ((#file as NSString).lastPathComponent), #line, #function, self.debugDescription, previousState.debugDescription) - viewModel?.loadOldestStateMachinePublisher.send(self) - } - } -} - -extension AsyncHomeTimelineViewModel.LoadOldestState { - class Initial: AsyncHomeTimelineViewModel.LoadOldestState { - override func isValidNextState(_ stateClass: AnyClass) -> Bool { - guard let viewModel = viewModel else { return false } - guard !(viewModel.fetchedResultsController.fetchedObjects ?? []).isEmpty else { return false } - return stateClass == Loading.self - } - } - - class Loading: AsyncHomeTimelineViewModel.LoadOldestState { - override func isValidNextState(_ stateClass: AnyClass) -> Bool { - return stateClass == Fail.self || stateClass == Idle.self || stateClass == NoMore.self - } - - override func didEnter(from previousState: GKState?) { - super.didEnter(from: previousState) - guard let viewModel = viewModel, let stateMachine = stateMachine else { return } - guard let activeMastodonAuthenticationBox = viewModel.context.authenticationService.activeMastodonAuthenticationBox.value else { - assertionFailure() - stateMachine.enter(Fail.self) - return - } - - guard let last = viewModel.fetchedResultsController.fetchedObjects?.last else { - stateMachine.enter(Idle.self) - return - } - - // TODO: only set large count when using Wi-Fi - let maxID = last.status.id - viewModel.context.apiService.homeTimeline(domain: activeMastodonAuthenticationBox.domain, maxID: maxID, authorizationBox: activeMastodonAuthenticationBox) - .delay(for: .seconds(1), scheduler: DispatchQueue.main) - .receive(on: DispatchQueue.main) - .sink { completion in - viewModel.homeTimelineNavigationBarTitleViewModel.receiveLoadingStateCompletion(completion) - switch completion { - case .failure(let error): - os_log("%{public}s[%{public}ld], %{public}s: fetch statuses failed. %s", ((#file as NSString).lastPathComponent), #line, #function, error.localizedDescription) - case .finished: - // handle isFetchingLatestTimeline in fetch controller delegate - break - } - } receiveValue: { response in - let statuses = response.value - // enter no more state when no new statuses - if statuses.isEmpty || (statuses.count == 1 && statuses[0].id == maxID) { - stateMachine.enter(NoMore.self) - } else { - stateMachine.enter(Idle.self) - } - } - .store(in: &viewModel.disposeBag) - } - } - - class Fail: AsyncHomeTimelineViewModel.LoadOldestState { - override func isValidNextState(_ stateClass: AnyClass) -> Bool { - return stateClass == Loading.self || stateClass == Idle.self - } - } - - class Idle: AsyncHomeTimelineViewModel.LoadOldestState { - override func isValidNextState(_ stateClass: AnyClass) -> Bool { - return stateClass == Loading.self - } - } - - class NoMore: AsyncHomeTimelineViewModel.LoadOldestState { - override func isValidNextState(_ stateClass: AnyClass) -> Bool { - // reset state if needs - return stateClass == Idle.self - } - - override func didEnter(from previousState: GKState?) { - guard let viewModel = viewModel else { return } - guard let diffableDataSource = viewModel.diffableDataSource else { - assertionFailure() - return - } - DispatchQueue.main.async { - var snapshot = diffableDataSource.snapshot() - snapshot.deleteItems([.bottomLoader]) - diffableDataSource.apply(snapshot) - } - } - } -} - -#endif diff --git a/Mastodon/Scene/HomeTimeline/AsyncHomeTimeline/AsyncHomeTimelineViewModel.swift b/Mastodon/Scene/HomeTimeline/AsyncHomeTimeline/AsyncHomeTimelineViewModel.swift deleted file mode 100644 index d7ed0b10..00000000 --- a/Mastodon/Scene/HomeTimeline/AsyncHomeTimeline/AsyncHomeTimelineViewModel.swift +++ /dev/null @@ -1,151 +0,0 @@ -// -// AsyncHomeTimelineViewModel.swift -// Mastodon -// -// Created by MainasuK Cirno on 2021-6-21. -// -// - -#if ASDK - -import os.log -import func AVFoundation.AVMakeRect -import UIKit -import AVKit -import Combine -import CoreData -import CoreDataStack -import GameplayKit -import AlamofireImage -import DateToolsSwift -import AsyncDisplayKit - -final class AsyncHomeTimelineViewModel: NSObject { - - var disposeBag = Set() - var observations = Set() - - // input - let context: AppContext - let timelinePredicate = CurrentValueSubject(nil) - let fetchedResultsController: NSFetchedResultsController - let isFetchingLatestTimeline = CurrentValueSubject(false) - let viewDidAppear = PassthroughSubject() - let homeTimelineNavigationBarTitleViewModel: HomeTimelineNavigationBarTitleViewModel - - weak var tableNode: ASTableNode? - weak var contentOffsetAdjustableTimelineViewControllerDelegate: ContentOffsetAdjustableTimelineViewControllerDelegate? - //weak var tableView: UITableView? - weak var timelineMiddleLoaderTableViewCellDelegate: TimelineMiddleLoaderTableViewCellDelegate? - - let timelineIsEmpty = CurrentValueSubject(false) - let homeTimelineNeedRefresh = PassthroughSubject() - - // output - var diffableDataSource: TableNodeDiffableDataSource? - - // top loader - private(set) lazy var loadLatestStateMachine: GKStateMachine = { - // exclude timeline middle fetcher state - let stateMachine = GKStateMachine(states: [ - LoadLatestState.Initial(viewModel: self), - LoadLatestState.Loading(viewModel: self), - LoadLatestState.Fail(viewModel: self), - LoadLatestState.Idle(viewModel: self), - ]) - stateMachine.enter(LoadLatestState.Initial.self) - return stateMachine - }() - lazy var loadLatestStateMachinePublisher = CurrentValueSubject(nil) - // bottom loader - private(set) lazy var loadOldestStateMachine: GKStateMachine = { - // exclude timeline middle fetcher state - let stateMachine = GKStateMachine(states: [ - LoadOldestState.Initial(viewModel: self), - LoadOldestState.Loading(viewModel: self), - LoadOldestState.Fail(viewModel: self), - LoadOldestState.Idle(viewModel: self), - LoadOldestState.NoMore(viewModel: self), - ]) - stateMachine.enter(LoadOldestState.Initial.self) - return stateMachine - }() - lazy var loadOldestStateMachinePublisher = CurrentValueSubject(nil) - // middle loader - let loadMiddleSateMachineList = CurrentValueSubject<[NSManagedObjectID: GKStateMachine], Never>([:]) // TimelineIndex.objectID : middle loading state machine - // var diffableDataSource: UITableViewDiffableDataSource? - var cellFrameCache = NSCache() - - - init(context: AppContext) { - self.context = context - self.fetchedResultsController = { - let fetchRequest = HomeTimelineIndex.sortedFetchRequest - fetchRequest.fetchBatchSize = 20 - fetchRequest.returnsObjectsAsFaults = false - fetchRequest.relationshipKeyPathsForPrefetching = [#keyPath(HomeTimelineIndex.status)] - let controller = NSFetchedResultsController( - fetchRequest: fetchRequest, - managedObjectContext: context.managedObjectContext, - sectionNameKeyPath: nil, - cacheName: nil - ) - - return controller - }() - self.homeTimelineNavigationBarTitleViewModel = HomeTimelineNavigationBarTitleViewModel(context: context) - super.init() - - fetchedResultsController.delegate = self - - timelinePredicate - .receive(on: DispatchQueue.main) - .compactMap { $0 } - .first() // set once - .sink { [weak self] predicate in - guard let self = self else { return } - self.fetchedResultsController.fetchRequest.predicate = predicate - do { - try self.fetchedResultsController.performFetch() - } catch { - assertionFailure(error.localizedDescription) - } - } - .store(in: &disposeBag) - - context.authenticationService.activeMastodonAuthentication - .sink { [weak self] activeMastodonAuthentication in - guard let self = self else { return } - guard let mastodonAuthentication = activeMastodonAuthentication else { return } - let activeMastodonUserID = mastodonAuthentication.userID - let predicate = NSCompoundPredicate(andPredicateWithSubpredicates: [ - HomeTimelineIndex.predicate(userID: activeMastodonUserID), - HomeTimelineIndex.notDeleted() - ]) - self.timelinePredicate.value = predicate - } - .store(in: &disposeBag) - - homeTimelineNeedRefresh - .sink { [weak self] _ in - self?.loadLatestStateMachine.enter(LoadLatestState.Loading.self) - } - .store(in: &disposeBag) - - homeTimelineNavigationBarTitleViewModel.isPublished - .sink { [weak self] isPublished in - guard let self = self else { return } - self.homeTimelineNeedRefresh.send() - } - .store(in: &disposeBag) - } - - deinit { - os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s:", ((#file as NSString).lastPathComponent), #line, #function) - } - -} - -extension AsyncHomeTimelineViewModel: SuggestionAccountViewModelDelegate { } - -#endif diff --git a/Mastodon/Scene/Onboarding/ConfirmEmail/MastodonConfirmEmailViewController.swift b/Mastodon/Scene/Onboarding/ConfirmEmail/MastodonConfirmEmailViewController.swift index 0718938f..585dcb31 100644 --- a/Mastodon/Scene/Onboarding/ConfirmEmail/MastodonConfirmEmailViewController.swift +++ b/Mastodon/Scene/Onboarding/ConfirmEmail/MastodonConfirmEmailViewController.swift @@ -46,21 +46,11 @@ final class MastodonConfirmEmailViewController: UIViewController, NeedsDependenc imageView.contentMode = .scaleAspectFit return imageView }() - - let openEmailButton: UIButton = { - let button = PrimaryActionButton() - button.setTitle(L10n.Scene.ConfirmEmail.Button.openEmailApp, for: .normal) - button.addTarget(self, action: #selector(openEmailButtonPressed(_:)), for: UIControl.Event.touchUpInside) - return button - }() - - let dontReceiveButton: UIButton = { - let button = UIButton(type: .system) - button.titleLabel?.font = UIFontMetrics(forTextStyle: .headline).scaledFont(for: UIFont.boldSystemFont(ofSize: 15)) - button.setTitleColor(Asset.Colors.brandBlue.color, for: .normal) - button.setTitle(L10n.Scene.ConfirmEmail.Button.dontReceiveEmail, for: .normal) - button.addTarget(self, action: #selector(dontReceiveButtonPressed(_:)), for: UIControl.Event.touchUpInside) - return button + + let navigationActionView: NavigationActionView = { + let navigationActionView = NavigationActionView() + navigationActionView.backgroundColor = Asset.Scene.Onboarding.onboardingBackground.color + return navigationActionView }() deinit { @@ -73,6 +63,8 @@ extension MastodonConfirmEmailViewController { override func viewDidLoad() { + navigationItem.leftBarButtonItem = UIBarButtonItem() + setupOnboardingAppearance() configureTitleLabel() configureMargin() @@ -83,13 +75,12 @@ extension MastodonConfirmEmailViewController { stackView.spacing = 10 stackView.layoutMargins = UIEdgeInsets(top: 10, left: 0, bottom: 23, right: 0) stackView.isLayoutMarginsRelativeArrangement = true - stackView.addArrangedSubview(self.largeTitleLabel) - stackView.addArrangedSubview(self.subtitleLabel) - stackView.addArrangedSubview(self.emailImageView) + stackView.addArrangedSubview(largeTitleLabel) + stackView.addArrangedSubview(subtitleLabel) + stackView.addArrangedSubview(emailImageView) emailImageView.setContentHuggingPriority(.defaultLow, for: .vertical) emailImageView.setContentCompressionResistancePriority(.defaultLow, for: .vertical) - stackView.addArrangedSubview(self.openEmailButton) - stackView.addArrangedSubview(self.dontReceiveButton) + stackView.addArrangedSubview(navigationActionView) view.addSubview(stackView) stackView.translatesAutoresizingMaskIntoConstraints = false @@ -99,10 +90,7 @@ extension MastodonConfirmEmailViewController { stackView.trailingAnchor.constraint(equalTo: view.layoutMarginsGuide.trailingAnchor), stackView.bottomAnchor.constraint(equalTo: view.readableContentGuide.bottomAnchor), ]) - NSLayoutConstraint.activate([ - self.openEmailButton.heightAnchor.constraint(equalToConstant: 46), - ]) - + self.viewModel.timestampUpdatePublisher .sink { [weak self] _ in guard let self = self else { return } @@ -140,6 +128,13 @@ extension MastodonConfirmEmailViewController { .store(in: &self.disposeBag) } .store(in: &self.disposeBag) + + + navigationActionView.backButton.setTitle("Resend", for: .normal) // TODO: i18n + navigationActionView.backButton.addTarget(self, action: #selector(MastodonConfirmEmailViewController.resendButtonPressed(_:)), for: .touchUpInside) + + navigationActionView.nextButton.setTitle(L10n.Scene.ConfirmEmail.Button.openEmailApp, for: .normal) + navigationActionView.nextButton.addTarget(self, action: #selector(MastodonConfirmEmailViewController.openEmailButtonPressed(_:)), for: .touchUpInside) } override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) { @@ -190,7 +185,7 @@ extension MastodonConfirmEmailViewController { self.coordinator.present(scene: .alertController(alertController: alertController), from: self, transition: .alertController(animated: true, completion: nil)) } - @objc private func dontReceiveButtonPressed(_ sender: UIButton) { + @objc private func resendButtonPressed(_ sender: UIButton) { let alertController = UIAlertController(title: L10n.Scene.ConfirmEmail.DontReceiveEmail.title, message: L10n.Scene.ConfirmEmail.DontReceiveEmail.description, preferredStyle: .alert) let resendAction = UIAlertAction(title: L10n.Scene.ConfirmEmail.DontReceiveEmail.resendEmail, style: .default) { _ in let url = Mastodon.API.resendEmailURL(domain: self.viewModel.authenticateInfo.domain) diff --git a/Mastodon/Scene/Onboarding/PickServer/CollectionViewCell/PickServerCategoryCollectionViewCell.swift b/Mastodon/Scene/Onboarding/PickServer/CollectionViewCell/PickServerCategoryCollectionViewCell.swift index 9793d40f..89ca8267 100644 --- a/Mastodon/Scene/Onboarding/PickServer/CollectionViewCell/PickServerCategoryCollectionViewCell.swift +++ b/Mastodon/Scene/Onboarding/PickServer/CollectionViewCell/PickServerCategoryCollectionViewCell.swift @@ -8,14 +8,10 @@ import UIKit class PickServerCategoryCollectionViewCell: UICollectionViewCell { - + var observations = Set() - var categoryView: PickServerCategoryView = { - let view = PickServerCategoryView() - view.translatesAutoresizingMaskIntoConstraints = false - return view - }() + var categoryView = PickServerCategoryView() override func prepareForReuse() { super.prepareForReuse() @@ -35,13 +31,15 @@ class PickServerCategoryCollectionViewCell: UICollectionViewCell { extension PickServerCategoryCollectionViewCell { private func configure() { - contentView.addSubview(categoryView) + backgroundColor = .clear + categoryView.translatesAutoresizingMaskIntoConstraints = false + contentView.addSubview(categoryView) NSLayoutConstraint.activate([ + categoryView.topAnchor.constraint(equalTo: contentView.topAnchor), categoryView.leadingAnchor.constraint(equalTo: contentView.leadingAnchor), categoryView.trailingAnchor.constraint(equalTo: contentView.trailingAnchor), - categoryView.topAnchor.constraint(equalTo: contentView.topAnchor, constant: 10), - contentView.bottomAnchor.constraint(equalTo: categoryView.bottomAnchor, constant: 10), + contentView.bottomAnchor.constraint(equalTo: categoryView.bottomAnchor), ]) } } diff --git a/Mastodon/Scene/Onboarding/PickServer/MastodonPickServerViewController.swift b/Mastodon/Scene/Onboarding/PickServer/MastodonPickServerViewController.swift index f3570c6c..c4bbd5bc 100644 --- a/Mastodon/Scene/Onboarding/PickServer/MastodonPickServerViewController.swift +++ b/Mastodon/Scene/Onboarding/PickServer/MastodonPickServerViewController.swift @@ -14,6 +14,7 @@ import AuthenticationServices final class MastodonPickServerViewController: UIViewController, NeedsDependency { private var disposeBag = Set() + private var observations = Set() private var tableViewObservation: NSKeyValueObservation? weak var context: AppContext! { willSet { precondition(!isViewLoaded) } } @@ -31,21 +32,16 @@ final class MastodonPickServerViewController: UIViewController, NeedsDependency private let emptyStateView = PickServerEmptyStateView() private var emptyStateViewLeadingLayoutConstraint: NSLayoutConstraint! private var emptyStateViewTrailingLayoutConstraint: NSLayoutConstraint! - let tableViewTopPaddingView = UIView() // fix empty state view background display when tableView bounce scrolling - var tableViewTopPaddingViewHeightLayoutConstraint: NSLayoutConstraint! let tableView: UITableView = { let tableView = ControlContainableTableView() - tableView.register(PickServerTitleCell.self, forCellReuseIdentifier: String(describing: PickServerTitleCell.self)) - tableView.register(PickServerCategoriesCell.self, forCellReuseIdentifier: String(describing: PickServerCategoriesCell.self)) - tableView.register(PickServerSearchCell.self, forCellReuseIdentifier: String(describing: PickServerSearchCell.self)) + tableView.register(OnboardingHeadlineTableViewCell.self, forCellReuseIdentifier: String(describing: OnboardingHeadlineTableViewCell.self)) tableView.register(PickServerCell.self, forCellReuseIdentifier: String(describing: PickServerCell.self)) tableView.register(PickServerLoaderTableViewCell.self, forCellReuseIdentifier: String(describing: PickServerLoaderTableViewCell.self)) tableView.rowHeight = UITableView.automaticDimension tableView.separatorStyle = .none tableView.backgroundColor = .clear tableView.keyboardDismissMode = .onDrag - tableView.translatesAutoresizingMaskIntoConstraints = false if #available(iOS 15.0, *) { tableView.sectionHeaderTopPadding = .leastNonzeroMagnitude } else { @@ -54,14 +50,11 @@ final class MastodonPickServerViewController: UIViewController, NeedsDependency return tableView }() - let buttonContainer = UIView() - let nextStepButton: PrimaryActionButton = { - let button = PrimaryActionButton() - button.setTitle(L10n.Common.Controls.Actions.signUp, for: .normal) - button.translatesAutoresizingMaskIntoConstraints = false - return button + let navigationActionView: NavigationActionView = { + let navigationActionView = NavigationActionView() + navigationActionView.backgroundColor = Asset.Scene.Onboarding.onboardingBackground.color + return navigationActionView }() - var buttonContainerBottomLayoutConstraint: NSLayoutConstraint! var mastodonAuthenticationController: MastodonAuthenticationController? @@ -72,16 +65,15 @@ final class MastodonPickServerViewController: UIViewController, NeedsDependency } -extension MastodonPickServerViewController { - +extension MastodonPickServerViewController { override func viewDidLoad() { super.viewDidLoad() + navigationItem.leftBarButtonItem = UIBarButtonItem() + setupOnboardingAppearance() defer { setupNavigationBarBackgroundView() } - configureTitleLabel() - configureMargin() #if DEBUG navigationItem.rightBarButtonItem = UIBarButtonItem(image: UIImage(systemName: "ellipsis.circle"), style: .plain, target: nil, action: nil) @@ -94,26 +86,35 @@ extension MastodonPickServerViewController { navigationItem.rightBarButtonItem?.menu = UIMenu(title: "Debug Tool", image: nil, identifier: nil, options: [], children: children) #endif - buttonContainer.translatesAutoresizingMaskIntoConstraints = false - buttonContainer.preservesSuperviewLayoutMargins = true - view.addSubview(buttonContainer) - buttonContainerBottomLayoutConstraint = view.bottomAnchor.constraint(equalTo: buttonContainer.bottomAnchor, constant: 0).priority(.defaultHigh) + tableView.translatesAutoresizingMaskIntoConstraints = false + view.addSubview(tableView) NSLayoutConstraint.activate([ - buttonContainer.leadingAnchor.constraint(equalTo: view.leadingAnchor), - buttonContainer.trailingAnchor.constraint(equalTo: view.trailingAnchor), - view.safeAreaLayoutGuide.bottomAnchor.constraint(greaterThanOrEqualTo: buttonContainer.bottomAnchor, constant: WelcomeViewController.viewBottomPaddingHeight), - buttonContainerBottomLayoutConstraint, + tableView.topAnchor.constraint(equalTo: view.topAnchor), + tableView.leadingAnchor.constraint(equalTo: view.leadingAnchor), + tableView.trailingAnchor.constraint(equalTo: view.trailingAnchor), + tableView.bottomAnchor.constraint(equalTo: view.bottomAnchor), ]) - view.addSubview(nextStepButton) + navigationActionView.translatesAutoresizingMaskIntoConstraints = false + view.addSubview(navigationActionView) + defer { + view.bringSubviewToFront(navigationActionView) + } NSLayoutConstraint.activate([ - nextStepButton.topAnchor.constraint(equalTo: buttonContainer.topAnchor), - nextStepButton.leadingAnchor.constraint(equalTo: buttonContainer.layoutMarginsGuide.leadingAnchor), - buttonContainer.layoutMarginsGuide.trailingAnchor.constraint(equalTo: nextStepButton.trailingAnchor), - nextStepButton.bottomAnchor.constraint(equalTo: buttonContainer.bottomAnchor), - nextStepButton.heightAnchor.constraint(equalToConstant: MastodonPickServerViewController.actionButtonHeight).priority(.defaultHigh), + navigationActionView.leadingAnchor.constraint(equalTo: view.leadingAnchor), + navigationActionView.trailingAnchor.constraint(equalTo: view.trailingAnchor), + view.bottomAnchor.constraint(equalTo: navigationActionView.bottomAnchor), ]) - + + navigationActionView + .observe(\.bounds, options: [.initial, .new]) { [weak self] navigationActionView, _ in + guard let self = self else { return } + let inset = navigationActionView.frame.height + self.tableView.contentInset.bottom = inset + } + .store(in: &observations) + + // fix AutoLayout warning when observe before view appear viewModel.viewWillAppear .receive(on: DispatchQueue.main) @@ -125,26 +126,7 @@ extension MastodonPickServerViewController { } } .store(in: &disposeBag) - - tableViewTopPaddingView.translatesAutoresizingMaskIntoConstraints = false - view.addSubview(tableViewTopPaddingView) - tableViewTopPaddingViewHeightLayoutConstraint = tableViewTopPaddingView.heightAnchor.constraint(equalToConstant: 0.0).priority(.defaultHigh) - NSLayoutConstraint.activate([ - tableViewTopPaddingView.topAnchor.constraint(equalTo: view.layoutMarginsGuide.topAnchor), - tableViewTopPaddingView.leadingAnchor.constraint(equalTo: view.leadingAnchor), - tableViewTopPaddingView.trailingAnchor.constraint(equalTo: view.trailingAnchor), - tableViewTopPaddingViewHeightLayoutConstraint, - ]) - tableViewTopPaddingView.backgroundColor = Asset.Theme.Mastodon.systemGroupedBackground.color - - view.addSubview(tableView) - NSLayoutConstraint.activate([ - tableView.topAnchor.constraint(equalTo: view.topAnchor), - tableView.leadingAnchor.constraint(equalTo: view.leadingAnchor), - tableView.trailingAnchor.constraint(equalTo: view.trailingAnchor), - buttonContainer.topAnchor.constraint(equalTo: tableView.bottomAnchor, constant: 7), - ]) - + emptyStateView.translatesAutoresizingMaskIntoConstraints = false view.addSubview(emptyStateView) emptyStateViewLeadingLayoutConstraint = emptyStateView.leadingAnchor.constraint(equalTo: tableView.leadingAnchor) @@ -153,64 +135,24 @@ extension MastodonPickServerViewController { emptyStateView.topAnchor.constraint(equalTo: view.topAnchor), emptyStateViewLeadingLayoutConstraint, emptyStateViewTrailingLayoutConstraint, - buttonContainer.topAnchor.constraint(equalTo: emptyStateView.bottomAnchor, constant: 21), + navigationActionView.topAnchor.constraint(equalTo: emptyStateView.bottomAnchor, constant: 21), ]) view.sendSubviewToBack(emptyStateView) - - // update layout when keyboard show/dismiss - let keyboardEventPublishers = Publishers.CombineLatest3( - KeyboardResponderService.shared.isShow, - KeyboardResponderService.shared.state, - KeyboardResponderService.shared.endFrame - ) - - keyboardEventPublishers - .sink { [weak self] keyboardEvents in - guard let self = self else { return } - let (isShow, state, endFrame) = keyboardEvents - - // guard external keyboard connected - guard isShow, state == .dock, GCKeyboard.coalesced != nil else { - self.buttonContainerBottomLayoutConstraint.constant = WelcomeViewController.viewBottomPaddingHeight - return - } - - let externalKeyboardToolbarHeight = self.view.frame.maxY - endFrame.minY - guard externalKeyboardToolbarHeight > 0 else { - self.buttonContainerBottomLayoutConstraint.constant = WelcomeViewController.viewBottomPaddingHeight - return - } - - UIView.animate(withDuration: 0.3) { - self.buttonContainerBottomLayoutConstraint.constant = externalKeyboardToolbarHeight + 16 - self.view.layoutIfNeeded() - } - } - .store(in: &disposeBag) - - switch viewModel.mode { - case .signIn: - nextStepButton.setTitle(L10n.Common.Controls.Actions.signIn, for: .normal) - case .signUp: - nextStepButton.setTitle(L10n.Common.Controls.Actions.continue, for: .normal) - } - nextStepButton.addTarget(self, action: #selector(nextStepButtonDidClicked(_:)), for: .touchUpInside) - + tableView.delegate = self viewModel.setupDiffableDataSource( for: tableView, dependency: self, - pickServerCategoriesCellDelegate: self, - pickServerSearchCellDelegate: self, + pickServerServerSectionTableHeaderViewDelegate: self, pickServerCellDelegate: self ) - + viewModel .selectedServer .map { $0 != nil } - .assign(to: \.isEnabled, on: nextStepButton) + .assign(to: \.isEnabled, on: navigationActionView.nextButton) .store(in: &disposeBag) - + Publishers.Merge( viewModel.error, authenticationViewModel.error @@ -229,7 +171,7 @@ extension MastodonPickServerViewController { ) } .store(in: &disposeBag) - + authenticationViewModel .authenticated .flatMap { [weak self] (domain, user) -> AnyPublisher, Never> in @@ -249,17 +191,17 @@ extension MastodonPickServerViewController { } } .store(in: &disposeBag) - + authenticationViewModel.isAuthenticating .receive(on: DispatchQueue.main) .sink { [weak self] isAuthenticating in guard let self = self else { return } - isAuthenticating ? self.nextStepButton.showLoading() : self.nextStepButton.stopLoading() + isAuthenticating ? self.navigationActionView.nextButton.showLoading() : self.navigationActionView.nextButton.stopLoading() } .store(in: &disposeBag) - + viewModel.emptyStateViewState - .receive(on: RunLoop.main) + .receive(on: DispatchQueue.main) .sink { [weak self] state in guard let self = self else { return } switch state { @@ -284,6 +226,9 @@ extension MastodonPickServerViewController { } } .store(in: &disposeBag) + + navigationActionView.backButton.addTarget(self, action: #selector(MastodonPickServerViewController.backButtonDidPressed(_:)), for: .touchUpInside) + navigationActionView.nextButton.addTarget(self, action: #selector(MastodonPickServerViewController.nextButtonDidPressed(_:)), for: .touchUpInside) } override func viewWillAppear(_ animated: Bool) { @@ -291,43 +236,31 @@ extension MastodonPickServerViewController { viewModel.viewWillAppear.send() } + override func viewDidAppear(_ animated: Bool) { + super.viewDidAppear(animated) + + tableView.flashScrollIndicators() + } + override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) { super.traitCollectionDidChange(previousTraitCollection) setupNavigationBarAppearance() updateEmptyStateViewLayout() - configureTitleLabel() - configureMargin() } } -extension MastodonPickServerViewController { - private func configureTitleLabel() { - guard UIDevice.current.userInterfaceIdiom == .pad else { - return - } - - switch traitCollection.horizontalSizeClass { - case .regular: - navigationItem.largeTitleDisplayMode = .always - navigationItem.title = L10n.Scene.ServerPicker.title.replacingOccurrences(of: "\n", with: " ") - default: - navigationItem.largeTitleDisplayMode = .never - navigationItem.title = nil - } - } -} - extension MastodonPickServerViewController { - @objc - private func nextStepButtonDidClicked(_ sender: UIButton) { + @objc private func backButtonDidPressed(_ sender: UIButton) { + navigationController?.popViewController(animated: true) + } + + @objc private func nextButtonDidPressed(_ sender: UIButton) { switch viewModel.mode { - case .signIn: - doSignIn() - case .signUp: - doSignUp() + case .signIn: doSignIn() + case .signUp: doSignUp() } } @@ -442,8 +375,8 @@ extension MastodonPickServerViewController { self.coordinator.present(scene: .mastodonServerRules(viewModel: mastodonServerRulesViewModel), from: self, transition: .show) } else { let mastodonRegisterViewModel = MastodonRegisterViewModel( - domain: server.domain, context: self.context, + domain: server.domain, authenticateInfo: response.authenticateInfo, instance: response.instance.value, applicationToken: response.applicationToken.value @@ -458,16 +391,6 @@ extension MastodonPickServerViewController { // MARK: - UITableViewDelegate extension MastodonPickServerViewController: UITableViewDelegate { - func scrollViewDidScroll(_ scrollView: UIScrollView) { - guard scrollView === tableView else { return } - let offsetY = scrollView.contentOffset.y + scrollView.safeAreaInsets.top - if offsetY < 0 { - tableViewTopPaddingViewHeightLayoutConstraint.constant = abs(offsetY) - } else { - tableViewTopPaddingViewHeightLayoutConstraint.constant = 0 - } - } - func tableView(_ tableView: UITableView, willSelectRowAt indexPath: IndexPath) -> IndexPath? { guard let diffableDataSource = viewModel.diffableDataSource else { return nil } guard let item = diffableDataSource.itemIdentifier(for: indexPath) else { return nil } @@ -500,87 +423,89 @@ extension MastodonPickServerViewController: UITableViewDelegate { guard let item = diffableDataSource.itemIdentifier(for: indexPath) else { return } switch item { - case .categoryPicker: - guard let cell = cell as? PickServerCategoriesCell else { return } - guard let diffableDataSource = cell.diffableDataSource else { return } - let snapshot = diffableDataSource.snapshot() - - let item = viewModel.selectCategoryItem.value - guard let section = snapshot.indexOfSection(.main), - let row = snapshot.indexOfItem(item) else { return } - cell.collectionView.selectItem(at: IndexPath(item: row, section: section), animated: false, scrollPosition: .centeredHorizontally) - case .search: - guard let cell = cell as? PickServerSearchCell else { return } - cell.searchTextField.text = viewModel.searchText.value +// case .categoryPicker: +// guard let cell = cell as? PickServerCategoriesCell else { return } +// guard let diffableDataSource = cell.diffableDataSource else { return } +// let snapshot = diffableDataSource.snapshot() +// +// let item = viewModel.selectCategoryItem.value +// guard let section = snapshot.indexOfSection(.main), +// let row = snapshot.indexOfItem(item) else { return } +// cell.collectionView.selectItem(at: IndexPath(item: row, section: section), animated: false, scrollPosition: .centeredHorizontally) +// case .search: +// guard let cell = cell as? PickServerSearchCell else { return } +// cell.searchTextField.text = viewModel.searchText.value default: break } } + func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? { + guard let diffableDataSource = viewModel.diffableDataSource else { return nil } + let snapshot = diffableDataSource.snapshot() + guard section < snapshot.numberOfSections else { return nil } + let section = snapshot.sectionIdentifiers[section] + + switch section { + case .servers: + return viewModel.serverSectionHeaderView + default: + return UIView() + } + } + + func tableView(_ tableView: UITableView, heightForHeaderInSection section: Int) -> CGFloat { + guard let diffableDataSource = viewModel.diffableDataSource else { return .leastNonzeroMagnitude } + let snapshot = diffableDataSource.snapshot() + guard section < snapshot.numberOfSections else { return .leastNonzeroMagnitude } + let section = snapshot.sectionIdentifiers[section] + + switch section { + case .servers: + return PickServerServerSectionTableHeaderView.height + default: + return .leastNonzeroMagnitude + } + } + } extension MastodonPickServerViewController { private func updateEmptyStateViewLayout() { - guard let diffableDataSource = self.viewModel.diffableDataSource else { return } - guard let indexPath = diffableDataSource.indexPath(for: .search) else { return } - let rectInTableView = tableView.rectForRow(at: indexPath) - - emptyStateView.topPaddingViewTopLayoutConstraint.constant = rectInTableView.maxY - - switch traitCollection.horizontalSizeClass { - case .regular: - emptyStateViewLeadingLayoutConstraint.constant = MastodonPickServerViewController.viewEdgeMargin - emptyStateViewTrailingLayoutConstraint.constant = MastodonPickServerViewController.viewEdgeMargin - default: - let margin = tableView.layoutMarginsGuide.layoutFrame.origin.x - emptyStateViewLeadingLayoutConstraint.constant = margin - emptyStateViewTrailingLayoutConstraint.constant = margin - } - } - - private func configureMargin() { - switch traitCollection.horizontalSizeClass { - case .regular: - let margin = MastodonPickServerViewController.viewEdgeMargin - buttonContainer.layoutMargins = UIEdgeInsets(top: 0, left: margin, bottom: 0, right: margin) - default: - buttonContainer.layoutMargins = .zero - } +// guard let diffableDataSource = self.viewModel.diffableDataSource else { return } +// guard let indexPath = diffableDataSource.indexPath(for: .search) else { return } +// let rectInTableView = tableView.rectForRow(at: indexPath) +// +// emptyStateView.topPaddingViewTopLayoutConstraint.constant = rectInTableView.maxY +// +// switch traitCollection.horizontalSizeClass { +// case .regular: +// emptyStateViewLeadingLayoutConstraint.constant = MastodonPickServerViewController.viewEdgeMargin +// emptyStateViewTrailingLayoutConstraint.constant = MastodonPickServerViewController.viewEdgeMargin +// default: +// let margin = tableView.layoutMarginsGuide.layoutFrame.origin.x +// emptyStateViewLeadingLayoutConstraint.constant = margin +// emptyStateViewTrailingLayoutConstraint.constant = margin +// } } } -// MARK: - PickServerCategoriesCellDelegate -extension MastodonPickServerViewController: PickServerCategoriesCellDelegate { - func pickServerCategoriesCell(_ cell: PickServerCategoriesCell, collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) { - guard let diffableDataSource = cell.diffableDataSource else { return } +// MARK: - PickServerServerSectionTableHeaderViewDelegate +extension MastodonPickServerViewController: PickServerServerSectionTableHeaderViewDelegate { + func pickServerServerSectionTableHeaderView(_ headerView: PickServerServerSectionTableHeaderView, collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) { + guard let diffableDataSource = headerView.diffableDataSource else { return } let item = diffableDataSource.itemIdentifier(for: indexPath) viewModel.selectCategoryItem.value = item ?? .all } -} - -// MARK: - PickServerSearchCellDelegate -extension MastodonPickServerViewController: PickServerSearchCellDelegate { - func pickServerSearchCell(_ cell: PickServerSearchCell, searchTextDidChange searchText: String?) { + + func pickServerServerSectionTableHeaderView(_ headerView: PickServerServerSectionTableHeaderView, searchTextDidChange searchText: String?) { viewModel.searchText.send(searchText ?? "") } } // MARK: - PickServerCellDelegate extension MastodonPickServerViewController: PickServerCellDelegate { - func pickServerCell(_ cell: PickServerCell, expandButtonPressed button: UIButton) { - guard let diffableDataSource = viewModel.diffableDataSource else { return } - guard let indexPath = tableView.indexPath(for: cell) else { return } - guard let item = diffableDataSource.itemIdentifier(for: indexPath) else { return } - guard case let .server(_, attribute) = item else { return } - - attribute.isExpand.toggle() - tableView.beginUpdates() - cell.updateExpandMode(mode: attribute.isExpand ? .expand : .collapse) - tableView.endUpdates() - - // expand attribute change do not needs apply snapshot to diffable data source - // but should I block the viewModel data binding during tableView.beginUpdates/endUpdates? - } + } // MARK: - OnboardingViewControllerAppearance diff --git a/Mastodon/Scene/Onboarding/PickServer/MastodonPickServerViewModel+Diffable.swift b/Mastodon/Scene/Onboarding/PickServer/MastodonPickServerViewModel+Diffable.swift index 9da0399e..35de40b8 100644 --- a/Mastodon/Scene/Onboarding/PickServer/MastodonPickServerViewModel+Diffable.swift +++ b/Mastodon/Scene/Onboarding/PickServer/MastodonPickServerViewModel+Diffable.swift @@ -6,32 +6,105 @@ // import UIKit +import Combine extension MastodonPickServerViewModel { func setupDiffableDataSource( for tableView: UITableView, dependency: NeedsDependency, - pickServerCategoriesCellDelegate: PickServerCategoriesCellDelegate, - pickServerSearchCellDelegate: PickServerSearchCellDelegate, + pickServerServerSectionTableHeaderViewDelegate: PickServerServerSectionTableHeaderViewDelegate, pickServerCellDelegate: PickServerCellDelegate ) { + // set section header + serverSectionHeaderView.diffableDataSource = CategoryPickerSection.collectionViewDiffableDataSource( + for: serverSectionHeaderView.collectionView, + dependency: dependency + ) + var sectionHeaderSnapshot = NSDiffableDataSourceSnapshot() + sectionHeaderSnapshot.appendSections([.main]) + sectionHeaderSnapshot.appendItems(categoryPickerItems, toSection: .main) + serverSectionHeaderView.delegate = pickServerServerSectionTableHeaderViewDelegate + serverSectionHeaderView.diffableDataSource?.applySnapshot(sectionHeaderSnapshot, animated: false) { [weak self] in + guard let self = self else { return } + guard let indexPath = self.serverSectionHeaderView.diffableDataSource?.indexPath(for: .all) else { return } + self.serverSectionHeaderView.collectionView.selectItem(at: indexPath, animated: false, scrollPosition: .centeredHorizontally) + } + + // set tableView diffableDataSource = PickServerSection.tableViewDiffableDataSource( for: tableView, dependency: dependency, - pickServerCategoriesCellDelegate: pickServerCategoriesCellDelegate, - pickServerSearchCellDelegate: pickServerSearchCellDelegate, pickServerCellDelegate: pickServerCellDelegate ) var snapshot = NSDiffableDataSourceSnapshot() - snapshot.appendSections([.header, .category, .search, .servers]) + snapshot.appendSections([.header, .servers]) snapshot.appendItems([.header], toSection: .header) - snapshot.appendItems([.categoryPicker(items: categoryPickerItems)], toSection: .category) - snapshot.appendItems([.search], toSection: .search) diffableDataSource?.apply(snapshot, animatingDifferences: false, completion: nil) loadIndexedServerStateMachine.enter(LoadIndexedServerState.Loading.self) + + Publishers.CombineLatest( + filteredIndexedServers, + unindexedServers + ) + .receive(on: DispatchQueue.main) + .sink(receiveValue: { [weak self] indexedServers, unindexedServers in + guard let self = self else { return } + guard let diffableDataSource = self.diffableDataSource else { return } + + let oldSnapshot = diffableDataSource.snapshot() + var oldSnapshotServerItemAttributeDict: [String : PickServerItem.ServerItemAttribute] = [:] + for item in oldSnapshot.itemIdentifiers { + guard case let .server(server, attribute) = item else { continue } + oldSnapshotServerItemAttributeDict[server.domain] = attribute + } + + var snapshot = NSDiffableDataSourceSnapshot() + snapshot.appendSections([.header, .servers]) + snapshot.appendItems([.header], toSection: .header) + + // TODO: handle filter + var serverItems: [PickServerItem] = [] + for server in indexedServers { + let attribute = oldSnapshotServerItemAttributeDict[server.domain] ?? PickServerItem.ServerItemAttribute(isLast: false, isExpand: false) + attribute.isLast.value = false + let item = PickServerItem.server(server: server, attribute: attribute) + guard !serverItems.contains(item) else { continue } + serverItems.append(item) + } + + if let unindexedServers = unindexedServers { + if !unindexedServers.isEmpty { + for server in unindexedServers { + let attribute = oldSnapshotServerItemAttributeDict[server.domain] ?? PickServerItem.ServerItemAttribute(isLast: false, isExpand: false) + attribute.isLast.value = false + let item = PickServerItem.server(server: server, attribute: attribute) + guard !serverItems.contains(item) else { continue } + serverItems.append(item) + } + } else { + if indexedServers.isEmpty && !self.isLoadingIndexedServers.value { + serverItems.append(.loader(attribute: PickServerItem.LoaderItemAttribute(isLast: false, isEmptyResult: true))) + } + } + } else { + serverItems.append(.loader(attribute: PickServerItem.LoaderItemAttribute(isLast: false, isEmptyResult: false))) + } + + if case let .server(_, attribute) = serverItems.last { + attribute.isLast.value = true + } + if case let .loader(attribute) = serverItems.last { + attribute.isLast = true + } + snapshot.appendItems(serverItems, toSection: .servers) + + diffableDataSource.defaultRowAnimation = .fade + diffableDataSource.apply(snapshot, animatingDifferences: true, completion: nil) + }) + .store(in: &disposeBag) } } diff --git a/Mastodon/Scene/Onboarding/PickServer/MastodonPickServerViewModel.swift b/Mastodon/Scene/Onboarding/PickServer/MastodonPickServerViewModel.swift index 7a648011..af38b110 100644 --- a/Mastodon/Scene/Onboarding/PickServer/MastodonPickServerViewModel.swift +++ b/Mastodon/Scene/Onboarding/PickServer/MastodonPickServerViewModel.swift @@ -12,6 +12,7 @@ import GameplayKit import MastodonSDK import CoreDataStack import OrderedCollections +import Tabman class MastodonPickServerViewModel: NSObject { @@ -27,6 +28,8 @@ class MastodonPickServerViewModel: NSObject { } var disposeBag = Set() + + let serverSectionHeaderView = PickServerServerSectionTableHeaderView() // input let mode: PickServerMode @@ -82,68 +85,6 @@ class MastodonPickServerViewModel: NSObject { extension MastodonPickServerViewModel { private func configure() { - Publishers.CombineLatest( - filteredIndexedServers, - unindexedServers - ) - .receive(on: DispatchQueue.main) - .sink(receiveValue: { [weak self] indexedServers, unindexedServers in - guard let self = self else { return } - guard let diffableDataSource = self.diffableDataSource else { return } - - let oldSnapshot = diffableDataSource.snapshot() - var oldSnapshotServerItemAttributeDict: [String : PickServerItem.ServerItemAttribute] = [:] - for item in oldSnapshot.itemIdentifiers { - guard case let .server(server, attribute) = item else { continue } - oldSnapshotServerItemAttributeDict[server.domain] = attribute - } - - var snapshot = NSDiffableDataSourceSnapshot() - snapshot.appendSections([.header, .category, .search, .servers]) - snapshot.appendItems([.header], toSection: .header) - snapshot.appendItems([.categoryPicker(items: self.categoryPickerItems)], toSection: .category) - snapshot.appendItems([.search], toSection: .search) - // TODO: handle filter - var serverItems: [PickServerItem] = [] - for server in indexedServers { - let attribute = oldSnapshotServerItemAttributeDict[server.domain] ?? PickServerItem.ServerItemAttribute(isLast: false, isExpand: false) - attribute.isLast.value = false - let item = PickServerItem.server(server: server, attribute: attribute) - guard !serverItems.contains(item) else { continue } - serverItems.append(item) - } - - if let unindexedServers = unindexedServers { - if !unindexedServers.isEmpty { - for server in unindexedServers { - let attribute = oldSnapshotServerItemAttributeDict[server.domain] ?? PickServerItem.ServerItemAttribute(isLast: false, isExpand: false) - attribute.isLast.value = false - let item = PickServerItem.server(server: server, attribute: attribute) - guard !serverItems.contains(item) else { continue } - serverItems.append(item) - } - } else { - if indexedServers.isEmpty && !self.isLoadingIndexedServers.value { - serverItems.append(.loader(attribute: PickServerItem.LoaderItemAttribute(isLast: false, isEmptyResult: true))) - } - } - } else { - serverItems.append(.loader(attribute: PickServerItem.LoaderItemAttribute(isLast: false, isEmptyResult: false))) - } - - if case let .server(_, attribute) = serverItems.last { - attribute.isLast.value = true - } - if case let .loader(attribute) = serverItems.last { - attribute.isLast = true - } - snapshot.appendItems(serverItems, toSection: .servers) - - diffableDataSource.defaultRowAnimation = .fade - diffableDataSource.apply(snapshot, animatingDifferences: true, completion: nil) - }) - .store(in: &disposeBag) - Publishers.CombineLatest( isLoadingIndexedServers, loadingIndexedServersError @@ -301,3 +242,12 @@ extension MastodonPickServerViewModel { let applicationToken: Mastodon.Response.Content } } + +// MARK: - TMBarDataSource +extension MastodonPickServerViewModel: TMBarDataSource { + func barItem(for bar: TMBar, at index: Int) -> TMBarItemable { + let item = categoryPickerItems[index] + let barItem = TMBarItem(title: item.title) + return barItem + } +} diff --git a/Mastodon/Scene/Onboarding/PickServer/TableViewCell/PickServerCategoriesCell.swift b/Mastodon/Scene/Onboarding/PickServer/TableViewCell/PickServerCategoriesCell.swift deleted file mode 100644 index 65931775..00000000 --- a/Mastodon/Scene/Onboarding/PickServer/TableViewCell/PickServerCategoriesCell.swift +++ /dev/null @@ -1,145 +0,0 @@ -// -// PickServerCategoriesCell.swift -// Mastodon -// -// Created by BradGao on 2021/2/23. -// - -import os.log -import UIKit -import MastodonSDK - -protocol PickServerCategoriesCellDelegate: AnyObject { - func pickServerCategoriesCell(_ cell: PickServerCategoriesCell, collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) -} - -final class PickServerCategoriesCell: UITableViewCell { - - weak var delegate: PickServerCategoriesCellDelegate? - - var diffableDataSource: UICollectionViewDiffableDataSource? - - let metricView = UIView() - - let collectionView: UICollectionView = { - let flowLayout = UICollectionViewFlowLayout() - flowLayout.scrollDirection = .horizontal - let view = ControlContainableCollectionView(frame: .zero, collectionViewLayout: flowLayout) - view.register(PickServerCategoryCollectionViewCell.self, forCellWithReuseIdentifier: String(describing: PickServerCategoryCollectionViewCell.self)) - view.backgroundColor = .clear - view.showsHorizontalScrollIndicator = false - view.showsVerticalScrollIndicator = false - view.layer.masksToBounds = false - view.translatesAutoresizingMaskIntoConstraints = false - return view - }() - - override func prepareForReuse() { - super.prepareForReuse() - - delegate = nil - } - - override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { - super.init(style: style, reuseIdentifier: reuseIdentifier) - _init() - } - - required init?(coder: NSCoder) { - super.init(coder: coder) - _init() - } -} - -extension PickServerCategoriesCell { - - private func _init() { - selectionStyle = .none - backgroundColor = Asset.Theme.Mastodon.systemGroupedBackground.color - configureMargin() - - metricView.translatesAutoresizingMaskIntoConstraints = false - contentView.addSubview(metricView) - NSLayoutConstraint.activate([ - metricView.leadingAnchor.constraint(equalTo: contentView.layoutMarginsGuide.leadingAnchor), - metricView.trailingAnchor.constraint(equalTo: contentView.layoutMarginsGuide.trailingAnchor), - metricView.topAnchor.constraint(equalTo: contentView.topAnchor), - metricView.bottomAnchor.constraint(equalTo: contentView.bottomAnchor), - metricView.heightAnchor.constraint(equalToConstant: 80).priority(.defaultHigh), - ]) - - contentView.addSubview(collectionView) - NSLayoutConstraint.activate([ - collectionView.leadingAnchor.constraint(equalTo: contentView.leadingAnchor), - collectionView.trailingAnchor.constraint(equalTo: contentView.trailingAnchor), - collectionView.topAnchor.constraint(equalTo: contentView.topAnchor, constant: 10), - contentView.bottomAnchor.constraint(equalTo: collectionView.bottomAnchor, constant: 20), - collectionView.heightAnchor.constraint(equalToConstant: 80).priority(.defaultHigh), - ]) - - collectionView.delegate = self - } - - override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) { - super.traitCollectionDidChange(previousTraitCollection) - - configureMargin() - } - - override func layoutSubviews() { - super.layoutSubviews() - - collectionView.collectionViewLayout.invalidateLayout() - } - -} - -extension PickServerCategoriesCell { - private func configureMargin() { - switch traitCollection.horizontalSizeClass { - case .regular: - let margin = MastodonPickServerViewController.viewEdgeMargin - contentView.layoutMargins = UIEdgeInsets(top: 0, left: margin, bottom: 0, right: margin) - default: - contentView.layoutMargins = .zero - } - } -} - -// MARK: - UICollectionViewDelegateFlowLayout -extension PickServerCategoriesCell: UICollectionViewDelegateFlowLayout { - - func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) { - os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s: indexPath: %s", ((#file as NSString).lastPathComponent), #line, #function, indexPath.debugDescription) - collectionView.selectItem(at: indexPath, animated: true, scrollPosition: .centeredHorizontally) - delegate?.pickServerCategoriesCell(self, collectionView: collectionView, didSelectItemAt: indexPath) - } - - func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, insetForSectionAt section: Int) -> UIEdgeInsets { - layoutIfNeeded() - return UIEdgeInsets(top: 0, left: metricView.frame.minX - collectionView.frame.minX, bottom: 0, right: collectionView.frame.maxX - metricView.frame.maxX) - } - - func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, minimumInteritemSpacingForSectionAt section: Int) -> CGFloat { - return 16 - } - - func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize { - return CGSize(width: 60, height: 80) - } - -} - -extension PickServerCategoriesCell { - - override func accessibilityElementCount() -> Int { - guard let diffableDataSource = diffableDataSource else { return 0 } - return diffableDataSource.snapshot().itemIdentifiers.count - } - - override func accessibilityElement(at index: Int) -> Any? { - guard let item = collectionView.cellForItem(at: IndexPath(item: index, section: 0)) else { return nil } - return item - } - -} diff --git a/Mastodon/Scene/Onboarding/PickServer/TableViewCell/PickServerCell.swift b/Mastodon/Scene/Onboarding/PickServer/TableViewCell/PickServerCell.swift index 2f60a520..6dd0f197 100644 --- a/Mastodon/Scene/Onboarding/PickServer/TableViewCell/PickServerCell.swift +++ b/Mastodon/Scene/Onboarding/PickServer/TableViewCell/PickServerCell.swift @@ -13,7 +13,7 @@ import AlamofireImage import Kanna protocol PickServerCellDelegate: AnyObject { - func pickServerCell(_ cell: PickServerCell, expandButtonPressed button: UIButton) +// func pickServerCell(_ cell: PickServerCell, expandButtonPressed button: UIButton) } class PickServerCell: UITableViewCell { @@ -21,20 +21,17 @@ class PickServerCell: UITableViewCell { weak var delegate: PickServerCellDelegate? var disposeBag = Set() - - let expandMode = CurrentValueSubject(.collapse) - - let containerView: UIView = { - let view = UIView() - view.layoutMargins = UIEdgeInsets(top: 16, left: 16, bottom: 10, right: 16) - view.backgroundColor = Asset.Theme.Mastodon.secondaryGroupedSystemBackground.color - view.translatesAutoresizingMaskIntoConstraints = false + + let containerView: UIStackView = { + let view = UIStackView() + view.axis = .vertical + view.spacing = 4 return view }() let domainLabel: UILabel = { let label = UILabel() - label.font = UIFontMetrics(forTextStyle: .headline).scaledFont(for: .systemFont(ofSize: 17, weight: .semibold), maximumPointSize: 22) + label.font = UIFontMetrics(forTextStyle: .headline).scaledFont(for: .systemFont(ofSize: 20, weight: .semibold)) label.textColor = Asset.Colors.Label.primary.color label.adjustsFontForContentSizeCategory = true label.translatesAutoresizingMaskIntoConstraints = false @@ -52,7 +49,7 @@ class PickServerCell: UITableViewCell { let descriptionLabel: UILabel = { let label = UILabel() - label.font = UIFontMetrics(forTextStyle: .body).scaledFont(for: .systemFont(ofSize: 15, weight: .regular)) + label.font = UIFontMetrics(forTextStyle: .caption1).scaledFont(for: .systemFont(ofSize: 13, weight: .regular)) label.numberOfLines = 0 label.textColor = Asset.Colors.Label.primary.color label.adjustsFontForContentSizeCategory = true @@ -60,112 +57,33 @@ class PickServerCell: UITableViewCell { return label }() - let thumbnailActivityIndicator = UIActivityIndicatorView(style: .medium) - - let thumbnailImageView: UIImageView = { - let imageView = UIImageView() - imageView.clipsToBounds = true - imageView.contentMode = .scaleAspectFill - imageView.translatesAutoresizingMaskIntoConstraints = false - return imageView - }() - let infoStackView: UIStackView = { let stackView = UIStackView() stackView.axis = .horizontal - stackView.alignment = .fill - stackView.distribution = .fillEqually - stackView.translatesAutoresizingMaskIntoConstraints = false + stackView.spacing = 16 return stackView }() - let expandBox: UIView = { - let view = UIView() - view.backgroundColor = .clear - view.translatesAutoresizingMaskIntoConstraints = false - return view - }() - - let expandButton: UIButton = { - let button = HitTestExpandedButton(type: .custom) - button.setImage(UIImage(systemName: "chevron.down", withConfiguration: UIImage.SymbolConfiguration(pointSize: 13)), for: .normal) - button.setTitle(L10n.Scene.ServerPicker.Button.seeMore, for: .normal) - button.setTitleColor(Asset.Colors.brandBlue.color, for: .normal) - button.titleLabel?.font = .systemFont(ofSize: 13, weight: .regular) - button.translatesAutoresizingMaskIntoConstraints = false - button.imageView?.transform = CGAffineTransform(scaleX: -1, y: 1) - button.titleLabel?.transform = CGAffineTransform(scaleX: -1, y: 1) - button.transform = CGAffineTransform(scaleX: -1, y: 1) - return button - }() - let separator: UIView = { let view = UIView() - view.backgroundColor = Asset.Theme.Mastodon.systemGroupedBackground.color - view.translatesAutoresizingMaskIntoConstraints = false + view.backgroundColor = Asset.Theme.System.separator.color return view }() let langValueLabel: UILabel = { let label = UILabel() label.textColor = Asset.Colors.Label.primary.color - label.font = UIFontMetrics(forTextStyle: .headline).scaledFont(for: .systemFont(ofSize: 22, weight: .semibold), maximumPointSize: 27) + label.font = UIFontMetrics(forTextStyle: .caption1).scaledFont(for: .systemFont(ofSize: 12, weight: .regular)) label.textAlignment = .center label.adjustsFontForContentSizeCategory = true - label.translatesAutoresizingMaskIntoConstraints = false return label }() let usersValueLabel: UILabel = { let label = UILabel() label.textColor = Asset.Colors.Label.primary.color - label.font = UIFontMetrics(forTextStyle: .headline).scaledFont(for: .systemFont(ofSize: 22, weight: .semibold), maximumPointSize: 27) - label.textAlignment = .center + label.font = UIFontMetrics(forTextStyle: .caption1).scaledFont(for: .systemFont(ofSize: 12, weight: .regular)) label.adjustsFontForContentSizeCategory = true - label.translatesAutoresizingMaskIntoConstraints = false - return label - }() - - let categoryValueLabel: UILabel = { - let label = UILabel() - label.textColor = Asset.Colors.Label.primary.color - label.font = UIFontMetrics(forTextStyle: .headline).scaledFont(for: .systemFont(ofSize: 22, weight: .semibold), maximumPointSize: 27) - label.textAlignment = .center - label.adjustsFontForContentSizeCategory = true - label.translatesAutoresizingMaskIntoConstraints = false - return label - }() - - let langTitleLabel: UILabel = { - let label = UILabel() - label.textColor = Asset.Colors.Label.primary.color - label.font = UIFontMetrics(forTextStyle: .subheadline).scaledFont(for: .systemFont(ofSize: 11, weight: .regular), maximumPointSize: 16) - label.text = L10n.Scene.ServerPicker.Label.language - label.textAlignment = .center - label.adjustsFontForContentSizeCategory = true - label.translatesAutoresizingMaskIntoConstraints = false - return label - }() - - let usersTitleLabel: UILabel = { - let label = UILabel() - label.textColor = Asset.Colors.Label.primary.color - label.font = UIFontMetrics(forTextStyle: .subheadline).scaledFont(for: .systemFont(ofSize: 11, weight: .regular), maximumPointSize: 16) - label.text = L10n.Scene.ServerPicker.Label.users - label.textAlignment = .center - label.adjustsFontForContentSizeCategory = true - label.translatesAutoresizingMaskIntoConstraints = false - return label - }() - - let categoryTitleLabel: UILabel = { - let label = UILabel() - label.textColor = Asset.Colors.Label.primary.color - label.font = UIFontMetrics(forTextStyle: .subheadline).scaledFont(for: .systemFont(ofSize: 11, weight: .regular), maximumPointSize: 16) - label.text = L10n.Scene.ServerPicker.Label.category - label.textAlignment = .center - label.adjustsFontForContentSizeCategory = true - label.translatesAutoresizingMaskIntoConstraints = false return label }() @@ -175,9 +93,6 @@ class PickServerCell: UITableViewCell { override func prepareForReuse() { super.prepareForReuse() - thumbnailImageView.isHidden = false - thumbnailImageView.af.cancelImageRequest() - thumbnailActivityIndicator.stopAnimating() disposeBag.removeAll() } @@ -197,172 +112,55 @@ class PickServerCell: UITableViewCell { extension PickServerCell { private func _init() { selectionStyle = .none - backgroundColor = .clear - configureMargin() + backgroundColor = Asset.Scene.Onboarding.onboardingBackground.color + + checkbox.translatesAutoresizingMaskIntoConstraints = false + contentView.addSubview(checkbox) + NSLayoutConstraint.activate([ + checkbox.leadingAnchor.constraint(equalTo: contentView.readableContentGuide.leadingAnchor, constant: 1), + checkbox.heightAnchor.constraint(equalToConstant: 32).priority(.required - 1), + checkbox.widthAnchor.constraint(equalToConstant: 32).priority(.required - 1), + ]) + containerView.translatesAutoresizingMaskIntoConstraints = false contentView.addSubview(containerView) - containerView.addSubview(domainLabel) - containerView.addSubview(checkbox) - containerView.addSubview(descriptionLabel) - containerView.addSubview(separator) - - containerView.addSubview(expandButton) - - // Always add the expandbox which contains elements only visible in expand mode - containerView.addSubview(expandBox) - expandBox.addSubview(thumbnailImageView) - expandBox.addSubview(infoStackView) - expandBox.isHidden = true - - let verticalInfoStackViewLang = makeVerticalInfoStackView(arrangedView: langValueLabel, langTitleLabel) - let verticalInfoStackViewUsers = makeVerticalInfoStackView(arrangedView: usersValueLabel, usersTitleLabel) - let verticalInfoStackViewCategory = makeVerticalInfoStackView(arrangedView: categoryValueLabel, categoryTitleLabel) - infoStackView.addArrangedSubview(verticalInfoStackViewLang) - infoStackView.addArrangedSubview(verticalInfoStackViewUsers) - infoStackView.addArrangedSubview(verticalInfoStackViewCategory) - - let expandButtonTopConstraintInCollapse = expandButton.topAnchor.constraint(equalTo: descriptionLabel.lastBaselineAnchor, constant: 12).priority(.required - 1) - collapseConstraints.append(expandButtonTopConstraintInCollapse) - - let expandButtonTopConstraintInExpand = expandButton.topAnchor.constraint(equalTo: expandBox.bottomAnchor, constant: 8).priority(.defaultHigh) - expandConstraints.append(expandButtonTopConstraintInExpand) - NSLayoutConstraint.activate([ - // Set background view - containerView.topAnchor.constraint(equalTo: contentView.topAnchor), - containerView.leadingAnchor.constraint(equalTo: contentView.layoutMarginsGuide.leadingAnchor), - contentView.layoutMarginsGuide.trailingAnchor.constraint(equalTo: containerView.trailingAnchor), - contentView.bottomAnchor.constraint(equalTo: containerView.bottomAnchor), - - // Set bottom separator - separator.leadingAnchor.constraint(equalTo: containerView.leadingAnchor), - containerView.trailingAnchor.constraint(equalTo: separator.trailingAnchor), - containerView.topAnchor.constraint(equalTo: separator.topAnchor), - separator.heightAnchor.constraint(equalToConstant: 1).priority(.defaultHigh), - - domainLabel.topAnchor.constraint(equalTo: containerView.layoutMarginsGuide.topAnchor), - domainLabel.leadingAnchor.constraint(equalTo: containerView.layoutMarginsGuide.leadingAnchor), - - checkbox.widthAnchor.constraint(equalToConstant: 23), - checkbox.heightAnchor.constraint(equalToConstant: 22), - containerView.layoutMarginsGuide.trailingAnchor.constraint(equalTo: checkbox.trailingAnchor), - checkbox.leadingAnchor.constraint(equalTo: domainLabel.trailingAnchor, constant: 16), - checkbox.centerYAnchor.constraint(equalTo: domainLabel.centerYAnchor), - - descriptionLabel.leadingAnchor.constraint(equalTo: containerView.layoutMarginsGuide.leadingAnchor), - descriptionLabel.topAnchor.constraint(equalTo: domainLabel.bottomAnchor, constant: 8), - containerView.layoutMarginsGuide.trailingAnchor.constraint(equalTo: descriptionLabel.trailingAnchor), - - // Set expandBox constraints - expandBox.leadingAnchor.constraint(equalTo: containerView.layoutMarginsGuide.leadingAnchor), - containerView.layoutMarginsGuide.trailingAnchor.constraint(equalTo: expandBox.trailingAnchor), - expandBox.topAnchor.constraint(equalTo: descriptionLabel.bottomAnchor, constant: 8), - expandBox.bottomAnchor.constraint(equalTo: infoStackView.bottomAnchor).priority(.defaultHigh), - - thumbnailImageView.topAnchor.constraint(equalTo: expandBox.topAnchor), - thumbnailImageView.leadingAnchor.constraint(equalTo: expandBox.leadingAnchor), - expandBox.trailingAnchor.constraint(equalTo: thumbnailImageView.trailingAnchor), - thumbnailImageView.heightAnchor.constraint(equalTo: thumbnailImageView.widthAnchor, multiplier: 151.0 / 303.0).priority(.defaultHigh), - - infoStackView.leadingAnchor.constraint(equalTo: expandBox.leadingAnchor), - expandBox.trailingAnchor.constraint(equalTo: infoStackView.trailingAnchor), - infoStackView.topAnchor.constraint(equalTo: thumbnailImageView.bottomAnchor, constant: 16), - - expandButton.leadingAnchor.constraint(equalTo: containerView.layoutMarginsGuide.leadingAnchor), - containerView.layoutMarginsGuide.trailingAnchor.constraint(equalTo: expandButton.trailingAnchor), - containerView.layoutMarginsGuide.bottomAnchor.constraint(equalTo: expandButton.bottomAnchor), + containerView.topAnchor.constraint(equalTo: contentView.topAnchor, constant: 11), + containerView.leadingAnchor.constraint(equalTo: checkbox.trailingAnchor, constant: 22), + containerView.trailingAnchor.constraint(equalTo: contentView.readableContentGuide.trailingAnchor), + contentView.bottomAnchor.constraint(equalTo: containerView.bottomAnchor, constant: 11), + checkbox.centerYAnchor.constraint(equalTo: containerView.centerYAnchor), ]) - thumbnailActivityIndicator.translatesAutoresizingMaskIntoConstraints = false - thumbnailImageView.addSubview(thumbnailActivityIndicator) + containerView.addArrangedSubview(domainLabel) + containerView.addArrangedSubview(descriptionLabel) + containerView.setCustomSpacing(6, after: descriptionLabel) + containerView.addArrangedSubview(infoStackView) + + infoStackView.addArrangedSubview(usersValueLabel) + infoStackView.addArrangedSubview(langValueLabel) + infoStackView.addArrangedSubview(UIView()) + + separator.translatesAutoresizingMaskIntoConstraints = false + contentView.addSubview(separator) NSLayoutConstraint.activate([ - thumbnailActivityIndicator.centerXAnchor.constraint(equalTo: thumbnailImageView.centerXAnchor), - thumbnailActivityIndicator.centerYAnchor.constraint(equalTo: thumbnailImageView.centerYAnchor), + separator.leadingAnchor.constraint(equalTo: contentView.readableContentGuide.leadingAnchor), + contentView.readableContentGuide.trailingAnchor.constraint(equalTo: separator.trailingAnchor), + separator.bottomAnchor.constraint(equalTo: contentView.bottomAnchor), + separator.heightAnchor.constraint(equalToConstant: UIView.separatorLineHeight(of: contentView)).priority(.required - 1), ]) - thumbnailActivityIndicator.hidesWhenStopped = true - thumbnailActivityIndicator.stopAnimating() - - NSLayoutConstraint.activate(collapseConstraints) - - domainLabel.setContentHuggingPriority(.required - 1, for: .vertical) - domainLabel.setContentCompressionResistancePriority(.required - 1, for: .vertical) - descriptionLabel.setContentHuggingPriority(.required - 2, for: .vertical) - descriptionLabel.setContentCompressionResistancePriority(.required - 2, for: .vertical) - - expandButton.addTarget(self, action: #selector(expandButtonDidPressed(_:)), for: .touchUpInside) } - override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) { - super.traitCollectionDidChange(previousTraitCollection) - - configureMargin() - } - - private func makeVerticalInfoStackView(arrangedView: UIView...) -> UIStackView { - let stackView = UIStackView() - stackView.translatesAutoresizingMaskIntoConstraints = false - stackView.axis = .vertical - stackView.alignment = .center - stackView.distribution = .equalCentering - stackView.spacing = 2 - arrangedView.forEach { stackView.addArrangedSubview($0) } - return stackView - } - override func setSelected(_ selected: Bool, animated: Bool) { super.setSelected(selected, animated: animated) if selected { checkbox.image = UIImage(systemName: "checkmark.circle.fill") + checkbox.tintColor = Asset.Colors.Label.primary.color } else { checkbox.image = UIImage(systemName: "circle") + checkbox.tintColor = Asset.Colors.Label.secondary.color } } - - @objc - private func expandButtonDidPressed(_ sender: UIButton) { - os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s", ((#file as NSString).lastPathComponent), #line, #function) - delegate?.pickServerCell(self, expandButtonPressed: sender) - } + } -extension PickServerCell { - private func configureMargin() { - switch traitCollection.horizontalSizeClass { - case .regular: - let margin = MastodonPickServerViewController.viewEdgeMargin - contentView.layoutMargins = UIEdgeInsets(top: 0, left: margin, bottom: 0, right: margin) - default: - contentView.layoutMargins = .zero - } - } -} - -extension PickServerCell { - - enum ExpandMode { - case collapse - case expand - } - - func updateExpandMode(mode: ExpandMode) { - switch mode { - case .collapse: - expandButton.setImage(UIImage(systemName: "chevron.down", withConfiguration: UIImage.SymbolConfiguration(pointSize: 13)), for: .normal) - expandButton.setTitle(L10n.Scene.ServerPicker.Button.seeMore, for: .normal) - expandBox.isHidden = true - expandButton.isSelected = false - NSLayoutConstraint.deactivate(expandConstraints) - NSLayoutConstraint.activate(collapseConstraints) - case .expand: - expandButton.setImage(UIImage(systemName: "chevron.up", withConfiguration: UIImage.SymbolConfiguration(pointSize: 13)), for: .normal) - expandButton.setTitle(L10n.Scene.ServerPicker.Button.seeLess, for: .normal) - expandBox.isHidden = false - expandButton.isSelected = true - NSLayoutConstraint.activate(expandConstraints) - NSLayoutConstraint.deactivate(collapseConstraints) - } - - expandMode.value = mode - } - -} diff --git a/Mastodon/Scene/Onboarding/PickServer/TableViewCell/PickServerLoaderTableViewCell.swift b/Mastodon/Scene/Onboarding/PickServer/TableViewCell/PickServerLoaderTableViewCell.swift index 945ecac6..eb0b619d 100644 --- a/Mastodon/Scene/Onboarding/PickServer/TableViewCell/PickServerLoaderTableViewCell.swift +++ b/Mastodon/Scene/Onboarding/PickServer/TableViewCell/PickServerLoaderTableViewCell.swift @@ -13,15 +13,7 @@ final class PickServerLoaderTableViewCell: TimelineLoaderTableViewCell { let containerView: UIView = { let view = UIView() view.layoutMargins = UIEdgeInsets(top: 16, left: 16, bottom: 10, right: 16) - view.backgroundColor = Asset.Theme.Mastodon.secondaryGroupedSystemBackground.color - view.translatesAutoresizingMaskIntoConstraints = false - return view - }() - - let seperator: UIView = { - let view = UIView() - view.backgroundColor = Asset.Theme.Mastodon.systemGroupedBackground.color - view.translatesAutoresizingMaskIntoConstraints = false + view.backgroundColor = .clear return view }() @@ -30,30 +22,22 @@ final class PickServerLoaderTableViewCell: TimelineLoaderTableViewCell { label.text = L10n.Scene.ServerPicker.EmptyState.noResults label.textColor = Asset.Colors.Label.secondary.color label.textAlignment = .center - label.font = UIFontMetrics(forTextStyle: .headline).scaledFont(for: .systemFont(ofSize: 14, weight: .semibold), maximumPointSize: 19) + label.font = UIFontMetrics(forTextStyle: .headline).scaledFont(for: .systemFont(ofSize: 14, weight: .semibold)) return label }() override func _init() { super._init() - - configureMargin() - - contentView.addSubview(containerView) - contentView.addSubview(seperator) + + // Set background view + containerView.translatesAutoresizingMaskIntoConstraints = false + contentView.addSubview(containerView) NSLayoutConstraint.activate([ - // Set background view containerView.topAnchor.constraint(equalTo: contentView.topAnchor), containerView.leadingAnchor.constraint(equalTo: contentView.layoutMarginsGuide.leadingAnchor), contentView.layoutMarginsGuide.trailingAnchor.constraint(equalTo: containerView.trailingAnchor), - contentView.bottomAnchor.constraint(equalTo: containerView.bottomAnchor, constant: 1), - - // Set bottom separator - seperator.leadingAnchor.constraint(equalTo: containerView.leadingAnchor), - containerView.trailingAnchor.constraint(equalTo: seperator.trailingAnchor), - containerView.topAnchor.constraint(equalTo: seperator.topAnchor), - seperator.heightAnchor.constraint(equalToConstant: 1).priority(.defaultHigh), + contentView.bottomAnchor.constraint(equalTo: containerView.bottomAnchor), ]) emptyStatusLabel.translatesAutoresizingMaskIntoConstraints = false @@ -69,24 +53,7 @@ final class PickServerLoaderTableViewCell: TimelineLoaderTableViewCell { activityIndicatorView.isHidden = false startAnimating() } - - override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) { - super.traitCollectionDidChange(previousTraitCollection) - - configureMargin() - } -} -extension PickServerLoaderTableViewCell { - private func configureMargin() { - switch traitCollection.horizontalSizeClass { - case .regular: - let margin = MastodonPickServerViewController.viewEdgeMargin - contentView.layoutMargins = UIEdgeInsets(top: 0, left: margin, bottom: 0, right: margin) - default: - contentView.layoutMargins = .zero - } - } } #if canImport(SwiftUI) && DEBUG diff --git a/Mastodon/Scene/Onboarding/PickServer/TableViewCell/PickServerSearchCell.swift b/Mastodon/Scene/Onboarding/PickServer/TableViewCell/PickServerSearchCell.swift deleted file mode 100644 index 0a64103d..00000000 --- a/Mastodon/Scene/Onboarding/PickServer/TableViewCell/PickServerSearchCell.swift +++ /dev/null @@ -1,171 +0,0 @@ -// -// PickServerSearchCell.swift -// Mastodon -// -// Created by BradGao on 2021/2/24. -// - -import UIKit - -protocol PickServerSearchCellDelegate: AnyObject { - func pickServerSearchCell(_ cell: PickServerSearchCell, searchTextDidChange searchText: String?) -} - -class PickServerSearchCell: UITableViewCell { - - weak var delegate: PickServerSearchCellDelegate? - - private var bgView: UIView = { - let view = UIView() - view.backgroundColor = Asset.Theme.Mastodon.secondaryGroupedSystemBackground.color - view.translatesAutoresizingMaskIntoConstraints = false - view.layer.maskedCorners = [ - .layerMinXMinYCorner, - .layerMaxXMinYCorner - ] - view.layer.cornerCurve = .continuous - view.layer.cornerRadius = MastodonPickServerAppearance.tableViewCornerRadius - return view - }() - - private var textFieldBgView: UIView = { - let view = UIView() - view.backgroundColor = Asset.Colors.TextField.background.color - view.translatesAutoresizingMaskIntoConstraints = false - view.layer.masksToBounds = true - view.layer.cornerRadius = 6 - view.layer.cornerCurve = .continuous - return view - }() - - let searchTextField: UITextField = { - let textField = UITextField() - textField.translatesAutoresizingMaskIntoConstraints = false - textField.leftView = { - let imageView = UIImageView( - image: UIImage( - systemName: "magnifyingglass", - withConfiguration: UIImage.SymbolConfiguration(pointSize: 15, weight: .regular) - ) - ) - imageView.tintColor = Asset.Colors.Label.secondary.color.withAlphaComponent(0.6) - - let containerView = UIView() - imageView.translatesAutoresizingMaskIntoConstraints = false - containerView.addSubview(imageView) - NSLayoutConstraint.activate([ - imageView.topAnchor.constraint(equalTo: containerView.topAnchor), - imageView.leadingAnchor.constraint(equalTo: containerView.leadingAnchor), - imageView.bottomAnchor.constraint(equalTo: containerView.bottomAnchor), - ]) - - let paddingView = UIView() - paddingView.translatesAutoresizingMaskIntoConstraints = false - containerView.addSubview(paddingView) - NSLayoutConstraint.activate([ - paddingView.topAnchor.constraint(equalTo: containerView.topAnchor), - paddingView.leadingAnchor.constraint(equalTo: imageView.trailingAnchor), - paddingView.trailingAnchor.constraint(equalTo: containerView.trailingAnchor), - paddingView.bottomAnchor.constraint(equalTo: containerView.bottomAnchor), - paddingView.widthAnchor.constraint(equalToConstant: 4).priority(.defaultHigh), - ]) - return containerView - }() - textField.leftViewMode = .always - textField.font = .systemFont(ofSize: 15, weight: .regular) - textField.tintColor = Asset.Colors.Label.primary.color - textField.textColor = Asset.Colors.Label.primary.color - textField.adjustsFontForContentSizeCategory = true - textField.attributedPlaceholder = - NSAttributedString(string: L10n.Scene.ServerPicker.Input.placeholder, - attributes: [.font: UIFont.systemFont(ofSize: 15, weight: .regular), - .foregroundColor: Asset.Colors.Label.secondary.color.withAlphaComponent(0.6)]) - textField.clearButtonMode = .whileEditing - textField.autocapitalizationType = .none - textField.autocorrectionType = .no - textField.returnKeyType = .done - textField.keyboardType = .URL - return textField - }() - - override func prepareForReuse() { - super.prepareForReuse() - - delegate = nil - } - - override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { - super.init(style: style, reuseIdentifier: reuseIdentifier) - _init() - } - - required init?(coder: NSCoder) { - super.init(coder: coder) - _init() - } -} - -extension PickServerSearchCell { - private func _init() { - selectionStyle = .none - backgroundColor = Asset.Theme.Mastodon.systemGroupedBackground.color - configureMargin() - - searchTextField.addTarget(self, action: #selector(textFieldDidChange(_:)), for: .editingChanged) - searchTextField.delegate = self - - contentView.addSubview(bgView) - contentView.addSubview(textFieldBgView) - contentView.addSubview(searchTextField) - - NSLayoutConstraint.activate([ - bgView.leadingAnchor.constraint(equalTo: contentView.layoutMarginsGuide.leadingAnchor), - bgView.topAnchor.constraint(equalTo: contentView.topAnchor), - bgView.trailingAnchor.constraint(equalTo: contentView.layoutMarginsGuide.trailingAnchor), - bgView.bottomAnchor.constraint(equalTo: contentView.bottomAnchor), - - textFieldBgView.leadingAnchor.constraint(equalTo: bgView.leadingAnchor, constant: 14), - textFieldBgView.topAnchor.constraint(equalTo: bgView.topAnchor, constant: 12), - bgView.trailingAnchor.constraint(equalTo: textFieldBgView.trailingAnchor, constant: 14), - bgView.bottomAnchor.constraint(equalTo: textFieldBgView.bottomAnchor, constant: 13), - - searchTextField.leadingAnchor.constraint(equalTo: textFieldBgView.leadingAnchor, constant: 11), - searchTextField.topAnchor.constraint(equalTo: textFieldBgView.topAnchor, constant: 4), - textFieldBgView.trailingAnchor.constraint(equalTo: searchTextField.trailingAnchor, constant: 11), - textFieldBgView.bottomAnchor.constraint(equalTo: searchTextField.bottomAnchor, constant: 4), - ]) - } - - override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) { - super.traitCollectionDidChange(previousTraitCollection) - - configureMargin() - } -} - -extension PickServerSearchCell { - private func configureMargin() { - switch traitCollection.horizontalSizeClass { - case .regular: - let margin = MastodonPickServerViewController.viewEdgeMargin - contentView.layoutMargins = UIEdgeInsets(top: 0, left: margin, bottom: 0, right: margin) - default: - contentView.layoutMargins = .zero - } - } -} - -extension PickServerSearchCell { - @objc private func textFieldDidChange(_ textField: UITextField) { - delegate?.pickServerSearchCell(self, searchTextDidChange: textField.text) - } -} - -// MARK: - UITextFieldDelegate -extension PickServerSearchCell: UITextFieldDelegate { - - func textFieldShouldReturn(_ textField: UITextField) -> Bool { - textField.resignFirstResponder() - return false - } -} diff --git a/Mastodon/Scene/Onboarding/PickServer/TableViewCell/PickServerTitleCell.swift b/Mastodon/Scene/Onboarding/PickServer/TableViewCell/PickServerTitleCell.swift deleted file mode 100644 index f0d78eb4..00000000 --- a/Mastodon/Scene/Onboarding/PickServer/TableViewCell/PickServerTitleCell.swift +++ /dev/null @@ -1,82 +0,0 @@ -// -// PickServerTitleCell.swift -// Mastodon -// -// Created by BradGao on 2021/2/23. -// - -import UIKit - -final class PickServerTitleCell: UITableViewCell { - - let titleLabel: UILabel = { - let label = UILabel() - label.font = UIFontMetrics(forTextStyle: .largeTitle).scaledFont(for: .systemFont(ofSize: 34, weight: .bold)) - label.textColor = Asset.Colors.Label.primary.color - label.text = L10n.Scene.ServerPicker.title - label.adjustsFontForContentSizeCategory = true - label.translatesAutoresizingMaskIntoConstraints = false - label.numberOfLines = 0 - return label - }() - - var containerHeightLayoutConstraint: NSLayoutConstraint! - - override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { - super.init(style: style, reuseIdentifier: reuseIdentifier) - _init() - } - - required init?(coder: NSCoder) { - super.init(coder: coder) - _init() - } -} - -extension PickServerTitleCell { - - private func _init() { - selectionStyle = .none - backgroundColor = Asset.Theme.Mastodon.systemGroupedBackground.color - - let container = UIStackView() - container.axis = .vertical - container.translatesAutoresizingMaskIntoConstraints = false - containerHeightLayoutConstraint = container.heightAnchor.constraint(equalToConstant: .leastNonzeroMagnitude) - contentView.addSubview(container) - NSLayoutConstraint.activate([ - container.topAnchor.constraint(equalTo: contentView.topAnchor), - container.leadingAnchor.constraint(equalTo: contentView.readableContentGuide.leadingAnchor), - container.trailingAnchor.constraint(equalTo: contentView.readableContentGuide.trailingAnchor), - container.bottomAnchor.constraint(equalTo: contentView.bottomAnchor), - ]) - - container.addArrangedSubview(titleLabel) - - configureTitleLabelDisplay() - } - - override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) { - super.traitCollectionDidChange(previousTraitCollection) - - configureTitleLabelDisplay() - } -} - -extension PickServerTitleCell { - private func configureTitleLabelDisplay() { - guard traitCollection.userInterfaceIdiom == .pad else { - titleLabel.isHidden = false - return - } - - switch traitCollection.horizontalSizeClass { - case .regular: - titleLabel.isHidden = true - containerHeightLayoutConstraint.isActive = true - default: - titleLabel.isHidden = false - containerHeightLayoutConstraint.isActive = false - } - } -} diff --git a/Mastodon/Scene/Onboarding/PickServer/View/PickServerCategoryView.swift b/Mastodon/Scene/Onboarding/PickServer/View/PickServerCategoryView.swift index 6565fbcf..f3bc3994 100644 --- a/Mastodon/Scene/Onboarding/PickServer/View/PickServerCategoryView.swift +++ b/Mastodon/Scene/Onboarding/PickServer/View/PickServerCategoryView.swift @@ -10,24 +10,24 @@ import MastodonSDK class PickServerCategoryView: UIView { - var bgShadowView: UIView = { + let highlightedIndicatorView: UIView = { let view = UIView() - view.translatesAutoresizingMaskIntoConstraints = false + view.backgroundColor = Asset.Colors.Label.primary.color return view }() - - var bgView: UIView = { - let view = UIView() - view.translatesAutoresizingMaskIntoConstraints = false - view.layer.masksToBounds = true - view.layer.cornerRadius = 30 - return view - }() - - var titleLabel: UILabel = { + + let emojiLabel: UILabel = { let label = UILabel() label.textAlignment = .center - label.translatesAutoresizingMaskIntoConstraints = false + label.font = .systemFont(ofSize: 34, weight: .regular) + return label + }() + + let titleLabel: UILabel = { + let label = UILabel() + label.textAlignment = .center + label.font = .systemFont(ofSize: 17, weight: .semibold) + label.textColor = Asset.Colors.Label.secondary.color return label }() @@ -45,20 +45,27 @@ class PickServerCategoryView: UIView { extension PickServerCategoryView { private func configure() { - addSubview(bgView) - addSubview(titleLabel) - - bgView.backgroundColor = Asset.Theme.Mastodon.secondaryGroupedSystemBackground.color - + let container = UIStackView() + container.axis = .vertical + container.distribution = .fillProportionally + + container.translatesAutoresizingMaskIntoConstraints = false + addSubview(container) NSLayoutConstraint.activate([ - bgView.leadingAnchor.constraint(equalTo: self.leadingAnchor), - bgView.trailingAnchor.constraint(equalTo: self.trailingAnchor), - bgView.topAnchor.constraint(equalTo: self.topAnchor), - bgView.bottomAnchor.constraint(equalTo: self.bottomAnchor), - - titleLabel.centerXAnchor.constraint(equalTo: self.centerXAnchor), - titleLabel.centerYAnchor.constraint(equalTo: self.centerYAnchor), + container.topAnchor.constraint(equalTo: topAnchor), + container.leadingAnchor.constraint(equalTo: leadingAnchor), + container.trailingAnchor.constraint(equalTo: trailingAnchor), + container.bottomAnchor.constraint(equalTo: bottomAnchor), ]) + + container.addArrangedSubview(emojiLabel) + container.addArrangedSubview(titleLabel) + highlightedIndicatorView.translatesAutoresizingMaskIntoConstraints = false + container.addArrangedSubview(highlightedIndicatorView) + NSLayoutConstraint.activate([ + highlightedIndicatorView.heightAnchor.constraint(equalToConstant: UIView.separatorLineHeight(of: self) * 3).priority(.required - 1), + ]) + titleLabel.setContentHuggingPriority(.required - 1, for: .vertical) } } diff --git a/Mastodon/Scene/Onboarding/PickServer/View/PickServerEmptyStateView.swift b/Mastodon/Scene/Onboarding/PickServer/View/PickServerEmptyStateView.swift index 1d2c17c7..c5682143 100644 --- a/Mastodon/Scene/Onboarding/PickServer/View/PickServerEmptyStateView.swift +++ b/Mastodon/Scene/Onboarding/PickServer/View/PickServerEmptyStateView.swift @@ -44,13 +44,7 @@ final class PickServerEmptyStateView: UIView { extension PickServerEmptyStateView { private func _init() { - backgroundColor = Asset.Theme.Mastodon.secondaryGroupedSystemBackground.color - layer.maskedCorners = [ - .layerMinXMaxYCorner, - .layerMaxXMaxYCorner - ] - layer.cornerCurve = .continuous - layer.cornerRadius = MastodonPickServerAppearance.tableViewCornerRadius + backgroundColor = .clear let topPaddingView = UIView() topPaddingView.translatesAutoresizingMaskIntoConstraints = false @@ -101,7 +95,7 @@ extension PickServerEmptyStateView { ]) NSLayoutConstraint.activate([ - bottomPaddingView.heightAnchor.constraint(equalTo: topPaddingView.heightAnchor, multiplier: 1.0).priority(.defaultHigh), + topPaddingView.heightAnchor.constraint(equalTo: bottomPaddingView.heightAnchor, multiplier: 2.5).priority(.defaultHigh), // magic scale ]) activityIndicatorView.hidesWhenStopped = true diff --git a/Mastodon/Scene/Onboarding/PickServer/View/PickServerServerSectionTableHeaderView.swift b/Mastodon/Scene/Onboarding/PickServer/View/PickServerServerSectionTableHeaderView.swift new file mode 100644 index 00000000..4afa31aa --- /dev/null +++ b/Mastodon/Scene/Onboarding/PickServer/View/PickServerServerSectionTableHeaderView.swift @@ -0,0 +1,204 @@ +// +// PickServerServerSectionTableHeaderView.swift +// Mastodon +// +// Created by MainasuK on 2022-1-4. +// + +import os.log +import UIKit +import Tabman + +protocol PickServerServerSectionTableHeaderViewDelegate: AnyObject { + func pickServerServerSectionTableHeaderView(_ headerView: PickServerServerSectionTableHeaderView, collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) + func pickServerServerSectionTableHeaderView(_ headerView: PickServerServerSectionTableHeaderView, searchTextDidChange searchText: String?) +} + +final class PickServerServerSectionTableHeaderView: UIView { + + static let collectionViewHeight: CGFloat = 88 + static let searchTextFieldHeight: CGFloat = 38 + static let spacing: CGFloat = 11 + + static let height: CGFloat = collectionViewHeight + spacing + searchTextFieldHeight + spacing + + weak var delegate: PickServerServerSectionTableHeaderViewDelegate? + + var diffableDataSource: UICollectionViewDiffableDataSource? + + static func createCollectionViewLayout() -> UICollectionViewLayout { + let itemSize = NSCollectionLayoutSize(widthDimension: .estimated(88), heightDimension: .absolute(PickServerServerSectionTableHeaderView.collectionViewHeight)) + let item = NSCollectionLayoutItem(layoutSize: itemSize) + + let groupSize = NSCollectionLayoutSize(widthDimension: itemSize.widthDimension, heightDimension: itemSize.heightDimension) + let group = NSCollectionLayoutGroup.horizontal(layoutSize: groupSize, subitems: [item]) + + let section = NSCollectionLayoutSection(group: group) + section.orthogonalScrollingBehavior = .continuous + section.contentInsetsReference = .readableContent + section.interGroupSpacing = 16 + + return UICollectionViewCompositionalLayout(section: section) + } + + let collectionView: UICollectionView = { + let collectionViewLayout = PickServerServerSectionTableHeaderView.createCollectionViewLayout() + let view = ControlContainableCollectionView( + frame: CGRect(origin: .zero, size: CGSize(width: 100, height: PickServerServerSectionTableHeaderView.collectionViewHeight)), + collectionViewLayout: collectionViewLayout + ) + view.register(PickServerCategoryCollectionViewCell.self, forCellWithReuseIdentifier: String(describing: PickServerCategoryCollectionViewCell.self)) + view.backgroundColor = .clear + view.alwaysBounceVertical = false + view.showsHorizontalScrollIndicator = false + view.showsVerticalScrollIndicator = false + view.layer.masksToBounds = false + return view + }() + + let searchTextField: UITextField = { + let textField = UITextField() + textField.backgroundColor = Asset.Scene.Onboarding.searchBarBackground.color + textField.leftView = { + let imageView = UIImageView( + image: UIImage( + systemName: "magnifyingglass", + withConfiguration: UIImage.SymbolConfiguration(pointSize: 15, weight: .regular) + ) + ) + imageView.tintColor = Asset.Colors.Label.secondary.color.withAlphaComponent(0.6) + + let containerView = UIView() + imageView.translatesAutoresizingMaskIntoConstraints = false + containerView.addSubview(imageView) + NSLayoutConstraint.activate([ + imageView.topAnchor.constraint(equalTo: containerView.topAnchor), + imageView.leadingAnchor.constraint(equalTo: containerView.leadingAnchor, constant: 8), + imageView.bottomAnchor.constraint(equalTo: containerView.bottomAnchor), + ]) + + let paddingView = UIView() + paddingView.translatesAutoresizingMaskIntoConstraints = false + containerView.addSubview(paddingView) + NSLayoutConstraint.activate([ + paddingView.topAnchor.constraint(equalTo: containerView.topAnchor), + paddingView.leadingAnchor.constraint(equalTo: imageView.trailingAnchor), + paddingView.trailingAnchor.constraint(equalTo: containerView.trailingAnchor), + paddingView.bottomAnchor.constraint(equalTo: containerView.bottomAnchor), + paddingView.widthAnchor.constraint(equalToConstant: 4).priority(.defaultHigh), + ]) + return containerView + }() + textField.leftViewMode = .always + textField.font = .systemFont(ofSize: 15, weight: .regular) + textField.tintColor = Asset.Colors.Label.primary.color + textField.textColor = Asset.Colors.Label.primary.color + textField.adjustsFontForContentSizeCategory = true + textField.attributedPlaceholder = + NSAttributedString( + string: L10n.Scene.ServerPicker.Input.placeholder, + attributes: [.font: UIFont.systemFont(ofSize: 15, weight: .regular), + .foregroundColor: Asset.Colors.Label.secondary.color.withAlphaComponent(0.6)]) + textField.clearButtonMode = .whileEditing + textField.autocapitalizationType = .none + textField.autocorrectionType = .no + textField.returnKeyType = .done + textField.keyboardType = .URL + textField.borderStyle = .none + + textField.layer.masksToBounds = true + textField.layer.cornerRadius = 10 + textField.layer.cornerCurve = .continuous + + return textField + }() + + override init(frame: CGRect) { + super.init(frame: frame) + _init() + } + + required init?(coder: NSCoder) { + super.init(coder: coder) + _init() + } + + override func layoutSubviews() { + super.layoutSubviews() + + collectionView.invalidateIntrinsicContentSize() + } + +} + +extension PickServerServerSectionTableHeaderView { + private func _init() { + preservesSuperviewLayoutMargins = true + backgroundColor = Asset.Scene.Onboarding.onboardingBackground.color + + collectionView.translatesAutoresizingMaskIntoConstraints = false + collectionView.preservesSuperviewLayoutMargins = true + addSubview(collectionView) + NSLayoutConstraint.activate([ + collectionView.topAnchor.constraint(equalTo: topAnchor), + collectionView.leadingAnchor.constraint(equalTo: leadingAnchor), + collectionView.trailingAnchor.constraint(equalTo: trailingAnchor), + collectionView.heightAnchor.constraint(equalToConstant: PickServerServerSectionTableHeaderView.collectionViewHeight).priority(.required - 1), + ]) + + searchTextField.translatesAutoresizingMaskIntoConstraints = false + addSubview(searchTextField) + NSLayoutConstraint.activate([ + searchTextField.topAnchor.constraint(equalTo: collectionView.bottomAnchor, constant: PickServerServerSectionTableHeaderView.spacing), + searchTextField.leadingAnchor.constraint(equalTo: readableContentGuide.leadingAnchor), + searchTextField.trailingAnchor.constraint(equalTo: readableContentGuide.trailingAnchor), + bottomAnchor.constraint(equalTo: searchTextField.bottomAnchor, constant: PickServerServerSectionTableHeaderView.spacing), + searchTextField.heightAnchor.constraint(equalToConstant: PickServerServerSectionTableHeaderView.searchTextFieldHeight).priority(.required - 1), + ]) + + collectionView.delegate = self + searchTextField.delegate = self + searchTextField.addTarget(self, action: #selector(PickServerServerSectionTableHeaderView.textFieldDidChange(_:)), for: .editingChanged) + } +} + +extension PickServerServerSectionTableHeaderView { + @objc private func textFieldDidChange(_ textField: UITextField) { + delegate?.pickServerServerSectionTableHeaderView(self, searchTextDidChange: textField.text) + } +} + +// MARK: - UICollectionViewDelegate +extension PickServerServerSectionTableHeaderView: UICollectionViewDelegate { + + func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) { + os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s: indexPath: %s", ((#file as NSString).lastPathComponent), #line, #function, indexPath.debugDescription) + collectionView.selectItem(at: indexPath, animated: true, scrollPosition: .centeredHorizontally) + delegate?.pickServerServerSectionTableHeaderView(self, collectionView: collectionView, didSelectItemAt: indexPath) + } + +} + +extension PickServerServerSectionTableHeaderView { + + override func accessibilityElementCount() -> Int { + guard let diffableDataSource = diffableDataSource else { return 0 } + return diffableDataSource.snapshot().itemIdentifiers.count + } + + override func accessibilityElement(at index: Int) -> Any? { + guard let item = collectionView.cellForItem(at: IndexPath(item: index, section: 0)) else { return nil } + return item + } + +} + +// MARK: - UITextFieldDelegate +extension PickServerServerSectionTableHeaderView: UITextFieldDelegate { + + func textFieldShouldReturn(_ textField: UITextField) -> Bool { + textField.resignFirstResponder() + return false + } + +} diff --git a/Mastodon/Scene/Onboarding/Register/Cell/MastodonRegisterAvatarTableViewCell.swift b/Mastodon/Scene/Onboarding/Register/Cell/MastodonRegisterAvatarTableViewCell.swift new file mode 100644 index 00000000..304bd02d --- /dev/null +++ b/Mastodon/Scene/Onboarding/Register/Cell/MastodonRegisterAvatarTableViewCell.swift @@ -0,0 +1,114 @@ +// +// MastodonRegisterAvatarTableViewCell.swift +// Mastodon +// +// Created by MainasuK on 2022-1-5. +// + +import UIKit +import Combine + +final class MastodonRegisterAvatarTableViewCell: UITableViewCell { + + static let containerSize = CGSize(width: 88, height: 88) + + var disposeBag = Set() + + let containerView: UIView = { + let view = UIView() + view.backgroundColor = .clear + view.layer.masksToBounds = true + view.layer.cornerCurve = .continuous + view.layer.cornerRadius = 22 + return view + }() + + let avatarButton: HighlightDimmableButton = { + let button = HighlightDimmableButton() + button.backgroundColor = Asset.Theme.Mastodon.secondaryGroupedSystemBackground.color + button.setImage(Asset.Scene.Onboarding.avatarPlaceholder.image, for: .normal) + return button + }() + + let editBannerView: UIView = { + let bannerView = UIView() + bannerView.backgroundColor = UIColor.black.withAlphaComponent(0.5) + bannerView.isUserInteractionEnabled = false + + let label: UILabel = { + let label = UILabel() + label.textColor = .white + label.text = L10n.Common.Controls.Actions.edit + label.font = .systemFont(ofSize: 13, weight: .semibold) + label.textAlignment = .center + label.minimumScaleFactor = 0.5 + label.adjustsFontSizeToFitWidth = true + return label + }() + + label.translatesAutoresizingMaskIntoConstraints = false + bannerView.addSubview(label) + NSLayoutConstraint.activate([ + label.topAnchor.constraint(equalTo: bannerView.topAnchor), + label.leadingAnchor.constraint(equalTo: bannerView.leadingAnchor), + label.trailingAnchor.constraint(equalTo: bannerView.trailingAnchor), + label.bottomAnchor.constraint(equalTo: bannerView.bottomAnchor), + ]) + + return bannerView + }() + + override func prepareForReuse() { + super.prepareForReuse() + + disposeBag.removeAll() + } + + override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { + super.init(style: style, reuseIdentifier: reuseIdentifier) + _init() + } + + required init?(coder: NSCoder) { + super.init(coder: coder) + _init() + } + +} + +extension MastodonRegisterAvatarTableViewCell { + + private func _init() { + selectionStyle = .none + backgroundColor = .clear + + containerView.translatesAutoresizingMaskIntoConstraints = false + contentView.addSubview(containerView) + NSLayoutConstraint.activate([ + containerView.topAnchor.constraint(equalTo: contentView.topAnchor, constant: 22), + containerView.centerXAnchor.constraint(equalTo: contentView.centerXAnchor), + contentView.bottomAnchor.constraint(equalTo: containerView.bottomAnchor, constant: 8), + containerView.widthAnchor.constraint(equalToConstant: MastodonRegisterAvatarTableViewCell.containerSize.width).priority(.required - 1), + containerView.heightAnchor.constraint(equalToConstant: MastodonRegisterAvatarTableViewCell.containerSize.height).priority(.required - 1), + ]) + + avatarButton.translatesAutoresizingMaskIntoConstraints = false + containerView.addSubview(avatarButton) + NSLayoutConstraint.activate([ + avatarButton.topAnchor.constraint(equalTo: containerView.topAnchor), + avatarButton.leadingAnchor.constraint(equalTo: containerView.leadingAnchor), + avatarButton.trailingAnchor.constraint(equalTo: containerView.trailingAnchor), + avatarButton.bottomAnchor.constraint(equalTo: containerView.bottomAnchor), + ]) + + editBannerView.translatesAutoresizingMaskIntoConstraints = false + containerView.addSubview(editBannerView) + NSLayoutConstraint.activate([ + editBannerView.leadingAnchor.constraint(equalTo: containerView.leadingAnchor), + editBannerView.trailingAnchor.constraint(equalTo: containerView.trailingAnchor), + editBannerView.bottomAnchor.constraint(equalTo: containerView.bottomAnchor), + editBannerView.heightAnchor.constraint(equalToConstant: 22), + ]) + } + +} diff --git a/Mastodon/Scene/Onboarding/Register/Cell/MastodonRegisterPasswordHintTableViewCell.swift b/Mastodon/Scene/Onboarding/Register/Cell/MastodonRegisterPasswordHintTableViewCell.swift new file mode 100644 index 00000000..829c70a7 --- /dev/null +++ b/Mastodon/Scene/Onboarding/Register/Cell/MastodonRegisterPasswordHintTableViewCell.swift @@ -0,0 +1,48 @@ +// +// MastodonRegisterPasswordHintTableViewCell.swift +// Mastodon +// +// Created by MainasuK on 2022-1-7. +// + +import UIKit + +final class MastodonRegisterPasswordHintTableViewCell: UITableViewCell { + + let passwordRuleLabel: UILabel = { + let label = UILabel() + label.font = .preferredFont(forTextStyle: .footnote) + label.textColor = Asset.Colors.Label.secondary.color + label.text = L10n.Scene.Register.Input.Password.hint + return label + }() + + override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { + super.init(style: style, reuseIdentifier: reuseIdentifier) + _init() + } + + required init?(coder: NSCoder) { + super.init(coder: coder) + _init() + } + +} + +extension MastodonRegisterPasswordHintTableViewCell { + + private func _init() { + selectionStyle = .none + backgroundColor = .clear + + passwordRuleLabel.translatesAutoresizingMaskIntoConstraints = false + contentView.addSubview(passwordRuleLabel) + NSLayoutConstraint.activate([ + passwordRuleLabel.topAnchor.constraint(equalTo: contentView.topAnchor), + passwordRuleLabel.leadingAnchor.constraint(equalTo: contentView.readableContentGuide.leadingAnchor), + passwordRuleLabel.trailingAnchor.constraint(equalTo: contentView.readableContentGuide.trailingAnchor), + passwordRuleLabel.bottomAnchor.constraint(equalTo: contentView.bottomAnchor), + ]) + } + +} diff --git a/Mastodon/Scene/Onboarding/Register/Cell/MastodonRegisterTextFieldTableViewCell.swift b/Mastodon/Scene/Onboarding/Register/Cell/MastodonRegisterTextFieldTableViewCell.swift new file mode 100644 index 00000000..8e54d1ff --- /dev/null +++ b/Mastodon/Scene/Onboarding/Register/Cell/MastodonRegisterTextFieldTableViewCell.swift @@ -0,0 +1,136 @@ +// +// MastodonRegisterTextFieldTableViewCell.swift +// Mastodon +// +// Created by MainasuK on 2022-1-7. +// + +import UIKit +import Combine +import MastodonUI + +final class MastodonRegisterTextFieldTableViewCell: UITableViewCell { + + static let textFieldHeight: CGFloat = 50 + static let textFieldLabelFont = UIFontMetrics(forTextStyle: .headline).scaledFont(for: .systemFont(ofSize: 17, weight: .semibold), maximumPointSize: 22) + + var disposeBag = Set() + + let textFieldShadowContainer = ShadowBackgroundContainer() + let textField: UITextField = { + let textField = UITextField() + textField.font = MastodonRegisterTextFieldTableViewCell.textFieldLabelFont + textField.backgroundColor = Asset.Scene.Onboarding.textFieldBackground.color + textField.layer.masksToBounds = true + textField.layer.cornerRadius = 10 + textField.layer.cornerCurve = .continuous + return textField + }() + + override func prepareForReuse() { + super.prepareForReuse() + + disposeBag.removeAll() + textFieldShadowContainer.shadowColor = .black + textFieldShadowContainer.shadowAlpha = 0.25 + resetTextField() + } + + override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { + super.init(style: style, reuseIdentifier: reuseIdentifier) + _init() + } + + required init?(coder: NSCoder) { + super.init(coder: coder) + _init() + } + +} + +extension MastodonRegisterTextFieldTableViewCell { + + private func _init() { + selectionStyle = .none + backgroundColor = .clear + + textFieldShadowContainer.translatesAutoresizingMaskIntoConstraints = false + contentView.addSubview(textFieldShadowContainer) + NSLayoutConstraint.activate([ + textFieldShadowContainer.topAnchor.constraint(equalTo: contentView.topAnchor, constant: 6), + textFieldShadowContainer.leadingAnchor.constraint(equalTo: contentView.readableContentGuide.leadingAnchor), + textFieldShadowContainer.trailingAnchor.constraint(equalTo: contentView.readableContentGuide.trailingAnchor), + contentView.bottomAnchor.constraint(equalTo: textFieldShadowContainer.bottomAnchor, constant: 6), + ]) + + textField.translatesAutoresizingMaskIntoConstraints = false + textFieldShadowContainer.addSubview(textField) + NSLayoutConstraint.activate([ + textField.topAnchor.constraint(equalTo: textFieldShadowContainer.topAnchor), + textField.leadingAnchor.constraint(equalTo: textFieldShadowContainer.leadingAnchor), + textField.trailingAnchor.constraint(equalTo: textFieldShadowContainer.trailingAnchor), + textField.bottomAnchor.constraint(equalTo: textFieldShadowContainer.bottomAnchor), + textField.heightAnchor.constraint(equalToConstant: MastodonRegisterTextFieldTableViewCell.textFieldHeight).priority(.required - 1), + ]) + + resetTextField() + } + +} + +extension MastodonRegisterTextFieldTableViewCell { + func resetTextField() { + textField.keyboardType = .default + textField.autocorrectionType = .default + textField.autocapitalizationType = .none + textField.attributedPlaceholder = nil + textField.isSecureTextEntry = false + + let paddingRect = CGRect(x: 0, y: 0, width: 16, height: 10) + textField.leftView = UIView(frame: paddingRect) + textField.leftViewMode = .always + textField.rightView = UIView(frame: paddingRect) + textField.rightViewMode = .always + } + + func setupTextViewRightView(text: String) { + textField.rightView = { + let containerView = UIView() + + let paddingView = UIView(frame: CGRect(x: 0, y: 0, width: 8, height: MastodonRegisterTextFieldTableViewCell.textFieldHeight)) + paddingView.translatesAutoresizingMaskIntoConstraints = false + containerView.addSubview(paddingView) + NSLayoutConstraint.activate([ + paddingView.topAnchor.constraint(equalTo: containerView.topAnchor), + paddingView.leadingAnchor.constraint(equalTo: containerView.leadingAnchor), + paddingView.bottomAnchor.constraint(equalTo: containerView.bottomAnchor), + paddingView.widthAnchor.constraint(equalToConstant: 8).priority(.defaultHigh), + ]) + + let label = UILabel() + label.font = MastodonRegisterTextFieldTableViewCell.textFieldLabelFont + label.textColor = Asset.Colors.Label.primary.color + label.text = text + + label.translatesAutoresizingMaskIntoConstraints = false + containerView.addSubview(label) + NSLayoutConstraint.activate([ + label.topAnchor.constraint(equalTo: containerView.topAnchor), + label.leadingAnchor.constraint(equalTo: paddingView.trailingAnchor), + containerView.trailingAnchor.constraint(equalTo: label.trailingAnchor, constant: 16), + label.bottomAnchor.constraint(equalTo: containerView.bottomAnchor), + ]) + return containerView + }() + } + + func setupTextViewPlaceholder(text: String) { + textField.attributedPlaceholder = NSAttributedString( + string: text, + attributes: [ + .foregroundColor: Asset.Colors.Label.secondary.color, + .font: MastodonRegisterTextFieldTableViewCell.textFieldLabelFont + ] + ) + } +} diff --git a/Mastodon/Scene/Onboarding/Register/MastodonRegisterViewController+Avatar.swift b/Mastodon/Scene/Onboarding/Register/MastodonRegisterViewController+Avatar.swift index b1fa1b43..0add10dc 100644 --- a/Mastodon/Scene/Onboarding/Register/MastodonRegisterViewController+Avatar.swift +++ b/Mastodon/Scene/Onboarding/Register/MastodonRegisterViewController+Avatar.swift @@ -12,36 +12,6 @@ import PhotosUI import UIKit extension MastodonRegisterViewController { - func createMediaContextMenu() -> UIMenu { - var children: [UIMenuElement] = [] - let photoLibraryAction = UIAction(title: L10n.Scene.Compose.MediaSelection.photoLibrary, image: UIImage(systemName: "rectangle.on.rectangle"), identifier: nil, discoverabilityTitle: nil, attributes: [], state: .off) { [weak self] _ in - guard let self = self else { return } - self.present(self.imagePicker, animated: true, completion: nil) - } - children.append(photoLibraryAction) - if UIImagePickerController.isSourceTypeAvailable(.camera) { - let cameraAction = UIAction(title: L10n.Scene.Compose.MediaSelection.camera, image: UIImage(systemName: "camera"), identifier: nil, discoverabilityTitle: nil, attributes: [], state: .off, handler: { [weak self] _ in - guard let self = self else { return } - self.present(self.imagePickerController, animated: true, completion: nil) - }) - children.append(cameraAction) - } - let browseAction = UIAction(title: L10n.Scene.Compose.MediaSelection.browse, image: UIImage(systemName: "ellipsis"), identifier: nil, discoverabilityTitle: nil, attributes: [], state: .off) { [weak self] _ in - guard let self = self else { return } - self.present(self.documentPickerController, animated: true, completion: nil) - } - children.append(browseAction) - if self.viewModel.avatarImage.value != nil { - let deleteAction = UIAction(title: L10n.Scene.Register.Input.Avatar.delete, image: UIImage(systemName: "delete.left"), identifier: nil, discoverabilityTitle: nil, attributes: [.destructive], state: .off) { [weak self] _ in - guard let self = self else { return } - self.viewModel.avatarImage.value = nil - } - children.append(deleteAction) - } - - return UIMenu(title: "", image: nil, identifier: nil, options: .displayInline, children: children) - } - private func cropImage(image: UIImage, pickerViewController: UIViewController) { DispatchQueue.main.async { let cropController = CropViewController(croppingStyle: .default, image: image) @@ -49,6 +19,12 @@ extension MastodonRegisterViewController { cropController.setAspectRatioPreset(.presetSquare, animated: true) cropController.aspectRatioPickerButtonHidden = true cropController.aspectRatioLockEnabled = true + + // fix iPad compatibility issue + // ref: https://github.com/TimOliver/TOCropViewController/issues/365#issuecomment-550239604 + cropController.modalTransitionStyle = .crossDissolve + cropController.transitioningDelegate = nil + pickerViewController.dismiss(animated: true, completion: { self.present(cropController, animated: true, completion: nil) }) @@ -57,7 +33,6 @@ extension MastodonRegisterViewController { } // MARK: - PHPickerViewControllerDelegate - extension MastodonRegisterViewController: PHPickerViewControllerDelegate { func picker(_ picker: PHPickerViewController, didFinishPicking results: [PHPickerResult]) { guard let itemProvider = results.first?.itemProvider, itemProvider.canLoadObject(ofClass: UIImage.self) else { @@ -86,7 +61,6 @@ extension MastodonRegisterViewController: PHPickerViewControllerDelegate { } // MARK: - UIImagePickerControllerDelegate - extension MastodonRegisterViewController: UIImagePickerControllerDelegate & UINavigationControllerDelegate { func imagePickerController(_ picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [UIImagePickerController.InfoKey: Any]) { picker.dismiss(animated: true, completion: nil) @@ -103,7 +77,6 @@ extension MastodonRegisterViewController: UIImagePickerControllerDelegate & UINa } // MARK: - UIDocumentPickerDelegate - extension MastodonRegisterViewController: UIDocumentPickerDelegate { func documentPicker(_ controller: UIDocumentPickerViewController, didPickDocumentsAt urls: [URL]) { guard let url = urls.first else { return } @@ -121,10 +94,9 @@ extension MastodonRegisterViewController: UIDocumentPickerDelegate { } // MARK: - CropViewControllerDelegate - extension MastodonRegisterViewController: CropViewControllerDelegate { public func cropViewController(_ cropViewController: CropViewController, didCropToImage image: UIImage, withRect cropRect: CGRect, angle: Int) { - self.viewModel.avatarImage.value = image + self.viewModel.avatarImage = image cropViewController.dismiss(animated: true, completion: nil) } } diff --git a/Mastodon/Scene/Onboarding/Register/MastodonRegisterViewController.swift b/Mastodon/Scene/Onboarding/Register/MastodonRegisterViewController.swift index 8428aaa7..a1fd9742 100644 --- a/Mastodon/Scene/Onboarding/Register/MastodonRegisterViewController.swift +++ b/Mastodon/Scene/Onboarding/Register/MastodonRegisterViewController.swift @@ -16,11 +16,11 @@ final class MastodonRegisterViewController: UIViewController, NeedsDependency, O static let avatarImageMaxSizeInPixel = CGSize(width: 400, height: 400) - static let textFieldLabelFont = UIFontMetrics(forTextStyle: .headline).scaledFont(for: .systemFont(ofSize: 17, weight: .semibold), maximumPointSize: 22) - static let errorPromptLabelFont = UIFontMetrics(forTextStyle: .caption1).scaledFont(for: .systemFont(ofSize: 13, weight: .semibold), maximumPointSize: 18) + let logger = Logger(subsystem: "MastodonRegisterViewController", category: "ViewController") var disposeBag = Set() - + private var observations = Set() + weak var context: AppContext! { willSet { precondition(!isViewLoaded) } } weak var coordinator: SceneCoordinator! { willSet { precondition(!isViewLoaded) } } @@ -51,236 +51,30 @@ final class MastodonRegisterViewController: UIViewController, NeedsDependency, O let tapGestureRecognizer = UITapGestureRecognizer.singleTapGestureRecognizer - let scrollView: UIScrollView = { - let scrollview = UIScrollView() - scrollview.showsVerticalScrollIndicator = false - scrollview.keyboardDismissMode = .interactive - scrollview.alwaysBounceVertical = true - scrollview.clipsToBounds = false // make content could display over bleeding - scrollview.translatesAutoresizingMaskIntoConstraints = false - return scrollview + let tableView: UITableView = { + let tableView = UITableView() + tableView.rowHeight = UITableView.automaticDimension + tableView.separatorStyle = .none + tableView.backgroundColor = .clear + tableView.keyboardDismissMode = .onDrag + if #available(iOS 15.0, *) { + tableView.sectionHeaderTopPadding = .leastNonzeroMagnitude + } else { + // Fallback on earlier versions + } + return tableView }() - let stackView = UIStackView() - - let largeTitleLabel: UILabel = { - let label = UILabel() - label.font = UIFontMetrics(forTextStyle: .largeTitle).scaledFont(for: .systemFont(ofSize: 34, weight: .bold)) - label.textColor = Asset.Colors.Label.primary.color - label.text = L10n.Scene.Register.title - label.numberOfLines = 0 - return label - }() - - let avatarView: UIView = { - let view = UIView() - view.backgroundColor = .clear - return view - }() - - let avatarButton: UIButton = { - let button = HighlightDimmableButton() - let boldFont = UIFont.systemFont(ofSize: 42) - let configuration = UIImage.SymbolConfiguration(font: boldFont) - let image = UIImage(systemName: "person.fill.viewfinder", withConfiguration: configuration) - - button.setImage(image?.withRenderingMode(UIImage.RenderingMode.alwaysTemplate), for: UIControl.State.normal) - button.imageView?.tintColor = Asset.Colors.Label.secondary.color - button.backgroundColor = Asset.Theme.Mastodon.secondaryGroupedSystemBackground.color - button.layer.cornerRadius = 10 - button.clipsToBounds = true - - return button - }() - - let plusIconImageView: UIImageView = { - let icon = UIImageView() - let image = Asset.Circles.plusCircleFill.image.withRenderingMode(.alwaysTemplate) - icon.image = image - icon.tintColor = Asset.Colors.Icon.plus.color - icon.backgroundColor = UIColor(dynamicProvider: { collection in - switch collection.userInterfaceStyle { - case .dark: - return Asset.Theme.Mastodon.secondaryGroupedSystemBackground.color - default: - return .white - } - }) - return icon - }() - - let domainLabel: UILabel = { - let label = UILabel() - label.font = MastodonRegisterViewController.textFieldLabelFont - label.textColor = Asset.Colors.Label.primary.color - return label - }() - - let usernameTextField: UITextField = { - let textField = UITextField() - textField.returnKeyType = .next - textField.autocapitalizationType = .none - textField.autocorrectionType = .no - textField.backgroundColor = Asset.Theme.Mastodon.secondaryGroupedSystemBackground.color - textField.textColor = Asset.Colors.Label.primary.color - textField.attributedPlaceholder = NSAttributedString(string: L10n.Scene.Register.Input.Username.placeholder, - attributes: [NSAttributedString.Key.foregroundColor: Asset.Colors.Label.secondary.color, - NSAttributedString.Key.font: MastodonRegisterViewController.textFieldLabelFont]) - textField.borderStyle = UITextField.BorderStyle.roundedRect - textField.font = MastodonRegisterViewController.textFieldLabelFont - textField.leftView = { - let containerView = UIView() - - let paddingView = UIView(frame: CGRect(x: 0, y: 0, width: 5, height: textField.frame.height)) - paddingView.translatesAutoresizingMaskIntoConstraints = false - containerView.addSubview(paddingView) - NSLayoutConstraint.activate([ - paddingView.topAnchor.constraint(equalTo: containerView.topAnchor), - paddingView.leadingAnchor.constraint(equalTo: containerView.leadingAnchor), - paddingView.bottomAnchor.constraint(equalTo: containerView.bottomAnchor), - paddingView.widthAnchor.constraint(equalToConstant: 5).priority(.defaultHigh), - ]) - - let label = UILabel() - label.font = MastodonRegisterViewController.textFieldLabelFont - label.textColor = Asset.Colors.Label.primary.color - label.text = " @" - - label.translatesAutoresizingMaskIntoConstraints = false - containerView.addSubview(label) - NSLayoutConstraint.activate([ - label.topAnchor.constraint(equalTo: containerView.topAnchor), - label.leadingAnchor.constraint(equalTo: paddingView.trailingAnchor), - label.trailingAnchor.constraint(equalTo: containerView.trailingAnchor), - label.bottomAnchor.constraint(equalTo: containerView.bottomAnchor), - ]) - return containerView - }() - textField.leftViewMode = .always - return textField - }() - - let usernameErrorPromptLabel: UILabel = { - let label = UILabel() - let color = Asset.Colors.danger.color - let font = MastodonRegisterViewController.errorPromptLabelFont - return label - }() - - let displayNameTextField: UITextField = { - let textField = UITextField() - textField.returnKeyType = .next - textField.autocapitalizationType = .none - textField.autocorrectionType = .no - textField.backgroundColor = Asset.Theme.Mastodon.secondaryGroupedSystemBackground.color - textField.textColor = Asset.Colors.Label.primary.color - textField.attributedPlaceholder = NSAttributedString(string: L10n.Scene.Register.Input.DisplayName.placeholder, - attributes: [NSAttributedString.Key.foregroundColor: Asset.Colors.Label.secondary.color, - NSAttributedString.Key.font: MastodonRegisterViewController.textFieldLabelFont]) - textField.borderStyle = UITextField.BorderStyle.roundedRect - let paddingView = UIView(frame: CGRect(x: 0, y: 0, width: 5, height: textField.frame.height)) - textField.leftView = paddingView - textField.leftViewMode = .always - textField.font = MastodonRegisterViewController.textFieldLabelFont - return textField - }() - - let emailTextField: UITextField = { - let textField = UITextField() - textField.returnKeyType = .next - textField.autocapitalizationType = .none - textField.autocorrectionType = .no - textField.keyboardType = .emailAddress - textField.backgroundColor = Asset.Theme.Mastodon.secondaryGroupedSystemBackground.color - textField.textColor = Asset.Colors.Label.primary.color - textField.attributedPlaceholder = NSAttributedString(string: L10n.Scene.Register.Input.Email.placeholder, - attributes: [NSAttributedString.Key.foregroundColor: Asset.Colors.Label.secondary.color, - NSAttributedString.Key.font: MastodonRegisterViewController.textFieldLabelFont]) - textField.borderStyle = UITextField.BorderStyle.roundedRect - let paddingView = UIView(frame: CGRect(x: 0, y: 0, width: 5, height: textField.frame.height)) - textField.leftView = paddingView - textField.leftViewMode = .always - textField.font = MastodonRegisterViewController.textFieldLabelFont - return textField - }() - - let emailErrorPromptLabel: UILabel = { - let label = UILabel() - let color = Asset.Colors.danger.color - let font = MastodonRegisterViewController.errorPromptLabelFont - return label - }() - - let passwordTextField: UITextField = { - let textField = UITextField() - textField.returnKeyType = .next // set to "Return" depends on if the last input field or not - textField.autocapitalizationType = .none - textField.autocorrectionType = .no - textField.keyboardType = .asciiCapable - textField.isSecureTextEntry = true - textField.backgroundColor = Asset.Theme.Mastodon.secondaryGroupedSystemBackground.color - textField.textColor = Asset.Colors.Label.primary.color - textField.attributedPlaceholder = NSAttributedString(string: L10n.Scene.Register.Input.Password.placeholder, - attributes: [NSAttributedString.Key.foregroundColor: Asset.Colors.Label.secondary.color, - NSAttributedString.Key.font: MastodonRegisterViewController.textFieldLabelFont]) - textField.borderStyle = UITextField.BorderStyle.roundedRect - let paddingView = UIView(frame: CGRect(x: 0, y: 0, width: 5, height: textField.frame.height)) - textField.leftView = paddingView - textField.leftViewMode = .always - textField.font = MastodonRegisterViewController.textFieldLabelFont - return textField - }() - - let passwordCheckLabel: UILabel = { - let label = UILabel() - label.numberOfLines = 0 - return label - }() - - let passwordErrorPromptLabel: UILabel = { - let label = UILabel() - let color = Asset.Colors.danger.color - let font = MastodonRegisterViewController.errorPromptLabelFont - return label - }() - - - lazy var reasonTextField: UITextField = { - let textField = UITextField() - textField.returnKeyType = .next // set to "Return" depends on if the last input field or not - textField.autocapitalizationType = .none - textField.autocorrectionType = .no - textField.backgroundColor = Asset.Theme.Mastodon.secondaryGroupedSystemBackground.color - textField.textColor = Asset.Colors.Label.primary.color - textField.attributedPlaceholder = NSAttributedString(string: L10n.Scene.Register.Input.Invite.registrationUserInviteRequest, - attributes: [NSAttributedString.Key.foregroundColor: Asset.Colors.Label.secondary.color, - NSAttributedString.Key.font: MastodonRegisterViewController.textFieldLabelFont]) - textField.borderStyle = UITextField.BorderStyle.roundedRect - let paddingView = UIView(frame: CGRect(x: 0, y: 0, width: 5, height: textField.frame.height)) - textField.leftView = paddingView - textField.leftViewMode = .always - textField.font = MastodonRegisterViewController.textFieldLabelFont - return textField - }() - - let reasonErrorPromptLabel: UILabel = { - let label = UILabel() - let color = Asset.Colors.danger.color - let font = MastodonRegisterViewController.errorPromptLabelFont - return label - }() - - let buttonContainer = UIView() - let signUpButton: PrimaryActionButton = { - let button = PrimaryActionButton() - button.isEnabled = false - button.setTitle(L10n.Common.Controls.Actions.continue, for: .normal) - return button + let navigationActionView: NavigationActionView = { + let navigationActionView = NavigationActionView() + navigationActionView.backgroundColor = Asset.Scene.Onboarding.onboardingBackground.color + return navigationActionView }() deinit { os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s", (#file as NSString).lastPathComponent, #line, #function) } + } extension MastodonRegisterViewController { @@ -288,518 +82,203 @@ extension MastodonRegisterViewController { override func viewDidLoad() { super.viewDidLoad() + navigationItem.leftBarButtonItem = UIBarButtonItem() + setupOnboardingAppearance() - configureTitleLabel() defer { setupNavigationBarBackgroundView() - configureFormLayout() } - avatarButton.menu = createMediaContextMenu() - avatarButton.showsMenuAsPrimaryAction = true + tableView.translatesAutoresizingMaskIntoConstraints = false + view.addSubview(tableView) + NSLayoutConstraint.activate([ + tableView.topAnchor.constraint(equalTo: view.topAnchor), + tableView.leadingAnchor.constraint(equalTo: view.leadingAnchor), + tableView.trailingAnchor.constraint(equalTo: view.trailingAnchor), + tableView.bottomAnchor.constraint(equalTo: view.bottomAnchor), + ]) - domainLabel.text = "@" + viewModel.domain + " " - domainLabel.sizeToFit() - passwordCheckLabel.attributedText = MastodonRegisterViewModel.attributeStringForPassword(validateState: .empty) - usernameTextField.rightView = domainLabel - usernameTextField.rightViewMode = .always - usernameTextField.delegate = self - displayNameTextField.delegate = self - emailTextField.delegate = self - passwordTextField.delegate = self - - // gesture - view.addGestureRecognizer(tapGestureRecognizer) - tapGestureRecognizer.addTarget(self, action: #selector(tapGestureRecognizerHandler)) - - // stackView - stackView.axis = .vertical - stackView.distribution = .fill - stackView.spacing = 40 - stackView.layoutMargins = UIEdgeInsets(top: 20, left: 0, bottom: 26, right: 0) - stackView.isLayoutMarginsRelativeArrangement = true - stackView.addArrangedSubview(largeTitleLabel) - stackView.addArrangedSubview(avatarView) - - let formTableStackView = UIStackView() - stackView.addArrangedSubview(formTableStackView) - formTableStackView.axis = .vertical - formTableStackView.distribution = .fill - formTableStackView.spacing = 40 - - formTableStackView.addArrangedSubview(usernameTextField) - formTableStackView.addArrangedSubview(displayNameTextField) - formTableStackView.addArrangedSubview(emailTextField) - formTableStackView.addArrangedSubview(passwordTextField) - formTableStackView.addArrangedSubview(passwordCheckLabel) - if viewModel.approvalRequired { - formTableStackView.addArrangedSubview(reasonTextField) + navigationActionView.translatesAutoresizingMaskIntoConstraints = false + view.addSubview(navigationActionView) + defer { + view.bringSubviewToFront(navigationActionView) } - - usernameErrorPromptLabel.translatesAutoresizingMaskIntoConstraints = false - formTableStackView.addSubview(usernameErrorPromptLabel) NSLayoutConstraint.activate([ - usernameErrorPromptLabel.topAnchor.constraint(equalTo: usernameTextField.bottomAnchor, constant: 6), - usernameErrorPromptLabel.leadingAnchor.constraint(equalTo: usernameTextField.leadingAnchor), - usernameErrorPromptLabel.trailingAnchor.constraint(equalTo: usernameTextField.trailingAnchor), + navigationActionView.leadingAnchor.constraint(equalTo: view.leadingAnchor), + navigationActionView.trailingAnchor.constraint(equalTo: view.trailingAnchor), + view.bottomAnchor.constraint(equalTo: navigationActionView.bottomAnchor), ]) - emailErrorPromptLabel.translatesAutoresizingMaskIntoConstraints = false - formTableStackView.addSubview(emailErrorPromptLabel) - NSLayoutConstraint.activate([ - emailErrorPromptLabel.topAnchor.constraint(equalTo: emailTextField.bottomAnchor, constant: 6), - emailErrorPromptLabel.leadingAnchor.constraint(equalTo: emailTextField.leadingAnchor), - emailErrorPromptLabel.trailingAnchor.constraint(equalTo: emailTextField.trailingAnchor), - ]) + navigationActionView + .observe(\.bounds, options: [.initial, .new]) { [weak self] navigationActionView, _ in + guard let self = self else { return } + let inset = navigationActionView.frame.height + self.tableView.contentInset.bottom = inset + } + .store(in: &observations) - passwordErrorPromptLabel.translatesAutoresizingMaskIntoConstraints = false - formTableStackView.addSubview(passwordErrorPromptLabel) - NSLayoutConstraint.activate([ - passwordErrorPromptLabel.topAnchor.constraint(equalTo: passwordCheckLabel.bottomAnchor, constant: 2), - passwordErrorPromptLabel.leadingAnchor.constraint(equalTo: passwordTextField.leadingAnchor), - passwordErrorPromptLabel.trailingAnchor.constraint(equalTo: passwordTextField.trailingAnchor), - ]) - - // scrollView - view.addSubview(scrollView) - NSLayoutConstraint.activate([ - 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.bottomAnchor.constraint(equalTo: view.layoutMarginsGuide.bottomAnchor), - scrollView.frameLayoutGuide.widthAnchor.constraint(equalTo: scrollView.contentLayoutGuide.widthAnchor), - ]) - - // stackView - scrollView.addSubview(stackView) - stackView.translatesAutoresizingMaskIntoConstraints = false - NSLayoutConstraint.activate([ - stackView.topAnchor.constraint(equalTo: scrollView.contentLayoutGuide.topAnchor), - stackView.leadingAnchor.constraint(equalTo: scrollView.contentLayoutGuide.leadingAnchor), - stackView.trailingAnchor.constraint(equalTo: scrollView.contentLayoutGuide.trailingAnchor), - stackView.widthAnchor.constraint(equalTo: scrollView.contentLayoutGuide.widthAnchor), - scrollView.contentLayoutGuide.bottomAnchor.constraint(equalTo: stackView.bottomAnchor), - ]) - - // photoview - avatarView.translatesAutoresizingMaskIntoConstraints = false - avatarView.addSubview(avatarButton) - NSLayoutConstraint.activate([ - avatarView.heightAnchor.constraint(equalToConstant: 92).priority(.required - 1), - ]) - avatarButton.translatesAutoresizingMaskIntoConstraints = false - NSLayoutConstraint.activate([ - avatarButton.heightAnchor.constraint(equalToConstant: 92).priority(.required - 1), - avatarButton.widthAnchor.constraint(equalToConstant: 92).priority(.required - 1), - avatarButton.leadingAnchor.constraint(greaterThanOrEqualTo: avatarView.leadingAnchor).priority(.required - 1), - avatarView.trailingAnchor.constraint(greaterThanOrEqualTo: avatarButton.trailingAnchor).priority(.required - 1), - avatarButton.centerXAnchor.constraint(equalTo: avatarView.centerXAnchor), - avatarButton.centerYAnchor.constraint(equalTo: avatarView.centerYAnchor), - ]) - - plusIconImageView.translatesAutoresizingMaskIntoConstraints = false - avatarView.addSubview(plusIconImageView) - NSLayoutConstraint.activate([ - plusIconImageView.centerXAnchor.constraint(equalTo: avatarButton.trailingAnchor), - plusIconImageView.centerYAnchor.constraint(equalTo: avatarButton.bottomAnchor), - ]) - - // textfield - NSLayoutConstraint.activate([ - usernameTextField.heightAnchor.constraint(equalToConstant: 50).priority(.required - 1), - displayNameTextField.heightAnchor.constraint(equalToConstant: 50).priority(.required - 1), - emailTextField.heightAnchor.constraint(equalToConstant: 50).priority(.required - 1), - passwordTextField.heightAnchor.constraint(equalToConstant: 50).priority(.required - 1), - ]) - - // password - formTableStackView.setCustomSpacing(6, after: passwordTextField) - formTableStackView.setCustomSpacing(32, after: passwordCheckLabel) + navigationActionView.backButton.addTarget(self, action: #selector(MastodonRegisterViewController.backButtonPressed(_:)), for: .touchUpInside) + navigationActionView.nextButton.addTarget(self, action: #selector(MastodonRegisterViewController.nextButtonPressed(_:)), for: .touchUpInside) - // return - if viewModel.approvalRequired { - reasonTextField.returnKeyType = .done - } else { - passwordTextField.returnKeyType = .done - } - - // button - formTableStackView.addArrangedSubview(buttonContainer) - signUpButton.translatesAutoresizingMaskIntoConstraints = false - buttonContainer.addSubview(signUpButton) - NSLayoutConstraint.activate([ - signUpButton.topAnchor.constraint(equalTo: buttonContainer.topAnchor), - signUpButton.leadingAnchor.constraint(equalTo: buttonContainer.leadingAnchor), - buttonContainer.trailingAnchor.constraint(equalTo: signUpButton.trailingAnchor), - buttonContainer.bottomAnchor.constraint(equalTo: signUpButton.bottomAnchor), - signUpButton.heightAnchor.constraint(equalToConstant: MastodonRegisterViewController.actionButtonHeight).priority(.required - 1), - buttonContainer.heightAnchor.constraint(equalToConstant: MastodonRegisterViewController.actionButtonHeight).priority(.required - 1), - ]) - signUpButton.setContentHuggingPriority(.defaultLow, for: .horizontal) - signUpButton.setContentHuggingPriority(.defaultLow, for: .vertical) - signUpButton.setContentCompressionResistancePriority(.required - 1, for: .vertical) - signUpButton.setContentCompressionResistancePriority(.required - 1, for: .horizontal) - buttonContainer.setContentCompressionResistancePriority(.required - 1, for: .vertical) - - Publishers.CombineLatest( - KeyboardResponderService.shared.state.eraseToAnyPublisher(), - KeyboardResponderService.shared.endFrame.eraseToAnyPublisher() - ) - .sink(receiveValue: { [weak self] state, endFrame in - guard let self = self else { return } - - guard state == .dock else { - self.scrollView.contentInset.bottom = 0.0 - self.scrollView.verticalScrollIndicatorInsets.bottom = 0.0 - return - } - - let contentFrame = self.view.convert(self.scrollView.frame, to: nil) - let padding = contentFrame.maxY - endFrame.minY - guard padding > 0 else { - self.scrollView.contentInset.bottom = 0.0 - self.scrollView.verticalScrollIndicatorInsets.bottom = 0.0 - return - } - - self.scrollView.contentInset.bottom = padding + 16 - self.scrollView.verticalScrollIndicatorInsets.bottom = padding + 16 - - if self.passwordTextField.isFirstResponder { - 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 { - self.scrollView.setContentOffset(CGPoint(x: 0, y: contentOffsetY + labelPadding + 16.0), animated: true) - } - } - }) - .store(in: &disposeBag) - - avatarButton.publisher(for: \.isHighlighted, options: .new) - .receive(on: DispatchQueue.main) - .sink { [weak self] isHighlighted in - guard let self = self else { return } - let alpha: CGFloat = isHighlighted ? 0.6 : 1 - self.plusIconImageView.alpha = alpha - } - .store(in: &disposeBag) - - viewModel.isRegistering - .receive(on: DispatchQueue.main) - .sink { [weak self] isRegistering in - guard let self = self else { return } - isRegistering ? self.signUpButton.showLoading() : self.signUpButton.stopLoading() - } - .store(in: &disposeBag) - - viewModel.usernameValidateState - .receive(on: DispatchQueue.main) - .sink { [weak self] validateState in - guard let self = self else { return } - self.setTextFieldValidAppearance(self.usernameTextField, validateState: validateState) - } - .store(in: &disposeBag) - viewModel.usernameErrorPrompt - .receive(on: DispatchQueue.main) - .sink { [weak self] prompt in - guard let self = self else { return } - self.usernameErrorPromptLabel.attributedText = prompt - } - .store(in: &disposeBag) - viewModel.displayNameValidateState - .receive(on: DispatchQueue.main) - .sink { [weak self] validateState in - guard let self = self else { return } - self.setTextFieldValidAppearance(self.displayNameTextField, validateState: validateState) - } - .store(in: &disposeBag) - viewModel.emailValidateState - .receive(on: DispatchQueue.main) - .sink { [weak self] validateState in - guard let self = self else { return } - self.setTextFieldValidAppearance(self.emailTextField, validateState: validateState) - } - .store(in: &disposeBag) - viewModel.emailErrorPrompt - .receive(on: DispatchQueue.main) - .sink { [weak self] prompt in - guard let self = self else { return } - self.emailErrorPromptLabel.attributedText = prompt - } - .store(in: &disposeBag) - viewModel.passwordValidateState - .receive(on: DispatchQueue.main) - .sink { [weak self] validateState in - guard let self = self else { return } - self.setTextFieldValidAppearance(self.passwordTextField, validateState: validateState) - self.passwordCheckLabel.attributedText = MastodonRegisterViewModel.attributeStringForPassword(validateState: validateState) - } - .store(in: &disposeBag) - viewModel.passwordErrorPrompt - .receive(on: DispatchQueue.main) - .sink { [weak self] prompt in - guard let self = self else { return } - self.passwordErrorPromptLabel.attributedText = prompt - } - .store(in: &disposeBag) - viewModel.reasonErrorPrompt - .receive(on: DispatchQueue.main) - .sink { [weak self] prompt in - guard let self = self else { return } - self.reasonErrorPromptLabel.attributedText = prompt - } - .store(in: &disposeBag) - - viewModel.isAllValid + viewModel.$isAllValid .receive(on: DispatchQueue.main) .sink { [weak self] isAllValid in guard let self = self else { return } - self.signUpButton.isEnabled = isAllValid + self.navigationActionView.nextButton.isEnabled = isAllValid } .store(in: &disposeBag) + + viewModel.setupDiffableDataSource(tableView: tableView) + +// KeyboardResponderService +// .configure( +// scrollView: tableView, +// layoutNeedsUpdate: viewModel.viewDidAppear.eraseToAnyPublisher() +// ) +// .store(in: &disposeBag) - viewModel.error - .receive(on: DispatchQueue.main) - .sink { [weak self] error in - guard let self = self else { return } - guard let error = error as? Mastodon.API.Error else { return } - let alertController = UIAlertController(for: error, title: "Sign Up Failure", preferredStyle: .alert) - let okAction = UIAlertAction(title: L10n.Common.Controls.Actions.ok, style: .default, handler: nil) - alertController.addAction(okAction) - self.coordinator.present( - scene: .alertController(alertController: alertController), - from: nil, - transition: .alertController(animated: true, completion: nil) - ) - } - .store(in: &disposeBag) + // gesture + view.addGestureRecognizer(tapGestureRecognizer) + tapGestureRecognizer.addTarget(self, action: #selector(tapGestureRecognizerHandler)) - viewModel.avatarImage +// // return +// if viewModel.approvalRequired { +// reasonTextField.returnKeyType = .done +// } else { +// passwordTextField.returnKeyType = .done +// } +// +// viewModel.usernameValidateState +// .receive(on: DispatchQueue.main) +// .sink { [weak self] validateState in +// guard let self = self else { return } +// self.setTextFieldValidAppearance(self.usernameTextField, validateState: validateState) +// } +// .store(in: &disposeBag) +// viewModel.usernameErrorPrompt +// .receive(on: DispatchQueue.main) +// .sink { [weak self] prompt in +// guard let self = self else { return } +// self.usernameErrorPromptLabel.attributedText = prompt +// } +// .store(in: &disposeBag) +// viewModel.displayNameValidateState +// .receive(on: DispatchQueue.main) +// .sink { [weak self] validateState in +// guard let self = self else { return } +// self.setTextFieldValidAppearance(self.displayNameTextField, validateState: validateState) +// } +// .store(in: &disposeBag) +// viewModel.emailValidateState +// .receive(on: DispatchQueue.main) +// .sink { [weak self] validateState in +// guard let self = self else { return } +// self.setTextFieldValidAppearance(self.emailTextField, validateState: validateState) +// } +// .store(in: &disposeBag) +// viewModel.emailErrorPrompt +// .receive(on: DispatchQueue.main) +// .sink { [weak self] prompt in +// guard let self = self else { return } +// self.emailErrorPromptLabel.attributedText = prompt +// } +// .store(in: &disposeBag) +// viewModel.passwordValidateState +// .receive(on: DispatchQueue.main) +// .sink { [weak self] validateState in +// guard let self = self else { return } +// self.setTextFieldValidAppearance(self.passwordTextField, validateState: validateState) +// self.passwordCheckLabel.attributedText = MastodonRegisterViewModel.attributeStringForPassword(validateState: validateState) +// } +// .store(in: &disposeBag) +// viewModel.passwordErrorPrompt +// .receive(on: DispatchQueue.main) +// .sink { [weak self] prompt in +// guard let self = self else { return } +// self.passwordErrorPromptLabel.attributedText = prompt +// } +// .store(in: &disposeBag) +// viewModel.reasonErrorPrompt +// .receive(on: DispatchQueue.main) +// .sink { [weak self] prompt in +// guard let self = self else { return } +// self.reasonErrorPromptLabel.attributedText = prompt +// } +// .store(in: &disposeBag) +// viewModel.error +// .receive(on: DispatchQueue.main) +// .sink { [weak self] error in +// guard let self = self else { return } +// guard let error = error as? Mastodon.API.Error else { return } +// let alertController = UIAlertController(for: error, title: "Sign Up Failure", preferredStyle: .alert) +// let okAction = UIAlertAction(title: L10n.Common.Controls.Actions.ok, style: .default, handler: nil) +// alertController.addAction(okAction) +// self.coordinator.present( +// scene: .alertController(alertController: alertController), +// from: nil, +// transition: .alertController(animated: true, completion: nil) +// ) +// } +// .store(in: &disposeBag) +// + + viewModel.avatarMediaMenuActionPublisher .receive(on: DispatchQueue.main) - .sink{ [weak self] image in + .sink { [weak self] action in guard let self = self else { return } - self.avatarButton.menu = self.createMediaContextMenu() - if let avatar = image { - self.avatarButton.setImage(avatar, for: .normal) - } else { - let boldFont = UIFont.systemFont(ofSize: 42) - let configuration = UIImage.SymbolConfiguration(font: boldFont) - let image = UIImage(systemName: "person.fill.viewfinder", withConfiguration: configuration) - self.avatarButton.setImage(image?.withRenderingMode(UIImage.RenderingMode.alwaysTemplate), for: UIControl.State.normal) + switch action { + case .photoLibrary: + self.present(self.imagePicker, animated: true, completion: nil) + case .camera: + self.present(self.imagePickerController, animated: true, completion: nil) + case .browse: + self.present(self.documentPickerController, animated: true, completion: nil) + case .delete: + self.viewModel.avatarImage = nil } } .store(in: &disposeBag) - NotificationCenter.default - .publisher(for: UITextField.textDidChangeNotification, object: usernameTextField) + + viewModel.$isRegistering .receive(on: DispatchQueue.main) - .sink { [weak self] _ in + .sink { [weak self] isRegistering in guard let self = self else { return } - self.viewModel.username.value = self.usernameTextField.text?.trimmingCharacters(in: .whitespacesAndNewlines) ?? "" + isRegistering ? self.navigationActionView.nextButton.showLoading() : self.navigationActionView.nextButton.stopLoading() } .store(in: &disposeBag) - - NotificationCenter.default - .publisher(for: UITextField.textDidChangeNotification, object: displayNameTextField) - .receive(on: DispatchQueue.main) - .sink { [weak self] _ in - guard let self = self else { return } - self.viewModel.displayName.value = self.displayNameTextField.text?.trimmingCharacters(in: .whitespacesAndNewlines) ?? "" - } - .store(in: &disposeBag) - - NotificationCenter.default - .publisher(for: UITextField.textDidChangeNotification, object: emailTextField) - .receive(on: DispatchQueue.main) - .sink { [weak self] _ in - guard let self = self else { return } - self.viewModel.email.value = self.emailTextField.text?.trimmingCharacters(in: .whitespacesAndNewlines) ?? "" - } - .store(in: &disposeBag) - - NotificationCenter.default - .publisher(for: UITextField.textDidChangeNotification, object: passwordTextField) - .receive(on: DispatchQueue.main) - .sink { [weak self] _ in - guard let self = self else { return } - self.viewModel.password.value = self.passwordTextField.text?.trimmingCharacters(in: .whitespacesAndNewlines) ?? "" - } - .store(in: &disposeBag) - - if viewModel.approvalRequired { - reasonTextField.delegate = self - NSLayoutConstraint.activate([ - reasonTextField.heightAnchor.constraint(equalToConstant: 50).priority(.defaultHigh), - ]) - reasonErrorPromptLabel.translatesAutoresizingMaskIntoConstraints = false - stackView.addSubview(reasonErrorPromptLabel) - NSLayoutConstraint.activate([ - reasonErrorPromptLabel.topAnchor.constraint(equalTo: reasonTextField.bottomAnchor, constant: 6), - reasonErrorPromptLabel.leadingAnchor.constraint(equalTo: reasonTextField.leadingAnchor), - reasonErrorPromptLabel.trailingAnchor.constraint(equalTo: reasonTextField.trailingAnchor), - ]) - - viewModel.reasonValidateState - .receive(on: DispatchQueue.main) - .sink { [weak self] validateState in - guard let self = self else { return } - self.setTextFieldValidAppearance(self.reasonTextField, validateState: validateState) - } - .store(in: &disposeBag) - NotificationCenter.default - .publisher(for: UITextField.textDidChangeNotification, object: reasonTextField) - .receive(on: DispatchQueue.main) - .sink { [weak self] _ in - guard let self = self else { return } - self.viewModel.reason.value = self.reasonTextField.text?.trimmingCharacters(in: .whitespacesAndNewlines) ?? "" - } - .store(in: &disposeBag) - } - - signUpButton.addTarget(self, action: #selector(MastodonRegisterViewController.signUpButtonPressed(_:)), for: .touchUpInside) } - override func viewDidLayoutSubviews() { - super.viewDidLayoutSubviews() - plusIconImageView.layer.cornerRadius = plusIconImageView.frame.width / 2 - plusIconImageView.layer.masksToBounds = true - } - - override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) { - super.traitCollectionDidChange(previousTraitCollection) + override func viewDidAppear(_ animated: Bool) { + super.viewDidAppear(animated) - configureTitleLabel() - configureFormLayout() - } -} - -extension MastodonRegisterViewController: UITextFieldDelegate { - func textFieldDidBeginEditing(_ textField: UITextField) { - let text = textField.text?.trimmingCharacters(in: .whitespacesAndNewlines) ?? "" - - switch textField { - case usernameTextField: - viewModel.username.value = text - case displayNameTextField: - viewModel.displayName.value = text - case emailTextField: - viewModel.email.value = text - case passwordTextField: - viewModel.password.value = text - case reasonTextField: - viewModel.reason.value = text - default: - break - } + viewModel.viewDidAppear.send() } - func textFieldDidEndEditing(_ textField: UITextField) { - let text = textField.text?.trimmingCharacters(in: .whitespacesAndNewlines) ?? "" - - switch textField { - case usernameTextField: - viewModel.username.value = text - case displayNameTextField: - viewModel.displayName.value = text - case emailTextField: - viewModel.email.value = text - case passwordTextField: - viewModel.password.value = text - case reasonTextField: - viewModel.reason.value = text - default: - break - } - } - - func textFieldShouldReturn(_ textField: UITextField) -> Bool { - switch textField { - case usernameTextField: - displayNameTextField.becomeFirstResponder() - case displayNameTextField: - emailTextField.becomeFirstResponder() - case emailTextField: - passwordTextField.becomeFirstResponder() - case passwordTextField: - if viewModel.approvalRequired { - reasonTextField.becomeFirstResponder() - } else { - passwordTextField.resignFirstResponder() - } - case reasonTextField: - reasonTextField.resignFirstResponder() - default: - break - } - return true - } - - func showShadowWithColor(color: UIColor, textField: UITextField) { - // To apply Shadow - textField.layer.shadowOpacity = 1 - textField.layer.shadowRadius = 2.0 - textField.layer.shadowOffset = CGSize.zero - textField.layer.shadowColor = color.cgColor - // textField.layer.shadowPath = UIBezierPath(roundedRect: textField.bounds, byRoundingCorners: .allCorners, cornerRadii: CGSize(width: 2.0, height: 2.0)).cgPath - } - - private func setTextFieldValidAppearance(_ textField: UITextField, validateState: MastodonRegisterViewModel.ValidateState) { - switch validateState { - case .empty: - showShadowWithColor(color: textField.isFirstResponder ? Asset.Colors.brandBlue.color : .clear, textField: textField) - case .valid: - showShadowWithColor(color: Asset.Colors.TextField.valid.color, textField: textField) - case .invalid: - showShadowWithColor(color: Asset.Colors.TextField.invalid.color, textField: textField) - } - } } extension MastodonRegisterViewController { - private func configureTitleLabel() { - switch traitCollection.horizontalSizeClass { - case .regular: - navigationItem.largeTitleDisplayMode = .always - navigationItem.title = L10n.Scene.ServerPicker.title.replacingOccurrences(of: "\n", with: " ") - largeTitleLabel.isHidden = true - default: - navigationItem.largeTitleDisplayMode = .never - navigationItem.title = nil - largeTitleLabel.isHidden = false - } - } - private func configureFormLayout() { - switch traitCollection.horizontalSizeClass { - case .regular: - stackView.axis = .horizontal - stackView.distribution = .fillProportionally - default: - stackView.axis = .vertical - stackView.distribution = .fill - } - } - - private func configureMargin() { - - } -} - -extension MastodonRegisterViewController { @objc private func tapGestureRecognizerHandler(_ sender: UITapGestureRecognizer) { view.endEditing(true) } - @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.isAllValid.value else { return } - - guard !viewModel.isRegistering.value else { return } - viewModel.isRegistering.value = true + @objc private func backButtonPressed(_ sender: UIButton) { + logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public)") + navigationController?.popViewController(animated: true) + } - let username = viewModel.username.value - let email = viewModel.email.value - let password = viewModel.password.value + @objc private func nextButtonPressed(_ sender: UIButton) { + logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public)") + + guard viewModel.isAllValid else { return } + + guard !viewModel.isRegistering else { return } + viewModel.isRegistering = true + + let username = viewModel.username + let email = viewModel.email + let password = viewModel.password + let reason = viewModel.reason let locale: String = { guard let url = Bundle.main.url(forResource: "local-codes", withExtension: "json"), @@ -814,7 +293,7 @@ extension MastodonRegisterViewController { guard localCode[code] != nil else { return "en" } return code }() - + // pick device preferred language guard let identifier = Locale.preferredLanguages.first else { return fallbackLanguageCode @@ -843,19 +322,19 @@ extension MastodonRegisterViewController { return languageCode } return firstMatchExtendCode - + }() let query = Mastodon.API.Account.RegisterQuery( - reason: viewModel.reason.value, + reason: reason, username: username, email: email, password: password, agreement: true, // user confirmed in the server rules scene locale: locale ) - + var retryCount = 0 - + // register without show server rules context.apiService.accountRegister( domain: viewModel.domain, @@ -864,7 +343,7 @@ extension MastodonRegisterViewController { ) .tryCatch { [weak self] error -> AnyPublisher, Error> in guard let self = self else { throw error } - guard let error = self.viewModel.error.value as? Mastodon.API.Error, + guard let error = self.viewModel.error as? Mastodon.API.Error, case let .generic(errorEntity) = error.mastodonError, errorEntity.error == "Validation failed: Locale is not included in the list" else { @@ -891,10 +370,10 @@ extension MastodonRegisterViewController { .receive(on: DispatchQueue.main) .sink { [weak self] completion in guard let self = self else { return } - self.viewModel.isRegistering.value = false + self.viewModel.isRegistering = false switch completion { case .failure(let error): - self.viewModel.error.send(error) + self.viewModel.error = error case .finished: break } @@ -902,9 +381,9 @@ extension MastodonRegisterViewController { guard let self = self else { return } let userToken = response.value let updateCredentialQuery: Mastodon.API.Account.UpdateCredentialQuery = { - let displayName: String? = self.viewModel.displayName.value.isEmpty ? nil : self.viewModel.displayName.value + let displayName: String? = self.viewModel.name.isEmpty ? nil : self.viewModel.name let avatar: Mastodon.Query.MediaAttachment? = { - guard let avatarImage = self.viewModel.avatarImage.value else { return nil } + guard let avatarImage = self.viewModel.avatarImage else { return nil } guard avatarImage.size.width <= MastodonRegisterViewController.avatarImageMaxSizeInPixel.width else { return .png(avatarImage.af.imageScaled(to: MastodonRegisterViewController.avatarImageMaxSizeInPixel).pngData()) } @@ -920,4 +399,67 @@ extension MastodonRegisterViewController { } .store(in: &disposeBag) } + +} + +extension MastodonRegisterViewController: UITextFieldDelegate { +// func textFieldDidBeginEditing(_ textField: UITextField) { +// let text = textField.text?.trimmingCharacters(in: .whitespacesAndNewlines) ?? "" +// +// switch textField { +// case usernameTextField: +// viewModel.username.value = text +// case displayNameTextField: +// viewModel.displayName.value = text +// case emailTextField: +// viewModel.email.value = text +// case passwordTextField: +// viewModel.password.value = text +// case reasonTextField: +// viewModel.reason.value = text +// default: +// break +// } +// } +// +// func textFieldDidEndEditing(_ textField: UITextField) { +// let text = textField.text?.trimmingCharacters(in: .whitespacesAndNewlines) ?? "" +// +// switch textField { +// case usernameTextField: +// viewModel.username.value = text +// case displayNameTextField: +// viewModel.displayName.value = text +// case emailTextField: +// viewModel.email.value = text +// case passwordTextField: +// viewModel.password.value = text +// case reasonTextField: +// viewModel.reason.value = text +// default: +// break +// } +// } +// +// func textFieldShouldReturn(_ textField: UITextField) -> Bool { +// switch textField { +// case usernameTextField: +// displayNameTextField.becomeFirstResponder() +// case displayNameTextField: +// emailTextField.becomeFirstResponder() +// case emailTextField: +// passwordTextField.becomeFirstResponder() +// case passwordTextField: +// if viewModel.approvalRequired { +// reasonTextField.becomeFirstResponder() +// } else { +// passwordTextField.resignFirstResponder() +// } +// case reasonTextField: +// reasonTextField.resignFirstResponder() +// default: +// break +// } +// return true +// } } diff --git a/Mastodon/Scene/Onboarding/Register/MastodonRegisterViewModel+Diffable.swift b/Mastodon/Scene/Onboarding/Register/MastodonRegisterViewModel+Diffable.swift new file mode 100644 index 00000000..e075f47c --- /dev/null +++ b/Mastodon/Scene/Onboarding/Register/MastodonRegisterViewModel+Diffable.swift @@ -0,0 +1,231 @@ +// +// MastodonRegisterViewModel+Diffable.swift +// Mastodon +// +// Created by MainasuK on 2022-1-5. +// + +import UIKit +import Combine + +extension MastodonRegisterViewModel { + func setupDiffableDataSource( + tableView: UITableView + ) { + tableView.register(OnboardingHeadlineTableViewCell.self, forCellReuseIdentifier: String(describing: OnboardingHeadlineTableViewCell.self)) + tableView.register(MastodonRegisterAvatarTableViewCell.self, forCellReuseIdentifier: String(describing: MastodonRegisterAvatarTableViewCell.self)) + tableView.register(MastodonRegisterTextFieldTableViewCell.self, forCellReuseIdentifier: String(describing: MastodonRegisterTextFieldTableViewCell.self)) + tableView.register(MastodonRegisterPasswordHintTableViewCell.self, forCellReuseIdentifier: String(describing: MastodonRegisterPasswordHintTableViewCell.self)) + + diffableDataSource = UITableViewDiffableDataSource(tableView: tableView) { tableView, indexPath, item in + switch item { + case .header: + let cell = tableView.dequeueReusableCell(withIdentifier: String(describing: OnboardingHeadlineTableViewCell.self), for: indexPath) as! OnboardingHeadlineTableViewCell + cell.titleLabel.text = L10n.Scene.Register.title + cell.subTitleLabel.isHidden = true + return cell + case .avatar: + let cell = tableView.dequeueReusableCell(withIdentifier: String(describing: MastodonRegisterAvatarTableViewCell.self), for: indexPath) as! MastodonRegisterAvatarTableViewCell + self.configureAvatar(cell: cell) + return cell + case .name: + let cell = tableView.dequeueReusableCell(withIdentifier: String(describing: MastodonRegisterTextFieldTableViewCell.self), for: indexPath) as! MastodonRegisterTextFieldTableViewCell + cell.setupTextViewPlaceholder(text: L10n.Scene.Register.Input.DisplayName.placeholder) + cell.textField.keyboardType = .default + cell.textField.autocapitalizationType = .words + cell.textField.text = self.name + NotificationCenter.default.publisher(for: UITextField.textDidChangeNotification, object: cell.textField) + .receive(on: DispatchQueue.main) + .compactMap { notification in + guard let textField = notification.object as? UITextField else { + assertionFailure() + return nil + } + return textField.text?.trimmingCharacters(in: .whitespacesAndNewlines) ?? "" + } + .assign(to: \.name, on: self) + .store(in: &cell.disposeBag) + return cell + case .username: + let cell = tableView.dequeueReusableCell(withIdentifier: String(describing: MastodonRegisterTextFieldTableViewCell.self), for: indexPath) as! MastodonRegisterTextFieldTableViewCell + cell.setupTextViewRightView(text: "@" + self.domain) + cell.setupTextViewPlaceholder(text: L10n.Scene.Register.Input.Username.placeholder) + cell.textField.keyboardType = .alphabet + cell.textField.autocorrectionType = .no + cell.textField.text = self.username + NotificationCenter.default.publisher(for: UITextField.textDidChangeNotification, object: cell.textField) + .receive(on: DispatchQueue.main) + .compactMap { notification in + guard let textField = notification.object as? UITextField else { + assertionFailure() + return nil + } + return textField.text?.trimmingCharacters(in: .whitespacesAndNewlines) ?? "" + } + .assign(to: \.username, on: self) + .store(in: &cell.disposeBag) + self.configureTextFieldCell(cell: cell, validateState: self.$usernameValidateState) + return cell + case .email: + let cell = tableView.dequeueReusableCell(withIdentifier: String(describing: MastodonRegisterTextFieldTableViewCell.self), for: indexPath) as! MastodonRegisterTextFieldTableViewCell + cell.setupTextViewPlaceholder(text: L10n.Scene.Register.Input.Email.placeholder) + cell.textField.keyboardType = .emailAddress + cell.textField.autocorrectionType = .no + cell.textField.text = self.email + NotificationCenter.default.publisher(for: UITextField.textDidChangeNotification, object: cell.textField) + .receive(on: DispatchQueue.main) + .compactMap { notification in + guard let textField = notification.object as? UITextField else { + assertionFailure() + return nil + } + return textField.text?.trimmingCharacters(in: .whitespacesAndNewlines) ?? "" + } + .assign(to: \.email, on: self) + .store(in: &cell.disposeBag) + self.configureTextFieldCell(cell: cell, validateState: self.$emailValidateState) + return cell + case .password: + let cell = tableView.dequeueReusableCell(withIdentifier: String(describing: MastodonRegisterTextFieldTableViewCell.self), for: indexPath) as! MastodonRegisterTextFieldTableViewCell + cell.setupTextViewPlaceholder(text: L10n.Scene.Register.Input.Password.placeholder) + cell.textField.keyboardType = .alphabet + cell.textField.autocorrectionType = .no + cell.textField.isSecureTextEntry = true + cell.textField.text = self.password + NotificationCenter.default.publisher(for: UITextField.textDidChangeNotification, object: cell.textField) + .receive(on: DispatchQueue.main) + .compactMap { notification in + guard let textField = notification.object as? UITextField else { + assertionFailure() + return nil + } + return textField.text?.trimmingCharacters(in: .whitespacesAndNewlines) ?? "" + } + .assign(to: \.password, on: self) + .store(in: &cell.disposeBag) + self.configureTextFieldCell(cell: cell, validateState: self.$passwordValidateState) + return cell + case .hint: + let cell = tableView.dequeueReusableCell(withIdentifier: String(describing: MastodonRegisterPasswordHintTableViewCell.self), for: indexPath) as! MastodonRegisterPasswordHintTableViewCell + return cell + case .reason: + let cell = tableView.dequeueReusableCell(withIdentifier: String(describing: MastodonRegisterTextFieldTableViewCell.self), for: indexPath) as! MastodonRegisterTextFieldTableViewCell + cell.setupTextViewPlaceholder(text: L10n.Scene.Register.Input.Invite.registrationUserInviteRequest) + cell.textField.keyboardType = .default + cell.textField.text = self.reason + NotificationCenter.default.publisher(for: UITextField.textDidChangeNotification, object: cell.textField) + .receive(on: DispatchQueue.main) + .compactMap { notification in + guard let textField = notification.object as? UITextField else { + assertionFailure() + return nil + } + return textField.text?.trimmingCharacters(in: .whitespacesAndNewlines) ?? "" + } + .assign(to: \.reason, on: self) + .store(in: &cell.disposeBag) + self.configureTextFieldCell(cell: cell, validateState: self.$reasonValidateState) + return cell + default: + assertionFailure() + return UITableViewCell() + } + } + + var snapshot = NSDiffableDataSourceSnapshot() + snapshot.appendSections([.main]) + snapshot.appendItems([.header], toSection: .main) + snapshot.appendItems([.avatar, .name, .username, .email, .password, .hint], toSection: .main) + if approvalRequired { + snapshot.appendItems([.reason], toSection: .main) + } + diffableDataSource?.applySnapshot(snapshot, animated: false, completion: nil) + } +} + +extension MastodonRegisterViewModel { + private func configureAvatar(cell: MastodonRegisterAvatarTableViewCell) { + self.$avatarImage + .receive(on: DispatchQueue.main) + .sink { [weak self, weak cell] image in + guard let self = self else { return } + guard let cell = cell else { return } + let image = image ?? Asset.Scene.Onboarding.avatarPlaceholder.image + cell.avatarButton.setImage(image, for: .normal) + cell.avatarButton.menu = self.createAvatarMediaContextMenu() + cell.avatarButton.showsMenuAsPrimaryAction = true + } + .store(in: &cell.disposeBag) + } + + enum AvatarMediaMenuAction { + case photoLibrary + case camera + case browse + case delete + } + + private func createAvatarMediaContextMenu() -> UIMenu { + var children: [UIMenuElement] = [] + + // Photo Library + let photoLibraryAction = UIAction(title: L10n.Scene.Compose.MediaSelection.photoLibrary, image: UIImage(systemName: "rectangle.on.rectangle"), identifier: nil, discoverabilityTitle: nil, attributes: [], state: .off) { [weak self] _ in + guard let self = self else { return } + self.avatarMediaMenuActionPublisher.send(.photoLibrary) + } + children.append(photoLibraryAction) + + // Camera + if UIImagePickerController.isSourceTypeAvailable(.camera) { + let cameraAction = UIAction(title: L10n.Scene.Compose.MediaSelection.camera, image: UIImage(systemName: "camera"), identifier: nil, discoverabilityTitle: nil, attributes: [], state: .off, handler: { [weak self] _ in + guard let self = self else { return } + self.avatarMediaMenuActionPublisher.send(.camera) + }) + children.append(cameraAction) + } + + // Browse + let browseAction = UIAction(title: L10n.Scene.Compose.MediaSelection.browse, image: UIImage(systemName: "ellipsis"), identifier: nil, discoverabilityTitle: nil, attributes: [], state: .off) { [weak self] _ in + guard let self = self else { return } + self.avatarMediaMenuActionPublisher.send(.browse) + } + children.append(browseAction) + + // Delete + if avatarImage != nil { + let deleteAction = UIAction(title: L10n.Scene.Register.Input.Avatar.delete, image: UIImage(systemName: "delete.left"), identifier: nil, discoverabilityTitle: nil, attributes: [.destructive], state: .off) { [weak self] _ in + guard let self = self else { return } + self.avatarMediaMenuActionPublisher.send(.delete) + } + children.append(deleteAction) + } + + return UIMenu(title: "", image: nil, identifier: nil, options: .displayInline, children: children) + } + + private func configureTextFieldCell( + cell: MastodonRegisterTextFieldTableViewCell, + validateState: Published.Publisher + ) { + Publishers.CombineLatest( + validateState, + cell.textField.publisher(for: \.isFirstResponder) + ) + .receive(on: DispatchQueue.main) + .sink { [weak cell] validateState, isFirstResponder in + guard let cell = cell else { return } + switch validateState { + case .empty: + cell.textFieldShadowContainer.shadowColor = isFirstResponder ? Asset.Colors.brandBlue.color : .black + cell.textFieldShadowContainer.shadowAlpha = isFirstResponder ? 1 : 0.25 + case .valid: + cell.textFieldShadowContainer.shadowColor = Asset.Colors.TextField.valid.color + cell.textFieldShadowContainer.shadowAlpha = 1 + case .invalid: + cell.textFieldShadowContainer.shadowColor = Asset.Colors.TextField.invalid.color + cell.textFieldShadowContainer.shadowAlpha = 1 + } + } + .store(in: &cell.disposeBag) + } +} diff --git a/Mastodon/Scene/Onboarding/Register/MastodonRegisterViewModel.swift b/Mastodon/Scene/Onboarding/Register/MastodonRegisterViewModel.swift index 6c9e0754..5971cc74 100644 --- a/Mastodon/Scene/Onboarding/Register/MastodonRegisterViewModel.swift +++ b/Mastodon/Scene/Onboarding/Register/MastodonRegisterViewModel.swift @@ -14,18 +14,19 @@ final class MastodonRegisterViewModel { var disposeBag = Set() // input + let context: AppContext let domain: String let authenticateInfo: AuthenticationViewModel.AuthenticateInfo let instance: Mastodon.Entity.Instance let applicationToken: Mastodon.Entity.Token - let context: AppContext - - let username = CurrentValueSubject("") - let displayName = CurrentValueSubject("") - let email = CurrentValueSubject("") - let password = CurrentValueSubject("") - let reason = CurrentValueSubject("") - let avatarImage = CurrentValueSubject(nil) + let viewDidAppear = CurrentValueSubject(Void()) + + @Published var avatarImage: UIImage? = nil + @Published var name = "" + @Published var username = "" + @Published var email = "" + @Published var password = "" + @Published var reason = "" let usernameErrorPrompt = CurrentValueSubject(nil) let emailErrorPrompt = CurrentValueSubject(nil) @@ -33,21 +34,25 @@ final class MastodonRegisterViewModel { let reasonErrorPrompt = CurrentValueSubject(nil) // output + var diffableDataSource: UITableViewDiffableDataSource? 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 reasonValidateState = CurrentValueSubject(.empty) + + @Published var usernameValidateState: ValidateState = .empty + @Published var displayNameValidateState: ValidateState = .empty + @Published var emailValidateState: ValidateState = .empty + @Published var passwordValidateState: ValidateState = .empty + @Published var reasonValidateState: ValidateState = .empty - let isRegistering = CurrentValueSubject(false) - let isAllValid = CurrentValueSubject(false) - let error = CurrentValueSubject(nil) + @Published var isRegistering = false + @Published var isAllValid = false + @Published var error: Error? = nil + + let avatarMediaMenuActionPublisher = PassthroughSubject() init( - domain: String, context: AppContext, + domain: String, authenticateInfo: AuthenticationViewModel.AuthenticateInfo, instance: Mastodon.Entity.Instance, applicationToken: Mastodon.Entity.Token @@ -60,7 +65,15 @@ final class MastodonRegisterViewModel { self.approvalRequired = instance.approvalRequired ?? false self.applicationAuthorization = Mastodon.API.OAuth.Authorization(accessToken: applicationToken.accessToken) - username + $name + .map { name in + guard !name.isEmpty else { return .empty } + return .valid + } + .assign(to: \.displayNameValidateState, on: self) + .store(in: &disposeBag) + + $username .map { username in guard !username.isEmpty else { return .empty } var isValid = true @@ -79,114 +92,120 @@ final class MastodonRegisterViewModel { } return isValid ? .valid : .invalid } - .assign(to: \.value, on: usernameValidateState) + .assign(to: \.usernameValidateState, on: self) .store(in: &disposeBag) - username - .filter { !$0.isEmpty } - .debounce(for: .milliseconds(300), scheduler: DispatchQueue.main) - .removeDuplicates() - .compactMap { [weak self] text -> AnyPublisher, Error>, Never>? in - guard let self = self else { return nil } - let query = Mastodon.API.Account.AccountLookupQuery(acct: text) - return context.apiService.accountLookup(domain: domain, query: query, authorization: self.applicationAuthorization) - .map { - response -> Result, Error> in - Result.success(response) - } - .catch { error in - Just(Result.failure(error)) - } - .eraseToAnyPublisher() - } - .switchToLatest() - .sink { [weak self] result in - guard let self = self else { return } - switch result { - case .success: - let text = L10n.Scene.Register.Error.Reason.taken(L10n.Scene.Register.Error.Item.username) - self.usernameErrorPrompt.value = MastodonRegisterViewModel.errorPromptAttributedString(for: text) - self.usernameValidateState.value = .invalid - case .failure: - break - } - } - .store(in: &disposeBag) - - usernameValidateState - .sink { [weak self] validateState in - if validateState == .valid { - self?.usernameErrorPrompt.value = nil - } - } - .store(in: &disposeBag) + // TODO: check username available +// username +// .filter { !$0.isEmpty } +// .debounce(for: .milliseconds(300), scheduler: DispatchQueue.main) +// .removeDuplicates() +// .compactMap { [weak self] text -> AnyPublisher, Error>, Never>? in +// guard let self = self else { return nil } +// let query = Mastodon.API.Account.AccountLookupQuery(acct: text) +// return context.apiService.accountLookup(domain: domain, query: query, authorization: self.applicationAuthorization) +// .map { +// response -> Result, Error> in +// Result.success(response) +// } +// .catch { error in +// Just(Result.failure(error)) +// } +// .eraseToAnyPublisher() +// } +// .switchToLatest() +// .sink { [weak self] result in +// guard let self = self else { return } +// switch result { +// case .success: +// let text = L10n.Scene.Register.Error.Reason.taken(L10n.Scene.Register.Error.Item.username) +// self.usernameErrorPrompt.value = MastodonRegisterViewModel.errorPromptAttributedString(for: text) +// self.usernameValidateState.value = .invalid +// case .failure: +// break +// } +// } +// .store(in: &disposeBag) +// +// usernameValidateState +// .sink { [weak self] validateState in +// if validateState == .valid { +// self?.usernameErrorPrompt.value = nil +// } +// } +// .store(in: &disposeBag) - displayName - .map { displayname in - guard !displayname.isEmpty else { return .empty } - return .valid - } - .assign(to: \.value, on: displayNameValidateState) - .store(in: &disposeBag) - email + $email .map { email in guard !email.isEmpty else { return .empty } return MastodonRegisterViewModel.isValidEmail(email) ? .valid : .invalid } - .assign(to: \.value, on: emailValidateState) + .assign(to: \.emailValidateState, on: self) .store(in: &disposeBag) - password + + $password .map { password in guard !password.isEmpty else { return .empty } return password.count >= 8 ? .valid : .invalid } - .assign(to: \.value, on: passwordValidateState) + .assign(to: \.passwordValidateState, on: self) .store(in: &disposeBag) + if approvalRequired { - reason + $reason .map { invite in guard !invite.isEmpty else { return .empty } return .valid } - .assign(to: \.value, on: reasonValidateState) + .assign(to: \.reasonValidateState, on: self) .store(in: &disposeBag) } - error - .sink { [weak self] error in - guard let self = self else { return } - let error = error as? Mastodon.API.Error - let mastodonError = error?.mastodonError - if case let .generic(genericMastodonError) = mastodonError, - let details = genericMastodonError.details - { - self.usernameErrorPrompt.value = details.usernameErrorDescriptions.first.flatMap { MastodonRegisterViewModel.errorPromptAttributedString(for: $0) } - self.emailErrorPrompt.value = details.emailErrorDescriptions.first.flatMap { MastodonRegisterViewModel.errorPromptAttributedString(for: $0) } - self.passwordErrorPrompt.value = details.passwordErrorDescriptions.first.flatMap { MastodonRegisterViewModel.errorPromptAttributedString(for: $0) } - self.reasonErrorPrompt.value = details.reasonErrorDescriptions.first.flatMap { MastodonRegisterViewModel.errorPromptAttributedString(for: $0) } - } else { - self.usernameErrorPrompt.value = nil - self.emailErrorPrompt.value = nil - self.passwordErrorPrompt.value = nil - self.reasonErrorPrompt.value = nil - } - } - .store(in: &disposeBag) - +// error +// .sink { [weak self] error in +// guard let self = self else { return } +// let error = error as? Mastodon.API.Error +// let mastodonError = error?.mastodonError +// if case let .generic(genericMastodonError) = mastodonError, +// let details = genericMastodonError.details +// { +// self.usernameErrorPrompt.value = details.usernameErrorDescriptions.first.flatMap { MastodonRegisterViewModel.errorPromptAttributedString(for: $0) } +// self.emailErrorPrompt.value = details.emailErrorDescriptions.first.flatMap { MastodonRegisterViewModel.errorPromptAttributedString(for: $0) } +// self.passwordErrorPrompt.value = details.passwordErrorDescriptions.first.flatMap { MastodonRegisterViewModel.errorPromptAttributedString(for: $0) } +// self.reasonErrorPrompt.value = details.reasonErrorDescriptions.first.flatMap { MastodonRegisterViewModel.errorPromptAttributedString(for: $0) } +// } else { +// self.usernameErrorPrompt.value = nil +// self.emailErrorPrompt.value = nil +// self.passwordErrorPrompt.value = nil +// self.reasonErrorPrompt.value = nil +// } +// } +// .store(in: &disposeBag) +// let publisherOne = Publishers.CombineLatest4( - usernameValidateState.eraseToAnyPublisher(), - displayNameValidateState.eraseToAnyPublisher(), - emailValidateState.eraseToAnyPublisher(), - passwordValidateState.eraseToAnyPublisher() + $usernameValidateState, + $displayNameValidateState, + $emailValidateState, + $passwordValidateState ) - .map { $0.0 == .valid && $0.1 == .valid && $0.2 == .valid && $0.3 == .valid } + .map { + $0.0 == .valid && + $0.1 == .valid && + $0.2 == .valid && + $0.3 == .valid + } + + let publisherTwo = $reasonValidateState.map { reasonValidateState -> Bool in + guard self.approvalRequired else { return true } + return reasonValidateState == .valid + } Publishers.CombineLatest( publisherOne, - approvalRequired ? reasonValidateState.map { $0 == .valid }.eraseToAnyPublisher() : Just(true).eraseToAnyPublisher() + publisherTwo ) .map { $0 && $1 } - .assign(to: \.value, on: isAllValid) + .assign(to: \.isAllValid, on: self) .store(in: &disposeBag) } } diff --git a/Mastodon/Scene/Onboarding/ServerRules/Cell/ServerRulesTableViewCell.swift b/Mastodon/Scene/Onboarding/ServerRules/Cell/ServerRulesTableViewCell.swift new file mode 100644 index 00000000..83378b99 --- /dev/null +++ b/Mastodon/Scene/Onboarding/ServerRules/Cell/ServerRulesTableViewCell.swift @@ -0,0 +1,83 @@ +// +// ServerRulesTableViewCell.swift +// Mastodon +// +// Created by MainasuK on 2022-1-5. +// + +import UIKit + +final class ServerRulesTableViewCell: UITableViewCell { + + static let margin: CGFloat = 23 + + let indexImageView: UIImageView = { + let imageView = UIImageView() + imageView.tintColor = Asset.Colors.Label.primary.color + return imageView + }() + + let ruleLabel: UILabel = { + let label = UILabel() + label.font = UIFontMetrics(forTextStyle: .headline).scaledFont(for: .systemFont(ofSize: 17, weight: .semibold)) + label.textColor = Asset.Colors.Label.primary.color + label.numberOfLines = 0 + return label + }() + + let separalerLine: UIView = { + let view = UIView() + view.backgroundColor = Asset.Theme.System.separator.color + return view + }() + + override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { + super.init(style: style, reuseIdentifier: reuseIdentifier) + _init() + } + + required init?(coder: NSCoder) { + super.init(coder: coder) + _init() + } + +} + +extension ServerRulesTableViewCell { + + private func _init() { + selectionStyle = .none + backgroundColor = .clear + + indexImageView.translatesAutoresizingMaskIntoConstraints = false + contentView.addSubview(indexImageView) + NSLayoutConstraint.activate([ + indexImageView.topAnchor.constraint(greaterThanOrEqualTo: contentView.topAnchor, constant: ServerRulesTableViewCell.margin), + indexImageView.leadingAnchor.constraint(equalTo: contentView.readableContentGuide.leadingAnchor), + contentView.bottomAnchor.constraint(greaterThanOrEqualTo: indexImageView.bottomAnchor, constant: ServerRulesTableViewCell.margin), + indexImageView.centerYAnchor.constraint(equalTo: contentView.centerYAnchor), + indexImageView.widthAnchor.constraint(equalToConstant: 32).priority(.required - 1), + indexImageView.heightAnchor.constraint(equalToConstant: 32).priority(.required - 1), + ]) + + ruleLabel.translatesAutoresizingMaskIntoConstraints = false + contentView.addSubview(ruleLabel) + NSLayoutConstraint.activate([ + ruleLabel.topAnchor.constraint(greaterThanOrEqualTo: contentView.topAnchor, constant: ServerRulesTableViewCell.margin), + ruleLabel.leadingAnchor.constraint(equalTo: indexImageView.trailingAnchor, constant: 16), + ruleLabel.trailingAnchor.constraint(equalTo: contentView.readableContentGuide.trailingAnchor), + contentView.bottomAnchor.constraint(greaterThanOrEqualTo: ruleLabel.bottomAnchor, constant: ServerRulesTableViewCell.margin), + ruleLabel.centerYAnchor.constraint(equalTo: contentView.centerYAnchor), + ]) + + separalerLine.translatesAutoresizingMaskIntoConstraints = false + contentView.addSubview(separalerLine) + NSLayoutConstraint.activate([ + separalerLine.leadingAnchor.constraint(equalTo: contentView.readableContentGuide.leadingAnchor), + separalerLine.trailingAnchor.constraint(equalTo: contentView.readableContentGuide.trailingAnchor), + separalerLine.bottomAnchor.constraint(equalTo: contentView.bottomAnchor), + separalerLine.heightAnchor.constraint(equalToConstant: UIView.separatorLineHeight(of: contentView)).priority(.required - 1), + ]) + } + +} diff --git a/Mastodon/Scene/Onboarding/ServerRules/MastodonServerRulesViewController.swift b/Mastodon/Scene/Onboarding/ServerRules/MastodonServerRulesViewController.swift index e93d06e1..f6369282 100644 --- a/Mastodon/Scene/Onboarding/ServerRules/MastodonServerRulesViewController.swift +++ b/Mastodon/Scene/Onboarding/ServerRules/MastodonServerRulesViewController.swift @@ -14,8 +14,11 @@ import MetaTextKit final class MastodonServerRulesViewController: UIViewController, NeedsDependency { - var disposeBag = Set() + let logger = Logger(subsystem: "MastodonServerRulesViewController", category: "ViewController") + var disposeBag = Set() + private var observations = Set() + weak var context: AppContext! { willSet { precondition(!isViewLoaded) } } weak var coordinator: SceneCoordinator! { willSet { precondition(!isViewLoaded) } } @@ -23,67 +26,26 @@ final class MastodonServerRulesViewController: UIViewController, NeedsDependency let stackView = UIStackView() - let largeTitleLabel: UILabel = { - let label = UILabel() - label.font = UIFontMetrics(forTextStyle: .largeTitle).scaledFont(for: .systemFont(ofSize: 34, weight: .bold)) - label.textColor = .label - label.text = L10n.Scene.ServerRules.title - label.numberOfLines = 0 - return label + let tableView: UITableView = { + let tableView = UITableView() + tableView.register(OnboardingHeadlineTableViewCell.self, forCellReuseIdentifier: String(describing: OnboardingHeadlineTableViewCell.self)) + tableView.register(ServerRulesTableViewCell.self, forCellReuseIdentifier: String(describing: ServerRulesTableViewCell.self)) + tableView.rowHeight = UITableView.automaticDimension + tableView.separatorStyle = .none + tableView.backgroundColor = .clear + tableView.keyboardDismissMode = .onDrag + if #available(iOS 15.0, *) { + tableView.sectionHeaderTopPadding = 0 + } else { + // Fallback on earlier versions + } + return tableView }() - - private(set) lazy var subtitleLabel: UILabel = { - let label = UILabel() - label.font = UIFontMetrics(forTextStyle: .title1).scaledFont(for: UIFont.systemFont(ofSize: 20)) - label.textColor = .secondaryLabel - label.text = L10n.Scene.ServerRules.subtitle(viewModel.domain) - label.numberOfLines = 0 - return label - }() - - let rulesLabel: UILabel = { - let label = UILabel() - label.font = UIFontMetrics(forTextStyle: .body).scaledFont(for: .systemFont(ofSize: 17, weight: .semibold)) - label.textColor = Asset.Colors.Label.primary.color - label.text = "Rules" - label.numberOfLines = 0 - return label - }() - - let bottomContainerView: UIView = { - let view = UIView() - view.backgroundColor = Asset.Theme.Mastodon.systemGroupedBackground.color - return view - }() - - private(set) lazy var bottomPromptMetaText: MetaText = { - let metaText = MetaText() - metaText.textAttributes = [ - .font: UIFontMetrics(forTextStyle: .body).scaledFont(for: .systemFont(ofSize: 17, weight: .regular), maximumPointSize: 22), - .foregroundColor: UIColor.label, - ] - metaText.linkAttributes = [ - .font: UIFontMetrics(forTextStyle: .body).scaledFont(for: .systemFont(ofSize: 17, weight: .regular), maximumPointSize: 22), - .foregroundColor: Asset.Colors.brandBlue.color, - ] - metaText.textView.isEditable = false - metaText.textView.isSelectable = false - metaText.textView.isScrollEnabled = false - metaText.textView.backgroundColor = Asset.Theme.Mastodon.systemGroupedBackground.color // needs background color to prevent server rules text overlap - return metaText - }() - - let confirmButton: PrimaryActionButton = { - let button = PrimaryActionButton() - button.setTitle(L10n.Scene.ServerRules.Button.confirm, for: .normal) - return button - }() - - let scrollView: UIScrollView = { - let scrollView = UIScrollView() - scrollView.alwaysBounceVertical = true - scrollView.showsVerticalScrollIndicator = false - return scrollView + + let navigationActionView: NavigationActionView = { + let navigationActionView = NavigationActionView() + navigationActionView.backgroundColor = Asset.Scene.Onboarding.onboardingBackground.color + return navigationActionView }() deinit { @@ -97,224 +59,96 @@ extension MastodonServerRulesViewController { override func viewDidLoad() { super.viewDidLoad() + navigationItem.leftBarButtonItem = UIBarButtonItem() + setupOnboardingAppearance() - configureTitleLabel() - configureMargin() - configTextView() - defer { setupNavigationBarBackgroundView() } - bottomContainerView.translatesAutoresizingMaskIntoConstraints = false - view.addSubview(bottomContainerView) + tableView.translatesAutoresizingMaskIntoConstraints = false + view.addSubview(tableView) NSLayoutConstraint.activate([ - view.bottomAnchor.constraint(equalTo: bottomContainerView.bottomAnchor), - bottomContainerView.leadingAnchor.constraint(equalTo: view.leadingAnchor), - bottomContainerView.trailingAnchor.constraint(equalTo: view.trailingAnchor), + tableView.topAnchor.constraint(equalTo: view.topAnchor), + tableView.leadingAnchor.constraint(equalTo: view.leadingAnchor), + tableView.trailingAnchor.constraint(equalTo: view.trailingAnchor), + tableView.bottomAnchor.constraint(equalTo: view.bottomAnchor), ]) - bottomContainerView.preservesSuperviewLayoutMargins = true + + navigationActionView.translatesAutoresizingMaskIntoConstraints = false + view.addSubview(navigationActionView) defer { - view.bringSubviewToFront(bottomContainerView) + view.bringSubviewToFront(navigationActionView) } - - confirmButton.translatesAutoresizingMaskIntoConstraints = false - bottomContainerView.addSubview(confirmButton) NSLayoutConstraint.activate([ - bottomContainerView.layoutMarginsGuide.bottomAnchor.constraint(equalTo: confirmButton.bottomAnchor, constant: MastodonServerRulesViewController.viewBottomPaddingHeight), - confirmButton.leadingAnchor.constraint(equalTo: bottomContainerView.layoutMarginsGuide.leadingAnchor), - bottomContainerView.layoutMarginsGuide.trailingAnchor.constraint(equalTo: confirmButton.trailingAnchor), - confirmButton.heightAnchor.constraint(equalToConstant: MastodonServerRulesViewController.actionButtonHeight).priority(.defaultHigh), + navigationActionView.leadingAnchor.constraint(equalTo: view.leadingAnchor), + navigationActionView.trailingAnchor.constraint(equalTo: view.trailingAnchor), + view.bottomAnchor.constraint(equalTo: navigationActionView.bottomAnchor), ]) - bottomPromptMetaText.textView.translatesAutoresizingMaskIntoConstraints = false - bottomContainerView.addSubview(bottomPromptMetaText.textView) - NSLayoutConstraint.activate([ - bottomPromptMetaText.textView.frameLayoutGuide.topAnchor.constraint(equalTo: bottomContainerView.topAnchor, constant: 20), - bottomPromptMetaText.textView.frameLayoutGuide.leadingAnchor.constraint(equalTo: bottomContainerView.layoutMarginsGuide.leadingAnchor), - bottomPromptMetaText.textView.frameLayoutGuide.trailingAnchor.constraint(equalTo: bottomContainerView.layoutMarginsGuide.trailingAnchor), - confirmButton.topAnchor.constraint(equalTo: bottomPromptMetaText.textView.frameLayoutGuide.bottomAnchor, constant: 20), - ]) + navigationActionView + .observe(\.bounds, options: [.initial, .new]) { [weak self] navigationActionView, _ in + guard let self = self else { return } + let inset = navigationActionView.frame.height + self.tableView.contentInset.bottom = inset + } + .store(in: &observations) - scrollView.translatesAutoresizingMaskIntoConstraints = false - view.addSubview(scrollView) - NSLayoutConstraint.activate([ - scrollView.frameLayoutGuide.topAnchor.constraint(equalTo: view.topAnchor), - scrollView.frameLayoutGuide.leadingAnchor.constraint(equalTo: view.readableContentGuide.leadingAnchor), - scrollView.frameLayoutGuide.trailingAnchor.constraint(equalTo: view.readableContentGuide.trailingAnchor), - scrollView.frameLayoutGuide.bottomAnchor.constraint(equalTo: view.bottomAnchor), - scrollView.frameLayoutGuide.widthAnchor.constraint(equalTo: scrollView.contentLayoutGuide.widthAnchor), - ]) - - stackView.axis = .vertical - stackView.distribution = .fill - stackView.spacing = 10 - stackView.isLayoutMarginsRelativeArrangement = true - stackView.layoutMargins = UIEdgeInsets(top: 20, left: 0, bottom: 20, right: 0) - stackView.addArrangedSubview(largeTitleLabel) - stackView.addArrangedSubview(subtitleLabel) - stackView.addArrangedSubview(rulesLabel) + tableView.delegate = self + viewModel.setupDiffableDataSource(tableView: tableView) - stackView.translatesAutoresizingMaskIntoConstraints = false - scrollView.addSubview(stackView) - NSLayoutConstraint.activate([ - stackView.topAnchor.constraint(equalTo: scrollView.contentLayoutGuide.topAnchor), - stackView.leadingAnchor.constraint(equalTo: scrollView.contentLayoutGuide.leadingAnchor), - stackView.trailingAnchor.constraint(equalTo: scrollView.contentLayoutGuide.trailingAnchor), - scrollView.contentLayoutGuide.bottomAnchor.constraint(equalTo: stackView.bottomAnchor), - ]) - - rulesLabel.attributedText = viewModel.rulesAttributedString - confirmButton.addTarget(self, action: #selector(MastodonServerRulesViewController.confirmButtonPressed(_:)), for: .touchUpInside) + navigationActionView.backButton.addTarget(self, action: #selector(MastodonServerRulesViewController.backButtonPressed(_:)), for: .touchUpInside) + navigationActionView.nextButton.addTarget(self, action: #selector(MastodonServerRulesViewController.nextButtonPressed(_:)), for: .touchUpInside) } override func viewDidAppear(_ animated: Bool) { super.viewDidAppear(animated) - scrollView.flashScrollIndicators() - } - - override func viewDidLayoutSubviews() { - super.viewDidLayoutSubviews() - updateScrollViewContentInset() - } - - override func viewSafeAreaInsetsDidChange() { - super.viewSafeAreaInsetsDidChange() - updateScrollViewContentInset() - } - - override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) { - super.traitCollectionDidChange(previousTraitCollection) - - setupNavigationBarAppearance() - configureTitleLabel() - configureMargin() + tableView.flashScrollIndicators() } } extension MastodonServerRulesViewController { - private func configureTitleLabel() { - guard UIDevice.current.userInterfaceIdiom == .pad else { - return - } - - switch traitCollection.horizontalSizeClass { - case .regular: - navigationItem.largeTitleDisplayMode = .always - navigationItem.title = L10n.Scene.ServerRules.title.replacingOccurrences(of: "\n", with: " ") - largeTitleLabel.isHidden = true - default: - navigationItem.leftBarButtonItem = nil - navigationItem.largeTitleDisplayMode = .never - navigationItem.title = nil - largeTitleLabel.isHidden = false - } + + @objc private func backButtonPressed(_ sender: UIButton) { + logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public)") + navigationController?.popViewController(animated: true) } - private func configureMargin() { - switch traitCollection.horizontalSizeClass { - case .regular: - let margin = MastodonPickServerViewController.viewEdgeMargin - stackView.layoutMargins = UIEdgeInsets(top: 32, left: margin, bottom: 20, right: margin) - bottomContainerView.layoutMargins = UIEdgeInsets(top: 0, left: margin, bottom: 0, right: margin) - default: - stackView.layoutMargins = UIEdgeInsets(top: 20, left: 0, bottom: 20, right: 0) - bottomContainerView.layoutMargins = .zero - } - } -} + @objc private func nextButtonPressed(_ sender: UIButton) { + logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public)") -extension MastodonServerRulesViewController { - func updateScrollViewContentInset() { - view.layoutIfNeeded() - scrollView.contentInset.bottom = bottomContainerView.frame.height - scrollView.verticalScrollIndicatorInsets.bottom = bottomContainerView.frame.height + let viewModel = MastodonRegisterViewModel( + context: context, + domain: viewModel.domain, + authenticateInfo: viewModel.authenticateInfo, + instance: viewModel.instance, + applicationToken: viewModel.applicationToken + ) + coordinator.present(scene: .mastodonRegister(viewModel: viewModel), from: self, transition: .show) } - func configTextView() { - let metaContent = ServerRulesPromptMetaContent(domain: viewModel.domain) - bottomPromptMetaText.configure(content: metaContent) - bottomPromptMetaText.textView.linkDelegate = self - } - - struct ServerRulesPromptMetaContent: MetaContent { - let string: String - let entities: [Meta.Entity] - - init(domain: String) { - let _string = L10n.Scene.ServerRules.prompt(domain) - self.string = _string - - var _entities: [Meta.Entity] = [] - - let termsOfServiceText = L10n.Scene.ServerRules.termsOfService - if let termsOfServiceRange = _string.range(of: termsOfServiceText) { - let url = Mastodon.API.serverRulesURL(domain: domain) - let entity = Meta.Entity(range: NSRange(termsOfServiceRange, in: _string), meta: .url(termsOfServiceText, trimmed: termsOfServiceText, url: url.absoluteString, userInfo: nil)) - _entities.append(entity) - } - - let privacyPolicyText = L10n.Scene.ServerRules.privacyPolicy - if let privacyPolicyRange = _string.range(of: privacyPolicyText) { - let url = Mastodon.API.privacyURL(domain: domain) - let entity = Meta.Entity(range: NSRange(privacyPolicyRange, in: _string), meta: .url(privacyPolicyText, trimmed: privacyPolicyText, url: url.absoluteString, userInfo: nil)) - _entities.append(entity) - } - - self.entities = _entities - } - - func metaAttachment(for entity: Meta.Entity) -> MetaAttachment? { - return nil - } - } - -} - -extension MastodonServerRulesViewController: UITextViewDelegate { - func textView(_ textView: UITextView, shouldInteractWith URL: URL, in characterRange: NSRange, interaction: UITextItemInteraction) -> Bool { - return false - } -} - -// MARK: - MetaTextViewDelegate -extension MastodonServerRulesViewController: MetaTextViewDelegate { - func metaTextView(_ metaTextView: MetaTextView, didSelectMeta meta: Meta) { - switch meta { - case .url(_, _, let url, _): - guard let url = URL(string: url) else { return } - coordinator.present(scene: .safari(url: url), from: nil, transition: .safariPresent(animated: true, completion: nil)) - default: - break - } - } -} - -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 viewModel = MastodonRegisterViewModel(domain: self.viewModel.domain, context: self.context, authenticateInfo: self.viewModel.authenticateInfo, instance: self.viewModel.instance, applicationToken: self.viewModel.applicationToken) - self.coordinator.present(scene: .mastodonRegister(viewModel: viewModel), from: self, transition: .show) - } } // MARK: - OnboardingViewControllerAppearance extension MastodonServerRulesViewController: OnboardingViewControllerAppearance { } -#if canImport(SwiftUI) && DEBUG -import SwiftUI - -struct ServerRulesViewController_Previews: PreviewProvider { - - static var previews: some View { - UIViewControllerPreview { - let viewController = MastodonServerRulesViewController() - return viewController - } - .previewLayout(.fixed(width: 375, height: 800)) +// MARK: - UITableViewDelegate +extension MastodonServerRulesViewController: UITableViewDelegate { + func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? { + return UIView() } + func tableView(_ tableView: UITableView, heightForHeaderInSection section: Int) -> CGFloat { + guard let diffableDataSource = viewModel.diffableDataSource, + section < diffableDataSource.snapshot().numberOfSections + else { return .leastNonzeroMagnitude } + + let sectionItem = diffableDataSource.snapshot().sectionIdentifiers[section] + switch sectionItem { + case .header: + return .leastNonzeroMagnitude + case .rules: + return 16 + } + } } - -#endif diff --git a/Mastodon/Scene/Onboarding/ServerRules/MastodonServerRulesViewModel+Diffable.swift b/Mastodon/Scene/Onboarding/ServerRules/MastodonServerRulesViewModel+Diffable.swift new file mode 100644 index 00000000..f6385a52 --- /dev/null +++ b/Mastodon/Scene/Onboarding/ServerRules/MastodonServerRulesViewModel+Diffable.swift @@ -0,0 +1,26 @@ +// +// MastodonServerRulesViewModel+Diffable.swift +// Mastodon +// +// Created by MainasuK on 2022-1-5. +// + +import UIKit + +extension MastodonServerRulesViewModel { + func setupDiffableDataSource( + tableView: UITableView + ) { + diffableDataSource = ServerRuleSection.tableViewDiffableDataSource(tableView: tableView) + + var snapshot = NSDiffableDataSourceSnapshot() + snapshot.appendSections([.header, .rules]) + snapshot.appendItems([.header(domain: domain)], toSection: .header) + let ruleItems: [ServerRuleItem] = rules.enumerated().map { i, rule in + let ruleContext = ServerRuleItem.RuleContext(index: i, rule: rule) + return ServerRuleItem.rule(ruleContext) + } + snapshot.appendItems(ruleItems, toSection: .rules) + diffableDataSource?.applySnapshot(snapshot, animated: false, completion: nil) + } +} diff --git a/Mastodon/Scene/Onboarding/ServerRules/MastodonServerRulesViewModel.swift b/Mastodon/Scene/Onboarding/ServerRules/MastodonServerRulesViewModel.swift index 5936a2c0..f2664e0e 100644 --- a/Mastodon/Scene/Onboarding/ServerRules/MastodonServerRulesViewModel.swift +++ b/Mastodon/Scene/Onboarding/ServerRules/MastodonServerRulesViewModel.swift @@ -18,6 +18,9 @@ final class MastodonServerRulesViewModel { let instance: Mastodon.Entity.Instance let applicationToken: Mastodon.Entity.Token + // output + var diffableDataSource: UITableViewDiffableDataSource? + init( domain: String, authenticateInfo: AuthenticationViewModel.AuthenticateInfo, diff --git a/Mastodon/Scene/Onboarding/Share/NavigationActionView.swift b/Mastodon/Scene/Onboarding/Share/NavigationActionView.swift new file mode 100644 index 00000000..dc30227c --- /dev/null +++ b/Mastodon/Scene/Onboarding/Share/NavigationActionView.swift @@ -0,0 +1,93 @@ +// +// NavigationActionView.swift +// Mastodon +// +// Created by MainasuK on 2021-12-31. +// + +import UIKit +import MastodonUI + +final class NavigationActionView: UIView { + + static let buttonHeight: CGFloat = 50 + + private var observations = Set() + + let buttonContainer: UIStackView = { + let stackView = UIStackView() + stackView.axis = .horizontal + stackView.spacing = 18 + return stackView + }() + + let backButtonShadowContainer = ShadowBackgroundContainer() + let backButton: PrimaryActionButton = { + let button = PrimaryActionButton() + button.action = .back + button.setTitle(L10n.Common.Controls.Actions.back, for: .normal) + return button + }() + + let nextButtonShadowContainer = ShadowBackgroundContainer() + let nextButton: PrimaryActionButton = { + let button = PrimaryActionButton() + button.action = .next + button.setTitle(L10n.Common.Controls.Actions.next, for: .normal) + return button + }() + + override init(frame: CGRect) { + super.init(frame: frame) + _init() + } + + required init?(coder: NSCoder) { + super.init(coder: coder) + _init() + } + +} + +extension NavigationActionView { + private func _init() { + buttonContainer.translatesAutoresizingMaskIntoConstraints = false + buttonContainer.preservesSuperviewLayoutMargins = true + addSubview(buttonContainer) + NSLayoutConstraint.activate([ + buttonContainer.topAnchor.constraint(equalTo: topAnchor, constant: 16), + buttonContainer.leadingAnchor.constraint(equalTo: readableContentGuide.leadingAnchor), + buttonContainer.trailingAnchor.constraint(equalTo: readableContentGuide.trailingAnchor), + safeAreaLayoutGuide.bottomAnchor.constraint(equalTo: buttonContainer.bottomAnchor, constant: 8), + ]) + + backButtonShadowContainer.translatesAutoresizingMaskIntoConstraints = false + buttonContainer.addArrangedSubview(backButtonShadowContainer) + nextButtonShadowContainer.translatesAutoresizingMaskIntoConstraints = false + buttonContainer.addArrangedSubview(nextButtonShadowContainer) + NSLayoutConstraint.activate([ + backButtonShadowContainer.heightAnchor.constraint(equalToConstant: NavigationActionView.buttonHeight).priority(.required - 1), + nextButtonShadowContainer.heightAnchor.constraint(equalToConstant: NavigationActionView.buttonHeight).priority(.required - 1), + nextButtonShadowContainer.widthAnchor.constraint(equalTo: backButtonShadowContainer.widthAnchor, multiplier: 2).priority(.required - 1), + ]) + + backButton.translatesAutoresizingMaskIntoConstraints = false + backButtonShadowContainer.addSubview(backButton) + NSLayoutConstraint.activate([ + backButton.topAnchor.constraint(equalTo: backButtonShadowContainer.topAnchor), + backButton.leadingAnchor.constraint(equalTo: backButtonShadowContainer.leadingAnchor), + backButton.trailingAnchor.constraint(equalTo: backButtonShadowContainer.trailingAnchor), + backButton.bottomAnchor.constraint(equalTo: backButtonShadowContainer.bottomAnchor), + ]) + + nextButton.translatesAutoresizingMaskIntoConstraints = false + nextButtonShadowContainer.addSubview(nextButton) + NSLayoutConstraint.activate([ + nextButton.topAnchor.constraint(equalTo: nextButtonShadowContainer.topAnchor), + nextButton.leadingAnchor.constraint(equalTo: nextButtonShadowContainer.leadingAnchor), + nextButton.trailingAnchor.constraint(equalTo: nextButtonShadowContainer.trailingAnchor), + nextButton.bottomAnchor.constraint(equalTo: nextButtonShadowContainer.bottomAnchor), + ]) + } + +} diff --git a/Mastodon/Scene/Onboarding/Share/OnboardingHeadlineTableViewCell.swift b/Mastodon/Scene/Onboarding/Share/OnboardingHeadlineTableViewCell.swift new file mode 100644 index 00000000..f8090734 --- /dev/null +++ b/Mastodon/Scene/Onboarding/Share/OnboardingHeadlineTableViewCell.swift @@ -0,0 +1,65 @@ +// +// OnboardingHeadlineTableViewCell.swift +// Mastodon +// +// Created by BradGao on 2021/2/23. +// + +import UIKit + +final class OnboardingHeadlineTableViewCell: UITableViewCell { + + let titleLabel: UILabel = { + let label = UILabel() + label.font = MastodonPickServerViewController.largeTitleFont + label.textColor = MastodonPickServerViewController.largeTitleTextColor + label.text = L10n.Scene.ServerPicker.title + label.adjustsFontForContentSizeCategory = true + label.numberOfLines = 0 + return label + }() + + let subTitleLabel: UILabel = { + let label = UILabel() + label.font = MastodonPickServerViewController.subTitleFont + label.textColor = MastodonPickServerViewController.subTitleTextColor + label.text = "Pick a community based on your interests, region, or a general purpose one. Each community is operated by an entirely independent organization or individual." + label.adjustsFontForContentSizeCategory = true + label.numberOfLines = 0 + return label + }() + + override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { + super.init(style: style, reuseIdentifier: reuseIdentifier) + _init() + } + + required init?(coder: NSCoder) { + super.init(coder: coder) + _init() + } +} + +extension OnboardingHeadlineTableViewCell { + + private func _init() { + selectionStyle = .none + backgroundColor = Asset.Scene.Onboarding.onboardingBackground.color + + let container = UIStackView() + container.axis = .vertical + container.spacing = 16 + container.translatesAutoresizingMaskIntoConstraints = false + contentView.addSubview(container) + NSLayoutConstraint.activate([ + container.topAnchor.constraint(equalTo: contentView.topAnchor), + container.leadingAnchor.constraint(equalTo: contentView.readableContentGuide.leadingAnchor), + container.trailingAnchor.constraint(equalTo: contentView.readableContentGuide.trailingAnchor), + contentView.bottomAnchor.constraint(equalTo: container.bottomAnchor, constant: 11), + ]) + + container.addArrangedSubview(titleLabel) + container.addArrangedSubview(subTitleLabel) + } + +} diff --git a/Mastodon/Scene/Onboarding/Share/OnboardingNavigationController.swift b/Mastodon/Scene/Onboarding/Share/OnboardingNavigationController.swift new file mode 100644 index 00000000..537102dc --- /dev/null +++ b/Mastodon/Scene/Onboarding/Share/OnboardingNavigationController.swift @@ -0,0 +1,51 @@ +// +// OnboardingNavigationController.swift +// Mastodon +// +// Created by MainasuK on 2021-12-31. +// + +import UIKit + +final class OnboardingNavigationController: AdaptiveStatusBarStyleNavigationController { + + private(set) lazy var gradientBorderView = GradientBorderView(frame: view.bounds) + +} + +extension OnboardingNavigationController { + + override func viewDidLoad() { + super.viewDidLoad() + + gradientBorderView.translatesAutoresizingMaskIntoConstraints = false + view.addSubview(gradientBorderView) + NSLayoutConstraint.activate([ + gradientBorderView.topAnchor.constraint(equalTo: view.topAnchor), + gradientBorderView.leadingAnchor.constraint(equalTo: view.leadingAnchor), + gradientBorderView.trailingAnchor.constraint(equalTo: view.trailingAnchor), + gradientBorderView.bottomAnchor.constraint(equalTo: view.bottomAnchor), + ]) + + updateBorderViewDisplay() + } + + override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) { + super.traitCollectionDidChange(previousTraitCollection) + + } + +} + +extension OnboardingNavigationController { + + private func updateBorderViewDisplay() { + switch traitCollection.userInterfaceIdiom { + case .phone: + gradientBorderView.isHidden = true + default: + gradientBorderView.isHidden = false + } + } + +} diff --git a/Mastodon/Scene/Onboarding/Share/OnboardingViewControllerAppearance.swift b/Mastodon/Scene/Onboarding/Share/OnboardingViewControllerAppearance.swift index 17c4699e..aef6a8ab 100644 --- a/Mastodon/Scene/Onboarding/Share/OnboardingViewControllerAppearance.swift +++ b/Mastodon/Scene/Onboarding/Share/OnboardingViewControllerAppearance.swift @@ -15,12 +15,30 @@ protocol OnboardingViewControllerAppearance: UIViewController { extension OnboardingViewControllerAppearance { - static var actionButtonHeight: CGFloat { return 46 } + static var actionButtonHeight: CGFloat { return 50 } static var actionButtonMargin: CGFloat { return 12 } + static var actionButtonMarginExtend: CGFloat { return 80 } static var viewBottomPaddingHeight: CGFloat { return 11 } + static var viewBottomPaddingHeightExtend: CGFloat { return 22 } + + static var largeTitleFont: UIFont { + return UIFontMetrics(forTextStyle: .largeTitle).scaledFont(for: .systemFont(ofSize: 28, weight: .bold)) + } + + static var largeTitleTextColor: UIColor { + return Asset.Colors.Label.primary.color + } + + static var subTitleFont: UIFont { + return UIFontMetrics(forTextStyle: .largeTitle).scaledFont(for: .systemFont(ofSize: 17, weight: .regular)) + } + + static var subTitleTextColor: UIColor { + return Asset.Colors.Label.secondary.color + } func setupOnboardingAppearance() { - view.backgroundColor = Asset.Theme.Mastodon.systemGroupedBackground.color + view.backgroundColor = Asset.Scene.Onboarding.onboardingBackground.color setupNavigationBarAppearance() @@ -37,31 +55,22 @@ extension OnboardingViewControllerAppearance { // use TransparentBackground so view push / dismiss will be more visual nature // please add opaque background for status bar manually if needs - switch traitCollection.userInterfaceIdiom { - case .pad: - if traitCollection.horizontalSizeClass == .regular { - // do nothing - } else { - fallthrough - } - default: - let barAppearance = UINavigationBarAppearance() - barAppearance.configureWithTransparentBackground() - navigationItem.standardAppearance = barAppearance - navigationItem.compactAppearance = barAppearance - navigationItem.scrollEdgeAppearance = barAppearance - if #available(iOS 15.0, *) { - navigationItem.compactScrollEdgeAppearance = barAppearance - } else { - // Fallback on earlier versions - } + let barAppearance = UINavigationBarAppearance() + barAppearance.configureWithTransparentBackground() + navigationItem.standardAppearance = barAppearance + navigationItem.compactAppearance = barAppearance + navigationItem.scrollEdgeAppearance = barAppearance + if #available(iOS 15.0, *) { + navigationItem.compactScrollEdgeAppearance = barAppearance + } else { + // Fallback on earlier versions } } func setupNavigationBarBackgroundView() { let navigationBarBackgroundView: UIView = { let view = UIView() - view.backgroundColor = Asset.Theme.Mastodon.systemGroupedBackground.color + view.backgroundColor = Asset.Scene.Onboarding.onboardingBackground.color return view }() diff --git a/Mastodon/Scene/Onboarding/Welcome/View/GradientBorderView.swift b/Mastodon/Scene/Onboarding/Welcome/View/GradientBorderView.swift new file mode 100644 index 00000000..68e7968b --- /dev/null +++ b/Mastodon/Scene/Onboarding/Welcome/View/GradientBorderView.swift @@ -0,0 +1,63 @@ +// +// GradientBorderView.swift +// Mastodon +// +// Created by MainasuK on 2021-12-31. +// + +import UIKit + +final class GradientBorderView: UIView { + + let gradientLayer = CAGradientLayer() + let maskLayer = CAShapeLayer() + + var cornerRadius: CGFloat = 9 { + didSet { setNeedsLayout() } + } + + override init(frame: CGRect) { + super.init(frame: frame) + _init() + } + + required init?(coder: NSCoder) { + super.init(coder: coder) + _init() + } + +} + +extension GradientBorderView { + private func _init() { + isUserInteractionEnabled = false + + gradientLayer.frame = bounds + + gradientLayer.colors = [ + UIColor.white.cgColor, + UIColor.white.withAlphaComponent(0.0).cgColor, + ] + + gradientLayer.startPoint = CGPoint(x: 0.5, y: 0) + gradientLayer.endPoint = CGPoint(x: 0.5, y: 1) + + layer.addSublayer(gradientLayer) + + // set blend mode to "Soft Light" + layer.compositingFilter = "softLightBlendMode" + } + + override func layoutSubviews() { + super.layoutSubviews() + + let bezierPath = UIBezierPath(rect: bounds) + bezierPath.append(UIBezierPath(roundedRect: bounds.insetBy(dx: 3, dy: 3), cornerRadius: cornerRadius)) + + maskLayer.fillRule = .evenOdd + maskLayer.path = bezierPath.cgPath + + gradientLayer.frame = bounds + gradientLayer.mask = maskLayer + } +} diff --git a/Mastodon/Scene/Onboarding/Welcome/View/WelcomeIllustrationView.swift b/Mastodon/Scene/Onboarding/Welcome/View/WelcomeIllustrationView.swift index f5d8c41c..23fa1505 100644 --- a/Mastodon/Scene/Onboarding/Welcome/View/WelcomeIllustrationView.swift +++ b/Mastodon/Scene/Onboarding/Welcome/View/WelcomeIllustrationView.swift @@ -8,18 +8,18 @@ import UIKit final class WelcomeIllustrationView: UIView { - - static let artworkImageSize = CGSize(width: 375, height: 1500) - + let cloudBaseImageView = UIImageView() let rightHillImageView = UIImageView() let leftHillImageView = UIImageView() let centerHillImageView = UIImageView() private let cloudBaseImage = Asset.Scene.Welcome.Illustration.cloudBase.image + private let cloudBaseExtendImage = Asset.Scene.Welcome.Illustration.cloudBaseExtend.image private let elephantThreeOnGrassWithTreeTwoImage = Asset.Scene.Welcome.Illustration.elephantThreeOnGrassWithTreeTwo.image private let elephantThreeOnGrassWithTreeThreeImage = Asset.Scene.Welcome.Illustration.elephantThreeOnGrassWithTreeThree.image private let elephantThreeOnGrassImage = Asset.Scene.Welcome.Illustration.elephantThreeOnGrass.image + private let elephantThreeOnGrassExtendImage = Asset.Scene.Welcome.Illustration.elephantThreeOnGrassExtend.image // layout outside let elephantOnAirplaneWithContrailImageView: UIImageView = { @@ -27,6 +27,13 @@ final class WelcomeIllustrationView: UIView { imageView.contentMode = .scaleAspectFill return imageView }() + + var layout: Layout = .compact { + didSet { + setNeedsLayout() + } + } + var aspectLayoutConstraint: NSLayoutConstraint! override init(frame: CGRect) { super.init(frame: frame) @@ -40,6 +47,20 @@ final class WelcomeIllustrationView: UIView { } +extension WelcomeIllustrationView { + enum Layout { + case compact + case regular + + var artworkImageSize: CGSize { + switch self { + case .compact: return CGSize(width: 375, height: 1500) + case .regular: return CGSize(width: 547, height: 3000) + } + } + } +} + extension WelcomeIllustrationView { private func _init() { @@ -62,7 +83,6 @@ extension WelcomeIllustrationView { cloudBaseImageView.leadingAnchor.constraint(equalTo: leadingAnchor), cloudBaseImageView.trailingAnchor.constraint(equalTo: trailingAnchor), cloudBaseImageView.bottomAnchor.constraint(equalTo: bottomAnchor), - cloudBaseImageView.widthAnchor.constraint(equalTo: cloudBaseImageView.heightAnchor, multiplier: WelcomeIllustrationView.artworkImageSize.width / WelcomeIllustrationView.artworkImageSize.height), ]) [ @@ -79,15 +99,28 @@ extension WelcomeIllustrationView { imageView.bottomAnchor.constraint(equalTo: cloudBaseImageView.bottomAnchor), ]) } + + aspectLayoutConstraint = cloudBaseImageView.widthAnchor.constraint(equalTo: cloudBaseImageView.heightAnchor, multiplier: layout.artworkImageSize.width / layout.artworkImageSize.height) + aspectLayoutConstraint.isActive = true } override func layoutSubviews() { super.layoutSubviews() - updateImage() + + switch layout { + case .compact: + layoutCompact() + case .regular: + layoutRegular() + } + + aspectLayoutConstraint.isActive = false + aspectLayoutConstraint = cloudBaseImageView.widthAnchor.constraint(equalTo: cloudBaseImageView.heightAnchor, multiplier: layout.artworkImageSize.width / layout.artworkImageSize.height) + aspectLayoutConstraint.isActive = true } - private func updateImage() { - let size = WelcomeIllustrationView.artworkImageSize + private func layoutCompact() { + let size = layout.artworkImageSize let width = size.width let height = size.height @@ -130,6 +163,50 @@ extension WelcomeIllustrationView { } } + private func layoutRegular() { + let size = layout.artworkImageSize + let width = size.width + let height = size.height + + cloudBaseImageView.image = UIGraphicsImageRenderer(size: size).image { context in + // clear background + UIColor.clear.setFill() + context.fill(CGRect(origin: .zero, size: size)) + + // draw cloud + cloudBaseExtendImage.draw(at: CGPoint(x: 0, y: height - cloudBaseExtendImage.size.height)) + + rightHillImageView.image = UIGraphicsImageRenderer(size: size).image { context in + // clear background + UIColor.clear.setFill() + context.fill(CGRect(origin: .zero, size: size)) + + // draw elephantThreeOnGrassWithTreeTwoImage + // elephantThreeOnGrassWithTreeTwo.bottomY - 25 align to elephantThreeOnGrassImage.centerY + elephantThreeOnGrassWithTreeTwoImage.draw(at: CGPoint(x: width - elephantThreeOnGrassWithTreeTwoImage.size.width, y: height - 0.5 * elephantThreeOnGrassImage.size.height - elephantThreeOnGrassWithTreeTwoImage.size.height - 20)) + } + + leftHillImageView.image = UIGraphicsImageRenderer(size: size).image { context in + // clear background + UIColor.clear.setFill() + context.fill(CGRect(origin: .zero, size: size)) + + // draw elephantThreeOnGrassWithTreeThree + // elephantThreeOnGrassWithTreeThree.bottomY + 30 align to elephantThreeOnGrassImage.centerY + elephantThreeOnGrassWithTreeThreeImage.draw(at: CGPoint(x: -160, y: height - 0.5 * elephantThreeOnGrassImage.size.height - elephantThreeOnGrassWithTreeThreeImage.size.height - 80)) + } + + centerHillImageView.image = UIGraphicsImageRenderer(size: size).image { context in + // clear background + UIColor.clear.setFill() + context.fill(CGRect(origin: .zero, size: size)) + + // draw elephantThreeOnGrass + elephantThreeOnGrassExtendImage.draw(at: CGPoint(x: 0, y: height - elephantThreeOnGrassExtendImage.size.height)) + } + } + } + } #if canImport(SwiftUI) && DEBUG @@ -140,13 +217,17 @@ struct WelcomeIllustrationView_Previews: PreviewProvider { static var previews: some View { Group { UIViewPreview(width: 375) { - WelcomeIllustrationView() + let view = WelcomeIllustrationView() + view.layout = .compact + return view } .previewLayout(.fixed(width: 375, height: 1500)) - UIViewPreview(width: 1125) { - WelcomeIllustrationView() + UIViewPreview(width: 547) { + let view = WelcomeIllustrationView() + view.layout = .regular + return view } - .previewLayout(.fixed(width: 1125, height: 5000)) + .previewLayout(.fixed(width: 547, height: 1500)) } } diff --git a/Mastodon/Scene/Onboarding/Welcome/WelcomeViewController.swift b/Mastodon/Scene/Onboarding/Welcome/WelcomeViewController.swift index bf33ea13..1dff6965 100644 --- a/Mastodon/Scene/Onboarding/Welcome/WelcomeViewController.swift +++ b/Mastodon/Scene/Onboarding/Welcome/WelcomeViewController.swift @@ -11,6 +11,8 @@ import Combine final class WelcomeViewController: UIViewController, NeedsDependency { + let logger = Logger(subsystem: "WelcomeViewController", category: "ViewController") + weak var context: AppContext! { willSet { precondition(!isViewLoaded) } } weak var coordinator: SceneCoordinator! { willSet { precondition(!isViewLoaded) } } @@ -41,29 +43,35 @@ final class WelcomeViewController: UIViewController, NeedsDependency { return label }() + let buttonContainer = UIStackView() + private(set) lazy var signUpButton: PrimaryActionButton = { let button = PrimaryActionButton() button.adjustsBackgroundImageWhenUserInterfaceStyleChanges = false - button.setTitle(L10n.Common.Controls.Actions.signUp, for: .normal) + button.setTitle("Get Started", for: .normal) // TODO: i18n let backgroundImageColor: UIColor = .white let backgroundImageHighlightedColor: UIColor = UIColor(white: 0.8, alpha: 1.0) button.setBackgroundImage(.placeholder(color: backgroundImageColor), for: .normal) button.setBackgroundImage(.placeholder(color: backgroundImageHighlightedColor), for: .highlighted) - let titleColor: UIColor = Asset.Colors.brandBlue.color - button.setTitleColor(titleColor, for: .normal) - button.translatesAutoresizingMaskIntoConstraints = false + button.setTitleColor(.black, for: .normal) return button }() + let signUpButtonShadowView = UIView() - private(set) lazy var signInButton: UIButton = { - let button = UIButton(type: .system) + private(set) lazy var signInButton: PrimaryActionButton = { + let button = PrimaryActionButton() + button.adjustsBackgroundImageWhenUserInterfaceStyleChanges = false button.titleLabel?.font = UIFontMetrics(forTextStyle: .headline).scaledFont(for: .systemFont(ofSize: 15, weight: .semibold)) - button.setTitle(L10n.Common.Controls.Actions.signIn, for: .normal) - let titleColor: UIColor = UIColor.white.withAlphaComponent(0.8) + button.setTitle("Log In", for: .normal) + let backgroundImageColor = Asset.Scene.Welcome.signInButtonBackground.color + let backgroundImageHighlightedColor = Asset.Scene.Welcome.signInButtonBackground.color.withAlphaComponent(0.8) + button.setBackgroundImage(.placeholder(color: backgroundImageColor), for: .normal) + button.setBackgroundImage(.placeholder(color: backgroundImageHighlightedColor), for: .highlighted) + let titleColor: UIColor = UIColor.white.withAlphaComponent(0.9) button.setTitleColor(titleColor, for: .normal) - button.translatesAutoresizingMaskIntoConstraints = false return button }() + let signInButtonShadowView = UIView() deinit { os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s", ((#file as NSString).lastPathComponent), #line, #function) @@ -76,7 +84,8 @@ extension WelcomeViewController { override func viewDidLoad() { super.viewDidLoad() - // preferredContentSize = CGSize(width: 547, height: 678) + definesPresentationContext = true + preferredContentSize = CGSize(width: 547, height: 678) navigationController?.navigationBar.prefersLargeTitles = true navigationItem.largeTitleDisplayMode = .never @@ -84,19 +93,48 @@ extension WelcomeViewController { setupOnboardingAppearance() setupIllustrationLayout() - - view.addSubview(signInButton) - view.addSubview(signUpButton) + + buttonContainer.axis = .vertical + buttonContainer.spacing = 12 + buttonContainer.isLayoutMarginsRelativeArrangement = true + + buttonContainer.translatesAutoresizingMaskIntoConstraints = false + view.addSubview(buttonContainer) NSLayoutConstraint.activate([ - 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: WelcomeViewController.actionButtonHeight).priority(.defaultHigh), - - signInButton.topAnchor.constraint(equalTo: signUpButton.bottomAnchor, constant: 9), - 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), + buttonContainer.leadingAnchor.constraint(equalTo: view.readableContentGuide.leadingAnchor), + buttonContainer.trailingAnchor.constraint(equalTo: view.readableContentGuide.trailingAnchor), + view.layoutMarginsGuide.bottomAnchor.constraint(equalTo: buttonContainer.bottomAnchor), + ]) + + signUpButton.translatesAutoresizingMaskIntoConstraints = false + buttonContainer.addArrangedSubview(signUpButton) + NSLayoutConstraint.activate([ + signUpButton.heightAnchor.constraint(equalToConstant: WelcomeViewController.actionButtonHeight).priority(.required - 1), + ]) + signInButton.translatesAutoresizingMaskIntoConstraints = false + buttonContainer.addArrangedSubview(signInButton) + NSLayoutConstraint.activate([ + signInButton.heightAnchor.constraint(equalToConstant: WelcomeViewController.actionButtonHeight).priority(.required - 1), + ]) + + signUpButtonShadowView.translatesAutoresizingMaskIntoConstraints = false + buttonContainer.addSubview(signUpButtonShadowView) + buttonContainer.sendSubviewToBack(signUpButtonShadowView) + NSLayoutConstraint.activate([ + signUpButtonShadowView.topAnchor.constraint(equalTo: signUpButton.topAnchor), + signUpButtonShadowView.leadingAnchor.constraint(equalTo: signUpButton.leadingAnchor), + signUpButtonShadowView.trailingAnchor.constraint(equalTo: signUpButton.trailingAnchor), + signUpButtonShadowView.bottomAnchor.constraint(equalTo: signUpButton.bottomAnchor), + ]) + + signInButtonShadowView.translatesAutoresizingMaskIntoConstraints = false + buttonContainer.addSubview(signInButtonShadowView) + buttonContainer.sendSubviewToBack(signInButtonShadowView) + NSLayoutConstraint.activate([ + signInButtonShadowView.topAnchor.constraint(equalTo: signInButton.topAnchor), + signInButtonShadowView.leadingAnchor.constraint(equalTo: signInButton.leadingAnchor), + signInButtonShadowView.trailingAnchor.constraint(equalTo: signInButton.trailingAnchor), + signInButtonShadowView.bottomAnchor.constraint(equalTo: signInButton.bottomAnchor), ]) signUpButton.addTarget(self, action: #selector(signUpButtonDidClicked(_:)), for: .touchUpInside) @@ -109,17 +147,12 @@ extension WelcomeViewController { self.navigationItem.leftBarButtonItem = needsShowDismissEntry ? self.dismissBarButtonItem : nil } .store(in: &disposeBag) - - view.observe(\.frame, options: [.initial, .new]) { [weak self] view, _ in - guard let self = self else { return } - switch view.traitCollection.userInterfaceIdiom { - case .phone: - break - default: - self.welcomeIllustrationView.elephantOnAirplaneWithContrailImageView.isHidden = view.frame.height < 800 - } - } - .store(in: &observations) + } + + override func viewDidLayoutSubviews() { + super.viewDidLayoutSubviews() + + setupButtonShadowView() } override func viewSafeAreaInsetsDidChange() { @@ -130,18 +163,75 @@ extension WelcomeViewController { if view.safeAreaInsets.bottom == 0 { overlap += 56 } - // shift illustration down for iPad modal - if UIDevice.current.userInterfaceIdiom != .phone { - overlap += 20 - } welcomeIllustrationViewBottomAnchorLayoutConstraint?.constant = overlap } + + override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) { + super.traitCollectionDidChange(previousTraitCollection) + + setupIllustrationLayout() + setupButtonShadowView() + } } extension WelcomeViewController { + private func setupButtonShadowView() { + signUpButtonShadowView.layer.setupShadow( + color: .black, + alpha: 0.25, + x: 0, + y: 1, + blur: 2, + spread: 0, + roundedRect: signInButtonShadowView.bounds, + byRoundingCorners: .allCorners, + cornerRadii: CGSize(width: 10, height: 10) + ) + signInButtonShadowView.layer.setupShadow( + color: .black, + alpha: 0.25, + x: 0, + y: 1, + blur: 2, + spread: 0, + roundedRect: signInButtonShadowView.bounds, + byRoundingCorners: .allCorners, + cornerRadii: CGSize(width: 10, height: 10) + ) + } + + private func updateButtonContainerLayoutMargins(traitCollection: UITraitCollection) { + switch traitCollection.userInterfaceIdiom { + case .phone: + buttonContainer.layoutMargins = UIEdgeInsets( + top: 0, + left: WelcomeViewController.actionButtonMargin, + bottom: WelcomeViewController.viewBottomPaddingHeight, + right: WelcomeViewController.actionButtonMargin + ) + default: + let margin = traitCollection.horizontalSizeClass == .regular ? WelcomeViewController.actionButtonMarginExtend : WelcomeViewController.actionButtonMargin + buttonContainer.layoutMargins = UIEdgeInsets( + top: 0, + left: margin, + bottom: WelcomeViewController.viewBottomPaddingHeightExtend, + right: margin + ) + } + } + private func setupIllustrationLayout() { + welcomeIllustrationView.layout = { + switch traitCollection.userInterfaceIdiom { + case .phone: + return .compact + default: + return .regular + } + }() + // set logo if logoImageView.superview == nil { view.addSubview(logoImageView) @@ -154,10 +244,11 @@ extension WelcomeViewController { logoImageView.setContentHuggingPriority(.defaultHigh, for: .vertical) } - // set illustration for phone + // set illustration guard welcomeIllustrationView.superview == nil else { return } + welcomeIllustrationView.contentMode = .scaleAspectFit welcomeIllustrationView.translatesAutoresizingMaskIntoConstraints = false welcomeIllustrationViewBottomAnchorLayoutConstraint = welcomeIllustrationView.bottomAnchor.constraint(equalTo: view.bottomAnchor, constant: 5) @@ -166,7 +257,7 @@ extension WelcomeViewController { NSLayoutConstraint.activate([ view.leftAnchor.constraint(equalTo: welcomeIllustrationView.leftAnchor, constant: 15), welcomeIllustrationView.rightAnchor.constraint(equalTo: view.rightAnchor, constant: 15), - welcomeIllustrationViewBottomAnchorLayoutConstraint! + welcomeIllustrationViewBottomAnchorLayoutConstraint!.priority(.required - 1), ]) welcomeIllustrationView.cloudBaseImageView.addMotionEffect( @@ -268,21 +359,36 @@ extension WelcomeViewController: OnboardingViewControllerAppearance { // MARK: - UIAdaptivePresentationControllerDelegate extension WelcomeViewController: UIAdaptivePresentationControllerDelegate { + func adaptivePresentationStyle(for controller: UIPresentationController, traitCollection: UITraitCollection) -> UIModalPresentationStyle { + logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public)") + + // update button layout + updateButtonContainerLayoutMargins(traitCollection: traitCollection) + + let navigationController = navigationController as? OnboardingNavigationController + switch traitCollection.userInterfaceIdiom { case .phone: + navigationController?.gradientBorderView.isHidden = true // make underneath view controller alive to fix layout issue due to view life cycle return .fullScreen default: - return .formSheet -// switch traitCollection.horizontalSizeClass { -// case .regular: -// default: -// return .fullScreen -// } + switch traitCollection.horizontalSizeClass { + case .compact: + navigationController?.gradientBorderView.isHidden = true + return .fullScreen + default: + navigationController?.gradientBorderView.isHidden = false + return .formSheet + } } } + func presentationController(_ controller: UIPresentationController, viewControllerForAdaptivePresentationStyle style: UIModalPresentationStyle) -> UIViewController? { + return nil + } + func presentationControllerShouldDismiss(_ presentationController: UIPresentationController) -> Bool { return false } diff --git a/Mastodon/Scene/Profile/Header/ProfileHeaderViewModel.swift b/Mastodon/Scene/Profile/Header/ProfileHeaderViewModel.swift index e8405b6a..40883120 100644 --- a/Mastodon/Scene/Profile/Header/ProfileHeaderViewModel.swift +++ b/Mastodon/Scene/Profile/Header/ProfileHeaderViewModel.swift @@ -14,6 +14,7 @@ import MastodonMeta final class ProfileHeaderViewModel { + static let avatarImageMaxSizeInPixel = CGSize(width: 400, height: 400) static let maxProfileFieldCount = 4 var disposeBag = Set() @@ -190,8 +191,8 @@ extension ProfileHeaderViewModel { let image: UIImage? = { guard case let .image(_image) = editProfileInfo.avatarImageResource.value else { return nil } guard let image = _image else { return nil } - guard image.size.width <= MastodonRegisterViewController.avatarImageMaxSizeInPixel.width else { - return image.af.imageScaled(to: MastodonRegisterViewController.avatarImageMaxSizeInPixel) + guard image.size.width <= ProfileHeaderViewModel.avatarImageMaxSizeInPixel.width else { + return image.af.imageScaled(to: ProfileHeaderViewModel.avatarImageMaxSizeInPixel) } return image }() diff --git a/Mastodon/Scene/Root/MainTab/MainTabBarController.swift b/Mastodon/Scene/Root/MainTab/MainTabBarController.swift index 4b803bc4..058a0fc3 100644 --- a/Mastodon/Scene/Root/MainTab/MainTabBarController.swift +++ b/Mastodon/Scene/Root/MainTab/MainTabBarController.swift @@ -74,11 +74,7 @@ class MainTabBarController: UITabBarController { let viewController: UIViewController switch self { case .home: - #if ASDK - let _viewController: NeedsDependency & UIViewController = UserDefaults.shared.preferAsyncHomeTimeline ? AsyncHomeTimelineViewController() : HomeTimelineViewController() - #else let _viewController = HomeTimelineViewController() - #endif _viewController.context = context _viewController.coordinator = coordinator viewController = _viewController @@ -596,33 +592,3 @@ extension MainTabBarController { } } - -#if ASDK -extension MainTabBarController { - override func motionEnded(_ motion: UIEvent.EventSubtype, with event: UIEvent?) { - guard let event = event else { return } - switch event.subtype { - case .motionShake: - let alertController = UIAlertController(title: "ASDK Debug Panel", message: nil, preferredStyle: .alert) - let toggleHomeAction = UIAlertAction(title: "Toggle Home", style: .default) { [weak self] _ in - guard let self = self else { return } - MainTabBarController.toggleAsyncHome() - let okAlertController = UIAlertController(title: "Success", message: "Please restart the app", preferredStyle: .alert) - let okAction = UIAlertAction(title: "OK", style: .default, handler: nil) - okAlertController.addAction(okAction) - self.coordinator.present(scene: .alertController(alertController: okAlertController), from: nil, transition: .alertController(animated: true, completion: nil)) - } - alertController.addAction(toggleHomeAction) - let cancelAction = UIAlertAction(title: "Cancel", style: .cancel, handler: nil) - alertController.addAction(cancelAction) - self.coordinator.present(scene: .alertController(alertController: alertController), from: nil, transition: .alertController(animated: true, completion: nil)) - default: - break - } - } - - static func toggleAsyncHome() { - UserDefaults.shared.preferAsyncHomeTimeline.toggle() - } -} -#endif diff --git a/Mastodon/Scene/Share/NavigationController/AdaptiveStatusBarStyleNavigationController.swift b/Mastodon/Scene/Share/NavigationController/AdaptiveStatusBarStyleNavigationController.swift index eb260853..aac23285 100644 --- a/Mastodon/Scene/Share/NavigationController/AdaptiveStatusBarStyleNavigationController.swift +++ b/Mastodon/Scene/Share/NavigationController/AdaptiveStatusBarStyleNavigationController.swift @@ -9,7 +9,7 @@ import UIKit // Make status bar style adaptive for child view controller // SeeAlso: `modalPresentationCapturesStatusBarAppearance` -final class AdaptiveStatusBarStyleNavigationController: UINavigationController { +class AdaptiveStatusBarStyleNavigationController: UINavigationController { override var childForStatusBarStyle: UIViewController? { visibleViewController } diff --git a/Mastodon/Scene/Share/View/Button/PrimaryActionButton.swift b/Mastodon/Scene/Share/View/Button/PrimaryActionButton.swift index 326dfa12..676d558a 100644 --- a/Mastodon/Scene/Share/View/Button/PrimaryActionButton.swift +++ b/Mastodon/Scene/Share/View/Button/PrimaryActionButton.swift @@ -9,8 +9,8 @@ import UIKit class PrimaryActionButton: UIButton { - var isLoading: Bool = false - + private var originalButtonTitle: String? + lazy var activityIndicator: UIActivityIndicatorView = { let indicator = UIActivityIndicatorView(style: .medium) indicator.color = .white @@ -18,10 +18,13 @@ class PrimaryActionButton: UIButton { indicator.translatesAutoresizingMaskIntoConstraints = false return indicator }() - - private var originalButtonTitle: String? - var adjustsBackgroundImageWhenUserInterfaceStyleChanges = true + var action: Action = .next { + didSet { + setupAppearance(action: action) + } + } + var isLoading: Bool = false override init(frame: CGRect) { super.init(frame: frame) @@ -35,26 +38,44 @@ class PrimaryActionButton: UIButton { } +extension PrimaryActionButton { + + public enum Action { + case back + case next + } + +} + extension PrimaryActionButton { private func _init() { titleLabel?.font = UIFontMetrics(forTextStyle: .headline).scaledFont(for: .systemFont(ofSize: 17, weight: .semibold)) setTitleColor(.white, for: .normal) - setupBackgroundAppearance() + setupAppearance(action: action) applyCornerRadius(radius: 10) } - func setupBackgroundAppearance() { - setBackgroundImage(UIImage.placeholder(color: Asset.Colors.brandBlue.color), for: .normal) - setBackgroundImage(UIImage.placeholder(color: Asset.Colors.brandBlueDarken20.color), for: .highlighted) - setBackgroundImage(UIImage.placeholder(color: Asset.Colors.disabled.color), for: .disabled) + func setupAppearance(action: Action) { + switch action { + case .back: + setTitleColor(Asset.Colors.Label.primary.color, for: .normal) + setBackgroundImage(UIImage.placeholder(color: Asset.Scene.Onboarding.navigationBackButtonBackground.color), for: .normal) + setBackgroundImage(UIImage.placeholder(color: Asset.Scene.Onboarding.navigationBackButtonBackgroundHighlighted.color), for: .highlighted) + setBackgroundImage(UIImage.placeholder(color: Asset.Colors.disabled.color), for: .disabled) + case .next: + setTitleColor(Asset.Colors.Label.primaryReverse.color, for: .normal) + setBackgroundImage(UIImage.placeholder(color: Asset.Scene.Onboarding.navigationNextButtonBackground.color), for: .normal) + setBackgroundImage(UIImage.placeholder(color: Asset.Scene.Onboarding.navigationNextButtonBackgroundHighlighted.color), for: .highlighted) + setBackgroundImage(UIImage.placeholder(color: Asset.Colors.disabled.color), for: .disabled) + } } override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) { super.traitCollectionDidChange(previousTraitCollection) if adjustsBackgroundImageWhenUserInterfaceStyleChanges { - setupBackgroundAppearance() + setupAppearance(action: action) } } diff --git a/Mastodon/Scene/Share/View/Container/TouchBlockingView.swift b/Mastodon/Scene/Share/View/Container/TouchBlockingView.swift index b86137f1..5a151812 100644 --- a/Mastodon/Scene/Share/View/Container/TouchBlockingView.swift +++ b/Mastodon/Scene/Share/View/Container/TouchBlockingView.swift @@ -7,7 +7,7 @@ import UIKit -final class TouchBlockingView: UIView { +class TouchBlockingView: UIView { override init(frame: CGRect) { super.init(frame: frame) diff --git a/Mastodon/Scene/Share/View/Content/TimelineHeaderView.swift b/Mastodon/Scene/Share/View/Content/TimelineHeaderView.swift index 2948af4c..50518e59 100644 --- a/Mastodon/Scene/Share/View/Content/TimelineHeaderView.swift +++ b/Mastodon/Scene/Share/View/Content/TimelineHeaderView.swift @@ -126,10 +126,10 @@ struct TimelineHeaderView_Previews: PreviewProvider { static var previews: some View { Group { UIViewPreview(width: 375) { - let headerView = TimelineHeaderView() - headerView.iconImageView.image = Item.EmptyStateHeaderAttribute.Reason.blocking(name: nil).iconImage - headerView.messageLabel.text = Item.EmptyStateHeaderAttribute.Reason.blocking(name: nil).message - return headerView + let serverSectionHeaderView = TimelineHeaderView() + serverSectionHeaderView.iconImageView.image = Item.EmptyStateHeaderAttribute.Reason.blocking(name: nil).iconImage + serverSectionHeaderView.messageLabel.text = Item.EmptyStateHeaderAttribute.Reason.blocking(name: nil).message + return serverSectionHeaderView } .previewLayout(.fixed(width: 375, height: 400)) } diff --git a/Mastodon/Scene/Share/View/Node/ASMetaEditableTextNode.swift b/Mastodon/Scene/Share/View/Node/ASMetaEditableTextNode.swift deleted file mode 100644 index e5037fdf..00000000 --- a/Mastodon/Scene/Share/View/Node/ASMetaEditableTextNode.swift +++ /dev/null @@ -1,25 +0,0 @@ -// -// ASMetaEditableTextNode.swift -// Mastodon -// -// Created by Cirno MainasuK on 2021-6-20. -// - -#if ASDK - -import UIKit -import AsyncDisplayKit - -protocol ASMetaEditableTextNodeDelegate: AnyObject { - func metaEditableTextNode(_ textNode: ASMetaEditableTextNode, shouldInteractWith URL: URL, in characterRange: NSRange, interaction: UITextItemInteraction) -> Bool -} - -final class ASMetaEditableTextNode: ASEditableTextNode, UITextViewDelegate { - weak var metaEditableTextNodeDelegate: ASMetaEditableTextNodeDelegate? - - func textView(_ textView: UITextView, shouldInteractWith URL: URL, in characterRange: NSRange, interaction: UITextItemInteraction) -> Bool { - return metaEditableTextNodeDelegate?.metaEditableTextNode(self, shouldInteractWith: URL, in: characterRange, interaction: interaction) ?? false - } -} - -#endif diff --git a/Mastodon/Scene/Share/View/Node/Status/StatusNode.swift b/Mastodon/Scene/Share/View/Node/Status/StatusNode.swift deleted file mode 100644 index 17054348..00000000 --- a/Mastodon/Scene/Share/View/Node/Status/StatusNode.swift +++ /dev/null @@ -1,234 +0,0 @@ -// -// StatusNNode.swift -// Mastodon -// -// Created by Cirno MainasuK on 2021-6-19. -// - -#if ASDK - -import UIKit -import Combine -import AsyncDisplayKit -import CoreDataStack -import func AVFoundation.AVMakeRect - -protocol StatusNodeDelegate: AnyObject { - //func statusNode(_ node: StatusNode, statusContentTextNode: ASMetaEditableTextNode, didSelectActiveEntityType type: ActiveEntityType) -} - -final class StatusNode: ASCellNode { - - var disposeBag = Set() - var timestamp: Date - var timestampSubscription: AnyCancellable? - - weak var delegate: StatusNodeDelegate? // needs assign on main queue - - static let avatarImageSize = CGSize(width: 42, height: 42) - static let avatarImageCornerRadius: CGFloat = 4 - -// static let statusContentAppearance: MastodonStatusContent.Appearance = { -// let linkAttributes: [NSAttributedString.Key: Any] = [ -// .font: UIFontMetrics(forTextStyle: .body).scaledFont(for: .systemFont(ofSize: 17, weight: .semibold)), -// .foregroundColor: Asset.Colors.brandBlue.color -// ] -// return MastodonStatusContent.Appearance( -// attributes: [ -// .font: UIFontMetrics(forTextStyle: .body).scaledFont(for: .systemFont(ofSize: 17, weight: .regular)), -// .foregroundColor: Asset.Colors.Label.primary.color -// ], -// urlAttributes: linkAttributes, -// hashtagAttributes: linkAttributes, -// mentionAttributes: linkAttributes -// ) -// }() - - let avatarImageNode: ASNetworkImageNode = { - let node = ASNetworkImageNode() - node.contentMode = .scaleAspectFill - node.defaultImage = UIImage.placeholder(color: .systemFill) - node.forcedSize = StatusNode.avatarImageSize - node.cornerRadius = StatusNode.avatarImageCornerRadius - // node.cornerRoundingType = .precomposited - // node.shouldRenderProgressImages = true - return node - }() - let nameTextNode = ASTextNode() - let nameDotTextNode = ASTextNode() - let dateTextNode = ASTextNode() - let usernameTextNode = ASTextNode() - let statusContentTextNode: ASMetaEditableTextNode = { - let node = ASMetaEditableTextNode() - node.scrollEnabled = false - return node - }() - - let mosaicImageViewModel: MosaicImageViewModel - let mediaMultiplexImageNodes: [ASMultiplexImageNode] - - init(status: Status) { - timestamp = (status.reblog ?? status).createdAt - let _mosaicImageViewModel: MosaicImageViewModel = { - let mediaAttachments = Array((status.reblog ?? status).mediaAttachments ?? []).sorted { $0.index.compare($1.index) == .orderedAscending } - return MosaicImageViewModel(mediaAttachments: mediaAttachments) - }() - mosaicImageViewModel = _mosaicImageViewModel - mediaMultiplexImageNodes = { - var imageNodes: [ASMultiplexImageNode] = [] - for _ in 0..<_mosaicImageViewModel.metas.count { - let imageNode = ASMultiplexImageNode() // TODO: adapt downloader - imageNode.downloadsIntermediateImages = true - imageNode.imageIdentifiers = ["url", "previewURL"].map { $0 as NSString } // quality in descending order - imageNodes.append(imageNode) - } - return imageNodes - }() - super.init() - - automaticallyManagesSubnodes = true - - if let url = (status.reblog ?? status).author.avatarImageURL() { - avatarImageNode.url = url - } - - nameTextNode.attributedText = NSAttributedString(string: status.author.displayNameWithFallback, attributes: [ - .foregroundColor: Asset.Colors.Label.primary.color, - .font: UIFont.systemFont(ofSize: 17, weight: .semibold) - ]) - nameDotTextNode.attributedText = NSAttributedString(string: "·", attributes: [ - .foregroundColor: Asset.Colors.Label.secondary.color, - .font: UIFont.systemFont(ofSize: 13, weight: .regular) - ]) - // set date - dateTextNode.attributedText = NSAttributedString(string: timestamp.localizedSlowedTimeAgoSinceNow, attributes: [ - .foregroundColor: Asset.Colors.Label.secondary.color, - .font: UIFont.systemFont(ofSize: 13, weight: .regular) - ]) - - usernameTextNode.attributedText = NSAttributedString(string: "@" + status.author.acct, attributes: [ - .foregroundColor: Asset.Colors.Label.secondary.color, - .font: UIFont.systemFont(ofSize: 15, weight: .regular) - ]) - - // FIXME: - // statusContentTextNode.metaEditableTextNodeDelegate = self -// if let parseResult = try? MastodonStatusContent.parse( -// content: (status.reblog ?? status).content, -// emojiDict: (status.reblog ?? status).emojiDict -// ) { -// statusContentTextNode.attributedText = parseResult.trimmedAttributedString(appearance: StatusNode.statusContentAppearance) -// } - - for imageNode in mediaMultiplexImageNodes { - imageNode.delegate = self - } - } - - override func didEnterDisplayState() { - super.didEnterDisplayState() - - timestampSubscription = AppContext.shared.timestampUpdatePublisher - .sink { [weak self] _ in - guard let self = self else { return } - self.dateTextNode.attributedText = NSAttributedString(string: self.timestamp.localizedSlowedTimeAgoSinceNow, attributes: [ - .foregroundColor: Asset.Colors.Label.secondary.color, - .font: UIFont.systemFont(ofSize: 13, weight: .regular) - ]) - } - - // FIXME: needs move to other only once called callback in life cycle like: `viewDidLoad` - statusContentTextNode.textView.isEditable = false - statusContentTextNode.textView.textDragInteraction?.isEnabled = false - statusContentTextNode.textView.linkTextAttributes = [ - .foregroundColor: Asset.Colors.brandBlue.color - ] - } - - override func didExitVisibleState() { - super.didExitVisibleState() - timestampSubscription = nil - } - - override func layoutSpecThatFits(_ constrainedSize: ASSizeRange) -> ASLayoutSpec { - let headerStack = ASStackLayoutSpec.horizontal() - headerStack.alignItems = .center - headerStack.spacing = 5 - var headerStackChildren: [ASLayoutElement] = [] - - avatarImageNode.style.preferredSize = StatusNode.avatarImageSize - headerStackChildren.append(avatarImageNode) - - let authorMetaHeaderStack = ASStackLayoutSpec.horizontal() - authorMetaHeaderStack.alignItems = .center - authorMetaHeaderStack.spacing = 4 - authorMetaHeaderStack.children = [ - nameTextNode, - nameDotTextNode, - dateTextNode, - ] - let authorMetaStack = ASStackLayoutSpec.vertical() - authorMetaStack.children = [ - authorMetaHeaderStack, - usernameTextNode, - ] - - headerStackChildren.append(authorMetaStack) - - headerStack.children = headerStackChildren - - let verticalStack = ASStackLayoutSpec.vertical() - verticalStack.spacing = 10 - var verticalStackChildren: [ASLayoutElement] = [ - headerStack, - statusContentTextNode, - ] - if !mediaMultiplexImageNodes.isEmpty { - for (imageNode, meta) in zip(mediaMultiplexImageNodes, mosaicImageViewModel.metas) { - imageNode.style.preferredSize = AVMakeRect(aspectRatio: meta.size, insideRect: CGRect(origin: .zero, size: constrainedSize.max)).size - let layout = ASRatioLayoutSpec(ratio: meta.size.height / meta.size.width, child: imageNode) - verticalStackChildren.append(layout) - } - } - verticalStack.children = verticalStackChildren - - return ASInsetLayoutSpec( - insets: UIEdgeInsets(top: 12, left: 16, bottom: 12, right: 16), - child: verticalStack - ) - } - -} - -// MARK: - ASEditableTextNodeDelegate -//extension StatusNode: ASMetaEditableTextNodeDelegate { -// func metaEditableTextNode(_ textNode: ASMetaEditableTextNode, shouldInteractWith URL: URL, in characterRange: NSRange, interaction: UITextItemInteraction) -> Bool { -// guard let activityEntityType = ActiveEntityType(url: URL) else { -// return false -// } -// defer { -// delegate?.statusNode(self, statusContentTextNode: textNode, didSelectActiveEntityType: activityEntityType) -// } -// return false -// } -//} - -// MARK: - ASMultiplexImageNodeDataSource -extension StatusNode: ASMultiplexImageNodeDataSource { - func multiplexImageNode(_ imageNode: ASMultiplexImageNode, urlForImageIdentifier imageIdentifier: ASImageIdentifier) -> URL? { - guard let imageNodeIndex = mediaMultiplexImageNodes.firstIndex(of: imageNode) else { return nil } - guard imageNodeIndex < mosaicImageViewModel.metas.count else { return nil } - let meta = mosaicImageViewModel.metas[imageNodeIndex] - switch imageIdentifier { - case "url" as NSString: - return meta.url - case "previewURL" as NSString: - return meta.previewURL - default: - assertionFailure() - return nil - } - } -} - -#endif diff --git a/Mastodon/Scene/Share/View/Node/Status/TimelineBottomLoaderNode.swift b/Mastodon/Scene/Share/View/Node/Status/TimelineBottomLoaderNode.swift deleted file mode 100644 index 0ec83dfe..00000000 --- a/Mastodon/Scene/Share/View/Node/Status/TimelineBottomLoaderNode.swift +++ /dev/null @@ -1,41 +0,0 @@ -// -// TimelineBottomLoaderNode.swift -// Mastodon -// -// Created by Cirno MainasuK on 2021-6-19. -// - -#if ASDK - -import UIKit -import AsyncDisplayKit - -final class TimelineBottomLoaderNode: ASCellNode { - - let activityIndicatorNode = ActivityIndicatorNode() - - override init() { - super.init() - - automaticallyManagesSubnodes = true - activityIndicatorNode.bounds = CGRect(x: 0, y: 0, width: 40, height: 40) - } - - override func layoutSpecThatFits(_ constrainedSize: ASSizeRange) -> ASLayoutSpec { - let contentStack = ASStackLayoutSpec.horizontal() - contentStack.alignItems = .center - contentStack.spacing = 7 - - contentStack.children = [activityIndicatorNode] - - return contentStack - } - - override func didEnterDisplayState() { - super.didEnterDisplayState() - activityIndicatorNode.animating = true - } - -} - -#endif diff --git a/Mastodon/Scene/Share/View/Node/Status/TimelineMiddleLoaderNode.swift b/Mastodon/Scene/Share/View/Node/Status/TimelineMiddleLoaderNode.swift deleted file mode 100644 index bd662ad7..00000000 --- a/Mastodon/Scene/Share/View/Node/Status/TimelineMiddleLoaderNode.swift +++ /dev/null @@ -1,54 +0,0 @@ -// -// TimelineMiddleLoaderNode.swift -// Mastodon -// -// Created by Cirno MainasuK on 2021-6-19. -// - -#if ASDK - -import UIKit -import AsyncDisplayKit - -final class TimelineMiddleLoaderNode: ASCellNode { - - static let loadButtonFont = UIFontMetrics(forTextStyle: .body).scaledFont(for: .systemFont(ofSize: 17, weight: .medium)) - - let activityIndicatorNode = ASDisplayNode(viewBlock: { - let view = UIActivityIndicatorView(style: .medium) - view.hidesWhenStopped = true - return view - }) - - let loadButtonNode = ASButtonNode() - - override init() { - super.init() - - automaticallyManagesSubnodes = true - - loadButtonNode.setAttributedTitle( - NSAttributedString( - string: L10n.Common.Controls.Timeline.Loader.loadMissingPosts, - attributes: [ - .foregroundColor: Asset.Colors.brandBlue.color, - .font: TimelineMiddleLoaderNode.loadButtonFont - ]), - for: .normal - ) - } - - override func layoutSpecThatFits(_ constrainedSize: ASSizeRange) -> ASLayoutSpec { - let contentStack = ASStackLayoutSpec.horizontal() - contentStack.alignItems = .center - contentStack.spacing = 7 - - contentStack.children = [loadButtonNode] - - - return contentStack - } - -} - -#endif diff --git a/Mastodon/Scene/Wizard/WizardViewController.swift b/Mastodon/Scene/Wizard/WizardViewController.swift index 2678c712..9152e64f 100644 --- a/Mastodon/Scene/Wizard/WizardViewController.swift +++ b/Mastodon/Scene/Wizard/WizardViewController.swift @@ -35,7 +35,7 @@ class WizardViewController: UIViewController { let backgroundView: UIView = { let view = UIView() - view.backgroundColor = UIColor.black.withAlphaComponent(0.7) + view.backgroundColor = UIColor.black.withAlphaComponent(0.5) return view }() diff --git a/Mastodon/Supporting Files/AppDelegate.swift b/Mastodon/Supporting Files/AppDelegate.swift index e2cb7c41..6d7919c6 100644 --- a/Mastodon/Supporting Files/AppDelegate.swift +++ b/Mastodon/Supporting Files/AppDelegate.swift @@ -12,10 +12,6 @@ import AppShared import AVFoundation @_exported import MastodonUI -#if ASDK -import AsyncDisplayKit -#endif - @main class AppDelegate: UIResponder, UIApplicationDelegate { @@ -41,13 +37,6 @@ class AppDelegate: UIResponder, UIApplicationDelegate { count += 1 // Int64. could ignore overflow here UserDefaults.shared.processCompletedCount = count - #if ASDK && DEBUG - // PerformanceMonitor.shared().start() - // ASDisplayNode.shouldShowRangeDebugOverlay = true - // ASControlNode.enableHitTestDebug = true - // ASImageNode.shouldShowImageScalingOverlay = true - #endif - return true } diff --git a/Mastodon/Vender/ActivityIndicatorNode.swift b/Mastodon/Vender/ActivityIndicatorNode.swift deleted file mode 100644 index 8778f5ec..00000000 --- a/Mastodon/Vender/ActivityIndicatorNode.swift +++ /dev/null @@ -1,75 +0,0 @@ -// ref: https://github.com/Adlai-Holler/ASDKPlaceholderTest/blob/eea9fa7cff2d16a57efb47d208422ea9b49a630a/ASDKPlaceholderTest/ASDisplayNodeSubclasses.swift - -#if ASDK - -import Foundation -import AsyncDisplayKit -import UIKit - -/** - A node that shows a `UIActivityIndicatorView`. Does not support layer backing. - Note: You must not change the style to or from `.WhiteLarge` after init, or the node's size will not update. - */ -class ActivityIndicatorNode: ASDisplayNode { - - private static let defaultSize = CGSize(width: 20, height: 20) - private static let largeSize = CGSize(width: 37, height: 37) - - init(style: UIActivityIndicatorView.Style = .medium) { - super.init() - setViewBlock { - UIActivityIndicatorView(style: style) - } - - self.style.preferredSize = style == .large ? ActivityIndicatorNode.defaultSize : ActivityIndicatorNode.largeSize - } - - var activityIndicatorView: UIActivityIndicatorView { - return view as! UIActivityIndicatorView - } - - override func didLoad() { - super.didLoad() - if animating { - activityIndicatorView.startAnimating() - } - activityIndicatorView.color = color - activityIndicatorView.hidesWhenStopped = hidesWhenStopped - } - - /// Wrapper for `UIActivityIndicatorView.hidesWhenStopped`. NOTE: You must respect thread affinity. - var hidesWhenStopped = true { - didSet { - if isNodeLoaded { - assert(Thread.isMainThread) - activityIndicatorView.hidesWhenStopped = hidesWhenStopped - } - } - } - - /// Wrapper for `UIActivityIndicatorView.color`. NOTE: You must respect thread affinity. - var color: UIColor? { - didSet { - if isNodeLoaded { - assert(Thread.isMainThread) - activityIndicatorView.color = color - } - } - } - - /// Wrapper for `UIActivityIndicatorView.animating`. NOTE: You must respect thread affinity. - var animating = false { - didSet { - if isNodeLoaded { - assert(Thread.isMainThread) - if animating { - activityIndicatorView.startAnimating() - } else { - activityIndicatorView.stopAnimating() - } - } - } - } -} - -#endif diff --git a/MastodonIntent/Info.plist b/MastodonIntent/Info.plist index 8ac3d165..78d3b58e 100644 --- a/MastodonIntent/Info.plist +++ b/MastodonIntent/Info.plist @@ -17,9 +17,9 @@ CFBundlePackageType $(PRODUCT_BUNDLE_PACKAGE_TYPE) CFBundleShortVersionString - 1.2.0 + 1.3.0 CFBundleVersion - 88 + 90 NSExtension NSExtensionAttributes diff --git a/Mastodon/Extension/CALayer.swift b/MastodonSDK/Sources/MastodonExtension/CALayer.swift similarity index 95% rename from Mastodon/Extension/CALayer.swift rename to MastodonSDK/Sources/MastodonExtension/CALayer.swift index 41ce739e..684a4a70 100644 --- a/Mastodon/Extension/CALayer.swift +++ b/MastodonSDK/Sources/MastodonExtension/CALayer.swift @@ -9,7 +9,7 @@ import UIKit extension CALayer { - func setupShadow( + public func setupShadow( color: UIColor = .black, alpha: Float = 0.5, x: CGFloat = 0, @@ -43,9 +43,8 @@ extension CALayer { } } - func removeShadow() { + public func removeShadow() { shadowRadius = 0 } - - + } diff --git a/MastodonSDK/Sources/MastodonExtension/UIImage.swift b/MastodonSDK/Sources/MastodonExtension/UIImage.swift index 178d289d..e3560af6 100644 --- a/MastodonSDK/Sources/MastodonExtension/UIImage.swift +++ b/MastodonSDK/Sources/MastodonExtension/UIImage.swift @@ -10,12 +10,28 @@ import CoreImage.CIFilterBuiltins import UIKit extension UIImage { - public static func placeholder(size: CGSize = CGSize(width: 1, height: 1), color: UIColor) -> UIImage { + public static func placeholder( + size: CGSize = CGSize(width: 1, height: 1), + color: UIColor, + cornerRadius: CGFloat = 0 + ) -> UIImage { let render = UIGraphicsImageRenderer(size: size) return render.image { (context: UIGraphicsImageRendererContext) in + // set clear fill context.cgContext.setFillColor(color.cgColor) - context.fill(CGRect(origin: .zero, size: size)) + + let rect = CGRect(origin: .zero, size: size) + + // clip corner if needs + if cornerRadius > 0 { + let path = UIBezierPath(roundedRect: rect, cornerRadius: cornerRadius).cgPath + context.cgContext.addPath(path) + context.cgContext.clip(using: .evenOdd) + } + + // set fill + context.fill(rect) } } } diff --git a/MastodonSDK/Sources/MastodonSDK/Entity/Mastodon+Entity+Instance.swift b/MastodonSDK/Sources/MastodonSDK/Entity/Mastodon+Entity+Instance.swift index d0d16ee4..f245d741 100644 --- a/MastodonSDK/Sources/MastodonSDK/Entity/Mastodon+Entity+Instance.swift +++ b/MastodonSDK/Sources/MastodonSDK/Entity/Mastodon+Entity+Instance.swift @@ -86,7 +86,7 @@ extension Mastodon.Entity.Instance { } extension Mastodon.Entity.Instance { - public struct Rule: Codable { + public struct Rule: Codable, Hashable { public let id: String public let text: String } diff --git a/MastodonSDK/Sources/MastodonUI/Service/KeyboardResponderService.swift b/MastodonSDK/Sources/MastodonUI/Service/KeyboardResponderService.swift index 65328afa..db600a67 100644 --- a/MastodonSDK/Sources/MastodonUI/Service/KeyboardResponderService.swift +++ b/MastodonSDK/Sources/MastodonUI/Service/KeyboardResponderService.swift @@ -90,3 +90,47 @@ extension KeyboardResponderService { case dock } } + +extension KeyboardResponderService { + public static func configure( + scrollView: UIScrollView, + layoutNeedsUpdate: AnyPublisher, + additionalSafeAreaInsets: AnyPublisher = CurrentValueSubject(.zero).eraseToAnyPublisher() + ) -> AnyCancellable { + let tuple = Publishers.CombineLatest3( + KeyboardResponderService.shared.isShow, + KeyboardResponderService.shared.state, + KeyboardResponderService.shared.endFrame + ) + + return Publishers.CombineLatest3( + tuple, + layoutNeedsUpdate, + additionalSafeAreaInsets + ) + .sink(receiveValue: { [weak scrollView] tuple, _, additionalSafeAreaInsets in + guard let scrollView = scrollView else { return } + guard let view = scrollView.superview else { return } + + let (isShow, state, endFrame) = tuple + + guard isShow, state == .dock else { + scrollView.contentInset.bottom = additionalSafeAreaInsets.bottom + scrollView.verticalScrollIndicatorInsets.bottom = additionalSafeAreaInsets.bottom + return + } + + // isShow AND dock state + let contentFrame = view.convert(scrollView.frame, to: nil) + let padding = contentFrame.maxY - endFrame.minY + guard padding > 0 else { + scrollView.contentInset.bottom = additionalSafeAreaInsets.bottom + scrollView.verticalScrollIndicatorInsets.bottom = additionalSafeAreaInsets.bottom + return + } + + scrollView.contentInset.bottom = padding - scrollView.safeAreaInsets.bottom + additionalSafeAreaInsets.bottom + scrollView.verticalScrollIndicatorInsets.bottom = padding - scrollView.safeAreaInsets.bottom + additionalSafeAreaInsets.bottom + }) + } +} diff --git a/MastodonSDK/Sources/MastodonUI/View/Container/ShadowBackgroundContainer.swift b/MastodonSDK/Sources/MastodonUI/View/Container/ShadowBackgroundContainer.swift new file mode 100644 index 00000000..3f2f5df4 --- /dev/null +++ b/MastodonSDK/Sources/MastodonUI/View/Container/ShadowBackgroundContainer.swift @@ -0,0 +1,60 @@ +// +// ShadowBackgroundContainer.swift +// +// +// Created by MainasuK on 2022-1-5. +// + +import UIKit +import MastodonExtension + +public final class ShadowBackgroundContainer: UIView { + + public var shadowAlpha: CGFloat = 0.25 { + didSet { setNeedsLayout() } + } + + public var shadowColor: UIColor = .black { + didSet { setNeedsLayout() } + } + + public var cornerRadius: CGFloat = 10 { + didSet { setNeedsLayout() } + } + + public let shadowLayer = CALayer() + + override init(frame: CGRect) { + super.init(frame: frame) + _init() + } + + required init?(coder: NSCoder) { + super.init(coder: coder) + _init() + } + +} + +extension ShadowBackgroundContainer { + private func _init() { + layer.insertSublayer(shadowLayer, at: 0) + } + + public override func layoutSubviews() { + super.layoutSubviews() + + shadowLayer.frame = bounds + shadowLayer.setupShadow( + color: shadowColor, + alpha: Float(shadowAlpha), + x: 0, + y: 1, + blur: 2, + spread: 0, + roundedRect: bounds, + byRoundingCorners: .allCorners, + cornerRadii: CGSize(width: cornerRadius, height: cornerRadius) + ) + } +} diff --git a/MastodonTests/Info.plist b/MastodonTests/Info.plist index 9fe845c6..f652792e 100644 --- a/MastodonTests/Info.plist +++ b/MastodonTests/Info.plist @@ -15,8 +15,8 @@ CFBundlePackageType $(PRODUCT_BUNDLE_PACKAGE_TYPE) CFBundleShortVersionString - 1.2.0 + 1.3.0 CFBundleVersion - 88 + 90 diff --git a/MastodonUITests/Info.plist b/MastodonUITests/Info.plist index 9fe845c6..f652792e 100644 --- a/MastodonUITests/Info.plist +++ b/MastodonUITests/Info.plist @@ -15,8 +15,8 @@ CFBundlePackageType $(PRODUCT_BUNDLE_PACKAGE_TYPE) CFBundleShortVersionString - 1.2.0 + 1.3.0 CFBundleVersion - 88 + 90 diff --git a/NotificationService/Info.plist b/NotificationService/Info.plist index 8e14f3a2..77c7421d 100644 --- a/NotificationService/Info.plist +++ b/NotificationService/Info.plist @@ -17,9 +17,9 @@ CFBundlePackageType $(PRODUCT_BUNDLE_PACKAGE_TYPE) CFBundleShortVersionString - 1.2.0 + 1.3.0 CFBundleVersion - 88 + 90 NSExtension NSExtensionPointIdentifier diff --git a/Podfile b/Podfile index 868af1a9..4a2e7bc6 100644 --- a/Podfile +++ b/Podfile @@ -8,7 +8,6 @@ target 'Mastodon' do # UI pod 'UITextField+Shake', '~> 1.2' - pod 'Texture', '~> 3.0.0', :configurations => ['ASDK - Debug', 'ASDK - Release'] # misc pod 'SwiftGen', '~> 6.4.0' @@ -16,7 +15,7 @@ target 'Mastodon' do pod 'Kanna', '~> 5.2.2' # DEBUG - pod 'FLEX', '~> 4.4.0', :configurations => ['Debug', 'ASDK - Debug'] + pod 'FLEX', '~> 4.4.0', :configurations => ['Debug'] target 'MastodonTests' do inherit! :search_paths @@ -63,4 +62,4 @@ post_install do |installer| config.build_settings.delete 'IPHONEOS_DEPLOYMENT_TARGET' end end -end \ No newline at end of file +end diff --git a/Podfile.lock b/Podfile.lock index 3541289d..ea4ef823 100644 --- a/Podfile.lock +++ b/Podfile.lock @@ -3,42 +3,7 @@ PODS: - FLEX (4.4.1) - Kanna (5.2.7) - Keys (1.0.1) - - PINCache (3.0.3): - - PINCache/Arc-exception-safe (= 3.0.3) - - PINCache/Core (= 3.0.3) - - PINCache/Arc-exception-safe (3.0.3): - - PINCache/Core - - PINCache/Core (3.0.3): - - PINOperation (~> 1.2.1) - - PINOperation (1.2.1) - - PINRemoteImage/Core (3.0.3): - - PINOperation - - PINRemoteImage/iOS (3.0.3): - - PINRemoteImage/Core - - PINRemoteImage/PINCache (3.0.3): - - PINCache (~> 3.0.3) - - PINRemoteImage/Core - SwiftGen (6.4.0) - - Texture (3.0.0): - - Texture/AssetsLibrary (= 3.0.0) - - Texture/Core (= 3.0.0) - - Texture/MapKit (= 3.0.0) - - Texture/Photos (= 3.0.0) - - Texture/PINRemoteImage (= 3.0.0) - - Texture/Video (= 3.0.0) - - Texture/AssetsLibrary (3.0.0): - - Texture/Core - - Texture/Core (3.0.0) - - Texture/MapKit (3.0.0): - - Texture/Core - - Texture/Photos (3.0.0): - - Texture/Core - - Texture/PINRemoteImage (3.0.0): - - PINRemoteImage/iOS (~> 3.0.0) - - PINRemoteImage/PINCache - - Texture/Core - - Texture/Video (3.0.0): - - Texture/Core - "UITextField+Shake (1.2.1)" DEPENDENCIES: @@ -47,7 +12,6 @@ DEPENDENCIES: - Kanna (~> 5.2.2) - Keys (from `Pods/CocoaPodsKeys`) - SwiftGen (~> 6.4.0) - - Texture (~> 3.0.0) - "UITextField+Shake (~> 1.2)" SPEC REPOS: @@ -55,11 +19,7 @@ SPEC REPOS: - DateToolsSwift - FLEX - Kanna - - PINCache - - PINOperation - - PINRemoteImage - SwiftGen - - Texture - "UITextField+Shake" EXTERNAL SOURCES: @@ -71,13 +31,9 @@ SPEC CHECKSUMS: FLEX: 7ca2c8cd3a435ff501ff6d2f2141e9bdc934eaab Kanna: 01cfbddc127f5ff0963692f285fcbc8a9d62d234 Keys: a576f4c9c1c641ca913a959a9c62ed3f215a8de9 - PINCache: 7a8fc1a691173d21dbddbf86cd515de6efa55086 - PINOperation: 00c935935f1e8cf0d1e2d6b542e75b88fc3e5e20 - PINRemoteImage: f1295b29f8c5e640e25335a1b2bd9d805171bd01 SwiftGen: 67860cc7c3cfc2ed25b9b74cfd55495fc89f9108 - Texture: 2f109e937850d94d1d07232041c9c7313ccddb81 "UITextField+Shake": 298ac5a0f239d731bdab999b19b628c956ca0ac3 -PODFILE CHECKSUM: 4db0bdf969729c5758bd923e33d9e097cb892086 +PODFILE CHECKSUM: 37aa3ed14a767c806ece40b6c99ab3c59b9f8475 COCOAPODS: 1.11.2 diff --git a/ShareActionExtension/Info.plist b/ShareActionExtension/Info.plist index 1b302547..ae948488 100644 --- a/ShareActionExtension/Info.plist +++ b/ShareActionExtension/Info.plist @@ -17,9 +17,9 @@ CFBundlePackageType $(PRODUCT_BUNDLE_PACKAGE_TYPE) CFBundleShortVersionString - 1.2.0 + 1.3.0 CFBundleVersion - 88 + 90 NSExtension NSExtensionAttributes