forked from zelo72/mastodon-ios
Merge branch 'release/0.7.1'
This commit is contained in:
commit
5cc240e13e
|
@ -187,7 +187,7 @@
|
|||
"blocking_warning": "You can’t view Artbot’s profile\n until you unblock them.\nYour account looks like this to them.",
|
||||
"blocked_warning": "You can’t view Artbot’s profile\n until they unblock you.",
|
||||
"suspended_warning": "This account has been suspended.",
|
||||
"user_suspended_warning": "%s's account has been suspended."
|
||||
"user_suspended_warning": "%s’s account has been suspended."
|
||||
},
|
||||
"accessibility": {
|
||||
"count_replies": "%s replies",
|
||||
|
@ -290,7 +290,7 @@
|
|||
},
|
||||
"special": {
|
||||
"username_invalid": "Username must only contain alphanumeric characters and underscores",
|
||||
"username_too_long": "Username is too long (can't be longer than 30 characters)",
|
||||
"username_too_long": "Username is too long (can’t be longer than 30 characters)",
|
||||
"email_invalid": "This is not a valid e-mail address",
|
||||
"password_too_short": "Password is too short (must be at least 8 characters)"
|
||||
}
|
||||
|
@ -299,7 +299,7 @@
|
|||
"server_rules": {
|
||||
"title": "Some ground rules.",
|
||||
"subtitle": "These rules are set by the admins of %s.",
|
||||
"prompt": "By continuing, you're subject to the terms of service and privacy policy for %s.",
|
||||
"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",
|
||||
"button": {
|
||||
|
@ -351,13 +351,13 @@
|
|||
"photo_library": "Photo Library",
|
||||
"browse": "Browse"
|
||||
},
|
||||
"content_input_placeholder": "Type or paste what's on your mind",
|
||||
"content_input_placeholder": "Type or paste what’s on your mind",
|
||||
"compose_action": "Publish",
|
||||
"replying_to_user": "replying to %s",
|
||||
"attachment": {
|
||||
"photo": "photo",
|
||||
"video": "video",
|
||||
"attachment_broken": "This %s is broken and can't be\nuploaded to Mastodon.",
|
||||
"attachment_broken": "This %s is broken and can’t be\nuploaded to Mastodon.",
|
||||
"description_photo": "Describe photo for low vision people...",
|
||||
"description_video": "Describe what’s happening for low vision people..."
|
||||
},
|
||||
|
@ -382,7 +382,8 @@
|
|||
},
|
||||
"auto_complete": {
|
||||
"single_people_talking": "%ld people talking",
|
||||
"multiple_people_talking": "%ld people talking"
|
||||
"multiple_people_talking": "%ld people talking",
|
||||
"space_to_add": "Space to add"
|
||||
},
|
||||
"accessibility": {
|
||||
"append_attachment": "Append attachment",
|
||||
|
|
|
@ -28,6 +28,7 @@
|
|||
0FB3D31E25E534C700AAD544 /* PickServerCategoryCollectionViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0FB3D31D25E534C700AAD544 /* PickServerCategoryCollectionViewCell.swift */; };
|
||||
0FB3D33225E5F50E00AAD544 /* PickServerSearchCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0FB3D33125E5F50E00AAD544 /* PickServerSearchCell.swift */; };
|
||||
0FB3D33825E6401400AAD544 /* PickServerCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0FB3D33725E6401400AAD544 /* PickServerCell.swift */; };
|
||||
164F0EBC267D4FE400249499 /* BoopSound.caf in Resources */ = {isa = PBXBuildFile; fileRef = 164F0EBB267D4FE400249499 /* BoopSound.caf */; };
|
||||
18BC7629F65E6DB12CB8416D /* Pods_Mastodon_MastodonUITests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 3C030226D3C73DCC23D67452 /* Pods_Mastodon_MastodonUITests.framework */; };
|
||||
2D04F42525C255B9003F936F /* APIService+PublicTimeline.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D04F42425C255B9003F936F /* APIService+PublicTimeline.swift */; };
|
||||
2D084B8D26258EA3003AA3AF /* NotificationViewModel+diffable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D084B8C26258EA3003AA3AF /* NotificationViewModel+diffable.swift */; };
|
||||
|
@ -183,6 +184,7 @@
|
|||
DB00CA972632DDB600A54956 /* CommonOSLog in Frameworks */ = {isa = PBXBuildFile; productRef = DB00CA962632DDB600A54956 /* CommonOSLog */; };
|
||||
DB0140BD25C40D7500F9F3CF /* CommonOSLog in Frameworks */ = {isa = PBXBuildFile; productRef = DB0140BC25C40D7500F9F3CF /* CommonOSLog */; };
|
||||
DB0140CF25C42AEE00F9F3CF /* OSLog.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB0140CE25C42AEE00F9F3CF /* OSLog.swift */; };
|
||||
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 */; };
|
||||
|
@ -202,6 +204,9 @@
|
|||
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 */; };
|
||||
DB1EE7AE267F3071000CC337 /* MastodonStatusContent+ParseResult.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB1EE7AD267F3071000CC337 /* MastodonStatusContent+ParseResult.swift */; };
|
||||
DB1EE7B0267F3088000CC337 /* MastodonStatusContent+Appearance.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB1EE7AF267F3088000CC337 /* MastodonStatusContent+Appearance.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 */; };
|
||||
|
@ -407,6 +412,16 @@
|
|||
DBA9443E265CFA6400C537E1 /* ProfileFieldCollectionViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBA9443D265CFA6400C537E1 /* ProfileFieldCollectionViewCell.swift */; };
|
||||
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 */; };
|
||||
DBAC648A267DC355007FE9FD /* NSDiffableDataSourceSnapshot.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBAC6489267DC355007FE9FD /* NSDiffableDataSourceSnapshot.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 */; };
|
||||
DBAE3F822615DDA3004B8251 /* ProfileViewController+UserProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBAE3F812615DDA3004B8251 /* ProfileViewController+UserProvider.swift */; };
|
||||
DBAE3F882615DDF4004B8251 /* UserProviderFacade.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBAE3F872615DDF4004B8251 /* UserProviderFacade.swift */; };
|
||||
|
@ -442,6 +457,15 @@
|
|||
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 */; };
|
||||
DBCC3B30261440A50045B23D /* UITabBarController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBCC3B2F261440A50045B23D /* UITabBarController.swift */; };
|
||||
|
@ -595,6 +619,7 @@
|
|||
0FB3D31D25E534C700AAD544 /* PickServerCategoryCollectionViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PickServerCategoryCollectionViewCell.swift; sourceTree = "<group>"; };
|
||||
0FB3D33125E5F50E00AAD544 /* PickServerSearchCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PickServerSearchCell.swift; sourceTree = "<group>"; };
|
||||
0FB3D33725E6401400AAD544 /* PickServerCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PickServerCell.swift; sourceTree = "<group>"; };
|
||||
164F0EBB267D4FE400249499 /* BoopSound.caf */ = {isa = PBXFileReference; lastKnownFileType = file; path = BoopSound.caf; sourceTree = "<group>"; };
|
||||
2D04F42425C255B9003F936F /* APIService+PublicTimeline.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "APIService+PublicTimeline.swift"; sourceTree = "<group>"; };
|
||||
2D084B8C26258EA3003AA3AF /* NotificationViewModel+diffable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NotificationViewModel+diffable.swift"; sourceTree = "<group>"; };
|
||||
2D084B9226259545003AA3AF /* NotificationViewModel+LoadLatestState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NotificationViewModel+LoadLatestState.swift"; sourceTree = "<group>"; };
|
||||
|
@ -733,6 +758,7 @@
|
|||
5D03938F2612D259007FE196 /* WebViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WebViewController.swift; sourceTree = "<group>"; };
|
||||
5D0393952612D266007FE196 /* WebViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WebViewModel.swift; sourceTree = "<group>"; };
|
||||
5DA732CB2629CEF500A92342 /* UIView+Remove.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIView+Remove.swift"; sourceTree = "<group>"; };
|
||||
5DA82A9B4ABDAFA3AB9A49C7 /* Pods-MastodonTests.asdk.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-MastodonTests.asdk.xcconfig"; path = "Target Support Files/Pods-MastodonTests/Pods-MastodonTests.asdk.xcconfig"; sourceTree = "<group>"; };
|
||||
5DDDF1922617442700311060 /* Mastodon+Entity+Account.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Mastodon+Entity+Account.swift"; sourceTree = "<group>"; };
|
||||
5DDDF1982617447F00311060 /* Mastodon+Entity+Tag.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Mastodon+Entity+Tag.swift"; sourceTree = "<group>"; };
|
||||
5DDDF1A82617489F00311060 /* Mastodon+Entity+History.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Mastodon+Entity+History.swift"; sourceTree = "<group>"; };
|
||||
|
@ -744,18 +770,21 @@
|
|||
5DF1058425F88AE500D6C0D4 /* NeedsDependency+AVPlayerViewControllerDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NeedsDependency+AVPlayerViewControllerDelegate.swift"; sourceTree = "<group>"; };
|
||||
5DFC35DE262068D20045711D /* SearchViewController+Follow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "SearchViewController+Follow.swift"; sourceTree = "<group>"; };
|
||||
75E3471C898DDD9631729B6E /* Pods-Mastodon.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Mastodon.release.xcconfig"; path = "Target Support Files/Pods-Mastodon/Pods-Mastodon.release.xcconfig"; sourceTree = "<group>"; };
|
||||
819CEC9DCAD8E8E7BD85A7BB /* Pods-Mastodon.asdk.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Mastodon.asdk.xcconfig"; path = "Target Support Files/Pods-Mastodon/Pods-Mastodon.asdk.xcconfig"; sourceTree = "<group>"; };
|
||||
8ED8C4B1F1BA2DCFF2926BB1 /* Pods-Mastodon-NotificationService.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Mastodon-NotificationService.debug.xcconfig"; path = "Target Support Files/Pods-Mastodon-NotificationService/Pods-Mastodon-NotificationService.debug.xcconfig"; sourceTree = "<group>"; };
|
||||
9553C689FFA9EBC880CAB78D /* Pods-NotificationService.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-NotificationService.debug.xcconfig"; path = "Target Support Files/Pods-NotificationService/Pods-NotificationService.debug.xcconfig"; sourceTree = "<group>"; };
|
||||
9776D7C4B79101CF70181127 /* Pods-NotificationService.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-NotificationService.release.xcconfig"; path = "Target Support Files/Pods-NotificationService/Pods-NotificationService.release.xcconfig"; sourceTree = "<group>"; };
|
||||
9780A4C98FFC65B32B50D1C0 /* Pods-MastodonTests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-MastodonTests.release.xcconfig"; path = "Target Support Files/Pods-MastodonTests/Pods-MastodonTests.release.xcconfig"; sourceTree = "<group>"; };
|
||||
9A0982D8F349244EB558CDFD /* Pods-AppShared.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-AppShared.debug.xcconfig"; path = "Target Support Files/Pods-AppShared/Pods-AppShared.debug.xcconfig"; sourceTree = "<group>"; };
|
||||
A4ABE34829701A4496C5BB64 /* Pods_Mastodon.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Mastodon.framework; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
A67FD038ECDA0E411AF8DB4D /* Pods-Mastodon-MastodonUITests.asdk.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Mastodon-MastodonUITests.asdk.xcconfig"; path = "Target Support Files/Pods-Mastodon-MastodonUITests/Pods-Mastodon-MastodonUITests.asdk.xcconfig"; sourceTree = "<group>"; };
|
||||
B31D44635FCF6452F7E1B865 /* Pods-Mastodon-AppShared.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Mastodon-AppShared.release.xcconfig"; path = "Target Support Files/Pods-Mastodon-AppShared/Pods-Mastodon-AppShared.release.xcconfig"; sourceTree = "<group>"; };
|
||||
B44342AC2B6585F8295F1DDF /* Pods-Mastodon-NotificationService.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Mastodon-NotificationService.release.xcconfig"; path = "Target Support Files/Pods-Mastodon-NotificationService/Pods-Mastodon-NotificationService.release.xcconfig"; sourceTree = "<group>"; };
|
||||
BB482D32A7B9825BF5327C4F /* Pods-Mastodon-MastodonUITests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Mastodon-MastodonUITests.release.xcconfig"; path = "Target Support Files/Pods-Mastodon-MastodonUITests/Pods-Mastodon-MastodonUITests.release.xcconfig"; sourceTree = "<group>"; };
|
||||
CD92E0F10BDE4FE7C4B999F2 /* Pods_MastodonTests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_MastodonTests.framework; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
D7D7CF93E262178800077512 /* Pods-Mastodon-AppShared.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Mastodon-AppShared.debug.xcconfig"; path = "Target Support Files/Pods-Mastodon-AppShared/Pods-Mastodon-AppShared.debug.xcconfig"; sourceTree = "<group>"; };
|
||||
DB0140CE25C42AEE00F9F3CF /* OSLog.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OSLog.swift; sourceTree = "<group>"; };
|
||||
DB023294267F0AB800031745 /* ASMetaEditableTextNode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ASMetaEditableTextNode.swift; sourceTree = "<group>"; };
|
||||
DB029E94266A20430062874E /* MastodonAuthenticationController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MastodonAuthenticationController.swift; sourceTree = "<group>"; };
|
||||
DB02CDAA26256A9500D0A2AF /* ThreadReplyLoaderTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ThreadReplyLoaderTableViewCell.swift; sourceTree = "<group>"; };
|
||||
DB02CDBE2625AE5000D0A2AF /* AdaptiveUserInterfaceStyleBarButtonItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AdaptiveUserInterfaceStyleBarButtonItem.swift; sourceTree = "<group>"; };
|
||||
|
@ -777,6 +806,9 @@
|
|||
DB1D84372657B275000346B3 /* SegmentedControlNavigateable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SegmentedControlNavigateable.swift; sourceTree = "<group>"; };
|
||||
DB1E346725F518E20079D7DF /* CategoryPickerSection.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CategoryPickerSection.swift; sourceTree = "<group>"; };
|
||||
DB1E347725F519300079D7DF /* PickServerItem.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PickServerItem.swift; sourceTree = "<group>"; };
|
||||
DB1EE7AD267F3071000CC337 /* MastodonStatusContent+ParseResult.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "MastodonStatusContent+ParseResult.swift"; sourceTree = "<group>"; };
|
||||
DB1EE7AF267F3088000CC337 /* MastodonStatusContent+Appearance.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "MastodonStatusContent+Appearance.swift"; sourceTree = "<group>"; };
|
||||
DB1EE7B1267F9525000CC337 /* StatusProvider+StatusNodeDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "StatusProvider+StatusNodeDelegate.swift"; sourceTree = "<group>"; };
|
||||
DB1FD43525F26899004CFCFC /* MastodonPickServerViewModel+LoadIndexedServerState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "MastodonPickServerViewModel+LoadIndexedServerState.swift"; sourceTree = "<group>"; };
|
||||
DB1FD44325F26CCC004CFCFC /* PickServerSection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PickServerSection.swift; sourceTree = "<group>"; };
|
||||
DB1FD44F25F26FA1004CFCFC /* MastodonPickServerViewModel+Diffable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "MastodonPickServerViewModel+Diffable.swift"; sourceTree = "<group>"; };
|
||||
|
@ -981,6 +1013,13 @@
|
|||
DBA9443D265CFA6400C537E1 /* ProfileFieldCollectionViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfileFieldCollectionViewCell.swift; sourceTree = "<group>"; };
|
||||
DBA9443F265D137600C537E1 /* Mastodon+Entity+Field.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Mastodon+Entity+Field.swift"; sourceTree = "<group>"; };
|
||||
DBABE3EB25ECAC4B00879EE5 /* WelcomeIllustrationView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WelcomeIllustrationView.swift; sourceTree = "<group>"; };
|
||||
DBAC6484267D0F9E007FE9FD /* StatusNode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatusNode.swift; sourceTree = "<group>"; };
|
||||
DBAC6487267D388B007FE9FD /* ASTableNode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ASTableNode.swift; sourceTree = "<group>"; };
|
||||
DBAC6489267DC355007FE9FD /* NSDiffableDataSourceSnapshot.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NSDiffableDataSourceSnapshot.swift; sourceTree = "<group>"; };
|
||||
DBAC648E267DC84D007FE9FD /* TableNodeDiffableDataSource.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TableNodeDiffableDataSource.swift; sourceTree = "<group>"; };
|
||||
DBAC6496267DECCB007FE9FD /* TimelineMiddleLoaderNode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineMiddleLoaderNode.swift; sourceTree = "<group>"; };
|
||||
DBAC6498267DF2C4007FE9FD /* TimelineBottomLoaderNode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineBottomLoaderNode.swift; sourceTree = "<group>"; };
|
||||
DBAC649A267DF8C8007FE9FD /* ActivityIndicatorNode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ActivityIndicatorNode.swift; sourceTree = "<group>"; };
|
||||
DBAE3F672615DD60004B8251 /* UserProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserProvider.swift; sourceTree = "<group>"; };
|
||||
DBAE3F812615DDA3004B8251 /* ProfileViewController+UserProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ProfileViewController+UserProvider.swift"; sourceTree = "<group>"; };
|
||||
DBAE3F872615DDF4004B8251 /* UserProviderFacade.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserProviderFacade.swift; sourceTree = "<group>"; };
|
||||
|
@ -1014,6 +1053,15 @@
|
|||
DBC7A671260C897100E57475 /* StatusContentWarningEditorView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatusContentWarningEditorView.swift; sourceTree = "<group>"; };
|
||||
DBC7A67B260DFADE00E57475 /* StatusPublishService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatusPublishService.swift; sourceTree = "<group>"; };
|
||||
DBCBCBF3267CB070000F5B51 /* Decode85.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Decode85.swift; sourceTree = "<group>"; };
|
||||
DBCBCBFB2680ADB7000F5B51 /* AsyncHomeTimelineViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AsyncHomeTimelineViewController.swift; sourceTree = "<group>"; };
|
||||
DBCBCBFE2680AE98000F5B51 /* AsyncHomeTimelineViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AsyncHomeTimelineViewModel.swift; sourceTree = "<group>"; };
|
||||
DBCBCC002680AF2A000F5B51 /* AsyncHomeTimelineViewModel+Diffable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "AsyncHomeTimelineViewModel+Diffable.swift"; sourceTree = "<group>"; };
|
||||
DBCBCC022680AF6E000F5B51 /* AsyncHomeTimelineViewController+DebugAction.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "AsyncHomeTimelineViewController+DebugAction.swift"; sourceTree = "<group>"; };
|
||||
DBCBCC042680AFB9000F5B51 /* AsyncHomeTimelineViewController+Provider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "AsyncHomeTimelineViewController+Provider.swift"; sourceTree = "<group>"; };
|
||||
DBCBCC062680AFEC000F5B51 /* AsyncHomeTimelineViewModel+LoadLatestState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "AsyncHomeTimelineViewModel+LoadLatestState.swift"; sourceTree = "<group>"; };
|
||||
DBCBCC082680B01B000F5B51 /* AsyncHomeTimelineViewModel+LoadMiddleState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "AsyncHomeTimelineViewModel+LoadMiddleState.swift"; sourceTree = "<group>"; };
|
||||
DBCBCC0A2680B03F000F5B51 /* AsyncHomeTimelineViewModel+LoadOldestState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "AsyncHomeTimelineViewModel+LoadOldestState.swift"; sourceTree = "<group>"; };
|
||||
DBCBCC0C2680B908000F5B51 /* HomeTimelinePreference.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomeTimelinePreference.swift; sourceTree = "<group>"; };
|
||||
DBCBED1626132DB500B49291 /* UserTimelineViewModel+Diffable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UserTimelineViewModel+Diffable.swift"; sourceTree = "<group>"; };
|
||||
DBCBED1C26132E1A00B49291 /* StatusFetchedResultsController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatusFetchedResultsController.swift; sourceTree = "<group>"; };
|
||||
DBCC3B2F261440A50045B23D /* UITabBarController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UITabBarController.swift; sourceTree = "<group>"; };
|
||||
|
@ -1045,6 +1093,8 @@
|
|||
DBF9814B265E339500E4BA07 /* ProfileFieldAddEntryCollectionViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfileFieldAddEntryCollectionViewCell.swift; sourceTree = "<group>"; };
|
||||
EC6E707B68A67DB08EC288FA /* Pods-MastodonTests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-MastodonTests.debug.xcconfig"; path = "Target Support Files/Pods-MastodonTests/Pods-MastodonTests.debug.xcconfig"; sourceTree = "<group>"; };
|
||||
ECA373ABA86BE3C2D7ED878E /* Pods-AppShared.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-AppShared.release.xcconfig"; path = "Target Support Files/Pods-AppShared/Pods-AppShared.release.xcconfig"; sourceTree = "<group>"; };
|
||||
EE13214BC0246BE5210CCC10 /* Pods-AppShared.asdk.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-AppShared.asdk.xcconfig"; path = "Target Support Files/Pods-AppShared/Pods-AppShared.asdk.xcconfig"; sourceTree = "<group>"; };
|
||||
F31E7502A7E3945B98C6CBAF /* Pods-NotificationService.asdk.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-NotificationService.asdk.xcconfig"; path = "Target Support Files/Pods-NotificationService/Pods-NotificationService.asdk.xcconfig"; sourceTree = "<group>"; };
|
||||
F4A2A2D7000E477CA459ADA9 /* Pods_AppShared.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_AppShared.framework; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
/* End PBXFileReference section */
|
||||
|
||||
|
@ -1065,8 +1115,11 @@
|
|||
DB6804862637CD4C00430867 /* AppShared.framework in Frameworks */,
|
||||
DB5086B825CC0D6400C2C187 /* Kingfisher in Frameworks */,
|
||||
DBF96326262EC0A6001D8D25 /* AuthenticationServices.framework in Frameworks */,
|
||||
DBAC6483267D0B21007FE9FD /* DifferenceKit in Frameworks */,
|
||||
2D61336925C18A4F00CAE157 /* AlamofireNetworkActivityIndicator in Frameworks */,
|
||||
DBAC64A1267E6D02007FE9FD /* Fuzi in Frameworks */,
|
||||
DB3D0FF325BAA61700EAA174 /* AlamofireImage in Frameworks */,
|
||||
DBAC649E267DFE43007FE9FD /* DiffableDataSources in Frameworks */,
|
||||
2D5981BA25E4D7F8000FB903 /* ThirdPartyMailer in Frameworks */,
|
||||
87FFDA5D898A5C42ADCB35E7 /* Pods_Mastodon.framework in Frameworks */,
|
||||
DB6804C82637CE2F00430867 /* AppShared.framework in Frameworks */,
|
||||
|
@ -1213,6 +1266,11 @@
|
|||
ECA373ABA86BE3C2D7ED878E /* Pods-AppShared.release.xcconfig */,
|
||||
9553C689FFA9EBC880CAB78D /* Pods-NotificationService.debug.xcconfig */,
|
||||
9776D7C4B79101CF70181127 /* Pods-NotificationService.release.xcconfig */,
|
||||
EE13214BC0246BE5210CCC10 /* Pods-AppShared.asdk.xcconfig */,
|
||||
819CEC9DCAD8E8E7BD85A7BB /* Pods-Mastodon.asdk.xcconfig */,
|
||||
A67FD038ECDA0E411AF8DB4D /* Pods-Mastodon-MastodonUITests.asdk.xcconfig */,
|
||||
5DA82A9B4ABDAFA3AB9A49C7 /* Pods-MastodonTests.asdk.xcconfig */,
|
||||
F31E7502A7E3945B98C6CBAF /* Pods-NotificationService.asdk.xcconfig */,
|
||||
);
|
||||
path = Pods;
|
||||
sourceTree = "<group>";
|
||||
|
@ -1263,6 +1321,7 @@
|
|||
isa = PBXGroup;
|
||||
children = (
|
||||
DB1F239626117C360057430E /* View */,
|
||||
DBCBCBFD2680ADBA000F5B51 /* AsyncHomeTimeline */,
|
||||
2D38F1D425CD465300561493 /* HomeTimelineViewController.swift */,
|
||||
2D38F1DE25CD46A400561493 /* HomeTimelineViewController+Provider.swift */,
|
||||
2D5A3D6125CFD9CB002347D6 /* HomeTimelineViewController+DebugAction.swift */,
|
||||
|
@ -1281,6 +1340,7 @@
|
|||
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 */,
|
||||
|
@ -1337,6 +1397,7 @@
|
|||
DB51D171262832380062B7A1 /* BlurHashEncode.swift */,
|
||||
DB6180EC26391C6C0018D199 /* TransitioningMath.swift */,
|
||||
DB75BF1D263C1C1B00EDBF1F /* CustomScheduler.swift */,
|
||||
DBAC649A267DF8C8007FE9FD /* ActivityIndicatorNode.swift */,
|
||||
);
|
||||
path = Vender;
|
||||
sourceTree = "<group>";
|
||||
|
@ -1414,6 +1475,7 @@
|
|||
2D76319D25C151F600929FB9 /* Section */,
|
||||
2D7631B125C159E700929FB9 /* Item */,
|
||||
DBCBED2226132E1D00B49291 /* FetchedResultsController */,
|
||||
DBAC6490267DC84F007FE9FD /* DataSource */,
|
||||
);
|
||||
path = Diffiable;
|
||||
sourceTree = "<group>";
|
||||
|
@ -1464,6 +1526,7 @@
|
|||
DB87D45C2609DE6600D12C0D /* TextField */,
|
||||
DB1D187125EF5BBD003F1F23 /* TableView */,
|
||||
2D7631A625C1533800929FB9 /* TableviewCell */,
|
||||
DBAC6486267D0FAC007FE9FD /* Node */,
|
||||
);
|
||||
path = View;
|
||||
sourceTree = "<group>";
|
||||
|
@ -1633,6 +1696,16 @@
|
|||
path = Onboarding;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
DB023296267F0ABE00031745 /* Status */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
DBAC6484267D0F9E007FE9FD /* StatusNode.swift */,
|
||||
DBAC6496267DECCB007FE9FD /* TimelineMiddleLoaderNode.swift */,
|
||||
DBAC6498267DF2C4007FE9FD /* TimelineBottomLoaderNode.swift */,
|
||||
);
|
||||
path = Status;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
DB084B5125CBC56300F898ED /* CoreDataStack */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
|
@ -1680,6 +1753,7 @@
|
|||
DB3D0FF825BAA6B200EAA174 /* Resources */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
164F0EBB267D4FE400249499 /* BoopSound.caf */,
|
||||
DB427DDE25BAA00100D1B89D /* Assets.xcassets */,
|
||||
DB118A8125E4B6E600FAB162 /* Preview Assets.xcassets */,
|
||||
DB3D100F25BAA75E00EAA174 /* Localizable.strings */,
|
||||
|
@ -1832,6 +1906,7 @@
|
|||
DB6D1B3C2636857500ACB481 /* AppearancePreference.swift */,
|
||||
DBE54AC52636C89F004E7C0B /* NotificationPreference.swift */,
|
||||
DB1D842F26566512000346B3 /* KeyboardPreference.swift */,
|
||||
DBCBCC0C2680B908000F5B51 /* HomeTimelinePreference.swift */,
|
||||
);
|
||||
path = Preference;
|
||||
sourceTree = "<group>";
|
||||
|
@ -2167,6 +2242,7 @@
|
|||
DBCC3B35261440BA0045B23D /* UINavigationController.swift */,
|
||||
DB6D1B23263684C600ACB481 /* UserDefaults.swift */,
|
||||
DB97131E2666078B00BD1E90 /* Date.swift */,
|
||||
DBAC6489267DC355007FE9FD /* NSDiffableDataSourceSnapshot.swift */,
|
||||
);
|
||||
path = Extension;
|
||||
sourceTree = "<group>";
|
||||
|
@ -2285,6 +2361,8 @@
|
|||
isa = PBXGroup;
|
||||
children = (
|
||||
2D42FF6A25C817D2004A627A /* MastodonStatusContent.swift */,
|
||||
DB1EE7AD267F3071000CC337 /* MastodonStatusContent+ParseResult.swift */,
|
||||
DB1EE7AF267F3088000CC337 /* MastodonStatusContent+Appearance.swift */,
|
||||
DB6F5E2E264E5518009108F4 /* MastodonRegex.swift */,
|
||||
DB35FC2E26130172006193C9 /* MastodonField.swift */,
|
||||
DBAE3FA82617106E004B8251 /* MastodonMetricFormatter.swift */,
|
||||
|
@ -2328,6 +2406,24 @@
|
|||
path = View;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
DBAC6486267D0FAC007FE9FD /* Node */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
DB023296267F0ABE00031745 /* Status */,
|
||||
DB023294267F0AB800031745 /* ASMetaEditableTextNode.swift */,
|
||||
);
|
||||
path = Node;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
DBAC6490267DC84F007FE9FD /* DataSource */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
DBAC6487267D388B007FE9FD /* ASTableNode.swift */,
|
||||
DBAC648E267DC84D007FE9FD /* TableNodeDiffableDataSource.swift */,
|
||||
);
|
||||
path = DataSource;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
DBAE3F742615DD63004B8251 /* UserProvider */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
|
@ -2409,6 +2505,21 @@
|
|||
path = Cell;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
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 = "<group>";
|
||||
};
|
||||
DBCBED2226132E1D00B49291 /* FetchedResultsController */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
|
@ -2510,6 +2621,9 @@
|
|||
DBB525072611EAC0002F1F29 /* Tabman */,
|
||||
DB6F5E31264E7410009108F4 /* TwitterTextEditor */,
|
||||
DBAEDE5E267A0B1500D25FF5 /* Nuke */,
|
||||
DBAC6482267D0B21007FE9FD /* DifferenceKit */,
|
||||
DBAC649D267DFE43007FE9FD /* DiffableDataSources */,
|
||||
DBAC64A0267E6D02007FE9FD /* Fuzi */,
|
||||
);
|
||||
productName = Mastodon;
|
||||
productReference = DB427DD225BAA00100D1B89D /* Mastodon.app */;
|
||||
|
@ -2698,6 +2812,9 @@
|
|||
DB6804722637CC1200430867 /* XCRemoteSwiftPackageReference "KeychainAccess" */,
|
||||
DB6F5E30264E7410009108F4 /* XCRemoteSwiftPackageReference "TwitterTextEditor" */,
|
||||
DBAEDE5D267A0B1500D25FF5 /* XCRemoteSwiftPackageReference "Nuke" */,
|
||||
DBAC6481267D0B21007FE9FD /* XCRemoteSwiftPackageReference "DifferenceKit" */,
|
||||
DBAC649C267DFE43007FE9FD /* XCRemoteSwiftPackageReference "DiffableDataSources" */,
|
||||
DBAC649F267E6D01007FE9FD /* XCRemoteSwiftPackageReference "Fuzi" */,
|
||||
);
|
||||
productRefGroup = DB427DD325BAA00100D1B89D /* Products */;
|
||||
projectDirPath = "";
|
||||
|
@ -2719,6 +2836,7 @@
|
|||
isa = PBXResourcesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
164F0EBC267D4FE400249499 /* BoopSound.caf in Resources */,
|
||||
DB427DE225BAA00100D1B89D /* LaunchScreen.storyboard in Resources */,
|
||||
DB3D100D25BAA75E00EAA174 /* Localizable.strings in Resources */,
|
||||
DB427DDF25BAA00100D1B89D /* Assets.xcassets in Resources */,
|
||||
|
@ -2953,6 +3071,7 @@
|
|||
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 */,
|
||||
|
@ -2992,9 +3111,11 @@
|
|||
DB68A06325E905E000CFDF14 /* UIApplication.swift in Sources */,
|
||||
DB02CDAB26256A9500D0A2AF /* ThreadReplyLoaderTableViewCell.swift in Sources */,
|
||||
DBB5255E2611F07A002F1F29 /* ProfileViewModel.swift in Sources */,
|
||||
DBAC648F267DC84D007FE9FD /* TableNodeDiffableDataSource.swift in Sources */,
|
||||
2D8434FB25FF46B300EECE90 /* HomeTimelineNavigationBarTitleView.swift in Sources */,
|
||||
0F1E2D0B2615C39400C38565 /* DoubleTitleLabelNavigationBarTitleView.swift in Sources */,
|
||||
DB9A488A26034D40008B817C /* ComposeViewModel+PublishState.swift in Sources */,
|
||||
DBAC649B267DF8C8007FE9FD /* ActivityIndicatorNode.swift in Sources */,
|
||||
DB45FAD725CA6C76005A8AC7 /* UIBarButtonItem.swift in Sources */,
|
||||
2DA504692601ADE7008F4E6C /* SawToothView.swift in Sources */,
|
||||
DB87D4572609DD5300D12C0D /* DeleteBackwardResponseTextField.swift in Sources */,
|
||||
|
@ -3036,6 +3157,7 @@
|
|||
DB0AC6FC25CD02E600D75117 /* APIService+Instance.swift in Sources */,
|
||||
2D69D00A25CAA00300C3A1B2 /* APIService+CoreData+Status.swift in Sources */,
|
||||
DB4481C625EE2ADA00BEFB67 /* PollSection.swift in Sources */,
|
||||
DBAC648A267DC355007FE9FD /* NSDiffableDataSourceSnapshot.swift in Sources */,
|
||||
DBCBED1726132DB500B49291 /* UserTimelineViewModel+Diffable.swift in Sources */,
|
||||
DB71FD4C25F8C80E00512AE1 /* StatusPrefetchingService.swift in Sources */,
|
||||
2DE0FACE2615F7AD00CDF649 /* RecommendAccountSection.swift in Sources */,
|
||||
|
@ -3054,9 +3176,12 @@
|
|||
DB4481B925EE289600BEFB67 /* UITableView.swift in Sources */,
|
||||
2D35238126256F690031AF25 /* NotificationTableViewCell.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 */,
|
||||
|
@ -3104,6 +3229,7 @@
|
|||
2DE0FAC12615F04D00CDF649 /* RecommendHashTagSection.swift in Sources */,
|
||||
DBA5E7A5263BD28C004598BB /* ContextMenuImagePreviewViewModel.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 */,
|
||||
|
@ -3129,6 +3255,7 @@
|
|||
5B90C45F262599800002E742 /* SettingsToggleTableViewCell.swift in Sources */,
|
||||
2D694A7425F9EB4E0038ADDC /* ContentWarningOverlayView.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 */,
|
||||
|
@ -3159,6 +3286,7 @@
|
|||
DB1D186C25EF5BA7003F1F23 /* PollTableView.swift in Sources */,
|
||||
DBA94434265CBB5300C537E1 /* ProfileFieldSection.swift in Sources */,
|
||||
DB6F5E2F264E5518009108F4 /* MastodonRegex.swift in Sources */,
|
||||
DB023295267F0AB800031745 /* ASMetaEditableTextNode.swift in Sources */,
|
||||
2D5981A125E4A593000FB903 /* MastodonConfirmEmailViewModel.swift in Sources */,
|
||||
DB87D4452609BE0500D12C0D /* ComposeStatusPollOptionCollectionViewCell.swift in Sources */,
|
||||
DB6D9F7D26358ED4008423CD /* SettingsSection.swift in Sources */,
|
||||
|
@ -3175,6 +3303,7 @@
|
|||
DBAE3F822615DDA3004B8251 /* ProfileViewController+UserProvider.swift in Sources */,
|
||||
DB938EED2623F79B00E5B6C1 /* ThreadViewModel.swift in Sources */,
|
||||
DBC7A67C260DFADE00E57475 /* StatusPublishService.swift in Sources */,
|
||||
DBCBCC092680B01B000F5B51 /* AsyncHomeTimelineViewModel+LoadMiddleState.swift in Sources */,
|
||||
2DCB73FD2615C13900EC03D4 /* SearchRecommendCollectionHeader.swift in Sources */,
|
||||
DB02CDBF2625AE5000D0A2AF /* AdaptiveUserInterfaceStyleBarButtonItem.swift in Sources */,
|
||||
DB1FD44425F26CCC004CFCFC /* PickServerSection.swift in Sources */,
|
||||
|
@ -3187,6 +3316,7 @@
|
|||
0FB3D33825E6401400AAD544 /* PickServerCell.swift in Sources */,
|
||||
DB6D9F8426358EEC008423CD /* SettingsItem.swift in Sources */,
|
||||
2D364F7825E66D8300204FDC /* MastodonResendEmailViewModel.swift in Sources */,
|
||||
DBCBCC052680AFB9000F5B51 /* AsyncHomeTimelineViewController+Provider.swift in Sources */,
|
||||
DB8AF54525C13647002E6C99 /* NeedsDependency.swift in Sources */,
|
||||
DB9D6BF825E4F5690051B173 /* NotificationViewController.swift in Sources */,
|
||||
2DAC9E46262FC9FD0062E1A6 /* SuggestionAccountTableViewCell.swift in Sources */,
|
||||
|
@ -3218,6 +3348,7 @@
|
|||
DBAE3FA92617106E004B8251 /* MastodonMetricFormatter.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 */,
|
||||
|
@ -3228,14 +3359,17 @@
|
|||
DB427DD625BAA00100D1B89D /* AppDelegate.swift in Sources */,
|
||||
DB6D9F57263577D2008423CD /* APIService+CoreData+Setting.swift in Sources */,
|
||||
DB9D6C0E25E4F9780051B173 /* MosaicImageViewContainer.swift in Sources */,
|
||||
DBCBCC0D2680B908000F5B51 /* HomeTimelinePreference.swift in Sources */,
|
||||
DB71FD3625F8A16C00512AE1 /* APIService+Persist+PersistMemo.swift in Sources */,
|
||||
DBBE1B4525F3474B0081417A /* MastodonPickServerAppearance.swift in Sources */,
|
||||
DB98338725C945ED00AD9700 /* Strings.swift in Sources */,
|
||||
2D7867192625B77500211898 /* NotificationItem.swift in Sources */,
|
||||
DB1EE7AE267F3071000CC337 /* MastodonStatusContent+ParseResult.swift in Sources */,
|
||||
DB45FAB625CA5485005A8AC7 /* UIAlertController.swift in Sources */,
|
||||
DBE0821525CD382600FD6BBD /* MastodonRegisterViewController.swift in Sources */,
|
||||
2D5A3D0325CF8742002347D6 /* ControlContainableScrollViews.swift in Sources */,
|
||||
DB98336B25C9420100AD9700 /* APIService+App.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 */,
|
||||
|
@ -3248,6 +3382,7 @@
|
|||
DB45FAF925CA80A2005A8AC7 /* APIService+CoreData+MastodonAuthentication.swift in Sources */,
|
||||
2D04F42525C255B9003F936F /* APIService+PublicTimeline.swift in Sources */,
|
||||
2D34D9E226149C920081BFC0 /* SearchRecommendTagsCollectionViewCell.swift in Sources */,
|
||||
DB1EE7B0267F3088000CC337 /* MastodonStatusContent+Appearance.swift in Sources */,
|
||||
2D76317D25C14DF500929FB9 /* PublicTimelineViewController+Provider.swift in Sources */,
|
||||
0F20223326145E51000C64BF /* HashtagTimelineViewModel+LoadMiddleState.swift in Sources */,
|
||||
2D206B8025F5F45E00143C56 /* UIImage.swift in Sources */,
|
||||
|
@ -3262,6 +3397,7 @@
|
|||
5DFC35DF262068D20045711D /* SearchViewController+Follow.swift in Sources */,
|
||||
DB8AF52E25C13561002E6C99 /* ViewStateStore.swift in Sources */,
|
||||
2DA7D04A25CA52CB00804E11 /* TimelineBottomLoaderTableViewCell.swift in Sources */,
|
||||
DBAC6499267DF2C4007FE9FD /* TimelineBottomLoaderNode.swift in Sources */,
|
||||
2D76318325C14E8F00929FB9 /* PublicTimelineViewModel+Diffable.swift in Sources */,
|
||||
DBBF1DBF2652401B00E5B703 /* AutoCompleteViewModel.swift in Sources */,
|
||||
DB6180FA26391F2E0018D199 /* MediaPreviewViewModel.swift in Sources */,
|
||||
|
@ -3286,6 +3422,7 @@
|
|||
DB9A489026035963008B817C /* APIService+Media.swift in Sources */,
|
||||
2D198649261C0B8500F0B013 /* SearchResultSection.swift in Sources */,
|
||||
DBB525302611EBF3002F1F29 /* ProfilePagingViewModel.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 */,
|
||||
|
@ -3301,6 +3438,7 @@
|
|||
DB938F3326243D6200E5B6C1 /* TimelineTopLoaderTableViewCell.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 */,
|
||||
5BB04FF5262F0E6D0043BFF6 /* ReportSection.swift in Sources */,
|
||||
DBA94436265CBB7400C537E1 /* ProfileFieldItem.swift in Sources */,
|
||||
|
@ -3630,7 +3768,7 @@
|
|||
CLANG_ENABLE_MODULES = YES;
|
||||
CODE_SIGN_ENTITLEMENTS = Mastodon/Mastodon.entitlements;
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 14;
|
||||
CURRENT_PROJECT_VERSION = 16;
|
||||
DEVELOPMENT_ASSET_PATHS = "Mastodon/Resources/Preview\\ Assets.xcassets";
|
||||
DEVELOPMENT_TEAM = 5Z4GVSS33P;
|
||||
INFOPLIST_FILE = Mastodon/Info.plist;
|
||||
|
@ -3638,7 +3776,7 @@
|
|||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
);
|
||||
MARKETING_VERSION = 0.7.0;
|
||||
MARKETING_VERSION = 0.7.1;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = org.joinmastodon.app;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
PROVISIONING_PROFILE_SPECIFIER = "";
|
||||
|
@ -3657,7 +3795,7 @@
|
|||
CLANG_ENABLE_MODULES = YES;
|
||||
CODE_SIGN_ENTITLEMENTS = Mastodon/Mastodon.entitlements;
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 14;
|
||||
CURRENT_PROJECT_VERSION = 16;
|
||||
DEVELOPMENT_ASSET_PATHS = "Mastodon/Resources/Preview\\ Assets.xcassets";
|
||||
DEVELOPMENT_TEAM = 5Z4GVSS33P;
|
||||
INFOPLIST_FILE = Mastodon/Info.plist;
|
||||
|
@ -3665,7 +3803,7 @@
|
|||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
);
|
||||
MARKETING_VERSION = 0.7.0;
|
||||
MARKETING_VERSION = 0.7.1;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = org.joinmastodon.app;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
PROVISIONING_PROFILE_SPECIFIER = "";
|
||||
|
@ -3914,13 +4052,192 @@
|
|||
};
|
||||
name = Release;
|
||||
};
|
||||
DBF8AE1C263293E400C9C23C /* Debug */ = {
|
||||
DBCBCC0E2680BE3E000F5B51 /* ASDK */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
baseConfigurationReference = 9553C689FFA9EBC880CAB78D /* Pods-NotificationService.debug.xcconfig */;
|
||||
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;
|
||||
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;
|
||||
};
|
||||
DBCBCC0F2680BE3E000F5B51 /* ASDK */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
baseConfigurationReference = 819CEC9DCAD8E8E7BD85A7BB /* Pods-Mastodon.asdk.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 = 16;
|
||||
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 = 0.7.1;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = org.joinmastodon.app;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
PROVISIONING_PROFILE_SPECIFIER = "";
|
||||
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
|
||||
SWIFT_VERSION = 5.0;
|
||||
TARGETED_DEVICE_FAMILY = "1,2";
|
||||
};
|
||||
name = ASDK;
|
||||
};
|
||||
DBCBCC102680BE3E000F5B51 /* ASDK */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
baseConfigurationReference = 5DA82A9B4ABDAFA3AB9A49C7 /* Pods-MastodonTests.asdk.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;
|
||||
};
|
||||
DBCBCC112680BE3E000F5B51 /* ASDK */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
baseConfigurationReference = A67FD038ECDA0E411AF8DB4D /* Pods-Mastodon-MastodonUITests.asdk.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;
|
||||
};
|
||||
DBCBCC122680BE3E000F5B51 /* ASDK */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 1;
|
||||
DEFINES_MODULE = YES;
|
||||
DEVELOPMENT_TEAM = 5Z4GVSS33P;
|
||||
DYLIB_COMPATIBILITY_VERSION = 1;
|
||||
DYLIB_CURRENT_VERSION = 1;
|
||||
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;
|
||||
};
|
||||
DBCBCC132680BE3E000F5B51 /* ASDK */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES;
|
||||
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;
|
||||
};
|
||||
DBCBCC142680BE3E000F5B51 /* ASDK */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
baseConfigurationReference = F31E7502A7E3945B98C6CBAF /* Pods-NotificationService.asdk.xcconfig */;
|
||||
buildSettings = {
|
||||
CODE_SIGN_ENTITLEMENTS = NotificationService/NotificationService.entitlements;
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 14;
|
||||
CURRENT_PROJECT_VERSION = 16;
|
||||
DEVELOPMENT_TEAM = 5Z4GVSS33P;
|
||||
INFOPLIST_FILE = NotificationService/Info.plist;
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
|
@ -3928,7 +4245,61 @@
|
|||
"@executable_path/Frameworks",
|
||||
"@executable_path/../../Frameworks",
|
||||
);
|
||||
MARKETING_VERSION = 0.7.0;
|
||||
MARKETING_VERSION = 0.7.1;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = org.joinmastodon.app.NotificationService;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SKIP_INSTALL = YES;
|
||||
SWIFT_VERSION = 5.0;
|
||||
TARGETED_DEVICE_FAMILY = "1,2";
|
||||
};
|
||||
name = ASDK;
|
||||
};
|
||||
DBCBCC152680BE3E000F5B51 /* ASDK */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
baseConfigurationReference = EE13214BC0246BE5210CCC10 /* Pods-AppShared.asdk.xcconfig */;
|
||||
buildSettings = {
|
||||
APPLICATION_EXTENSION_API_ONLY = YES;
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 1;
|
||||
DEFINES_MODULE = YES;
|
||||
DEVELOPMENT_TEAM = 5Z4GVSS33P;
|
||||
DYLIB_COMPATIBILITY_VERSION = 1;
|
||||
DYLIB_CURRENT_VERSION = 1;
|
||||
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;
|
||||
};
|
||||
DBF8AE1C263293E400C9C23C /* Debug */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
baseConfigurationReference = 9553C689FFA9EBC880CAB78D /* Pods-NotificationService.debug.xcconfig */;
|
||||
buildSettings = {
|
||||
CODE_SIGN_ENTITLEMENTS = NotificationService/NotificationService.entitlements;
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 16;
|
||||
DEVELOPMENT_TEAM = 5Z4GVSS33P;
|
||||
INFOPLIST_FILE = NotificationService/Info.plist;
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
"@executable_path/../../Frameworks",
|
||||
);
|
||||
MARKETING_VERSION = 0.7.1;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = org.joinmastodon.app.NotificationService;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SKIP_INSTALL = YES;
|
||||
|
@ -3943,7 +4314,7 @@
|
|||
buildSettings = {
|
||||
CODE_SIGN_ENTITLEMENTS = NotificationService/NotificationService.entitlements;
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 14;
|
||||
CURRENT_PROJECT_VERSION = 16;
|
||||
DEVELOPMENT_TEAM = 5Z4GVSS33P;
|
||||
INFOPLIST_FILE = NotificationService/Info.plist;
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
|
@ -3951,7 +4322,7 @@
|
|||
"@executable_path/Frameworks",
|
||||
"@executable_path/../../Frameworks",
|
||||
);
|
||||
MARKETING_VERSION = 0.7.0;
|
||||
MARKETING_VERSION = 0.7.1;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = org.joinmastodon.app.NotificationService;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SKIP_INSTALL = YES;
|
||||
|
@ -3967,6 +4338,7 @@
|
|||
isa = XCConfigurationList;
|
||||
buildConfigurations = (
|
||||
DB427DFA25BAA00100D1B89D /* Debug */,
|
||||
DBCBCC0E2680BE3E000F5B51 /* ASDK */,
|
||||
DB427DFB25BAA00100D1B89D /* Release */,
|
||||
);
|
||||
defaultConfigurationIsVisible = 0;
|
||||
|
@ -3976,6 +4348,7 @@
|
|||
isa = XCConfigurationList;
|
||||
buildConfigurations = (
|
||||
DB427DFD25BAA00100D1B89D /* Debug */,
|
||||
DBCBCC0F2680BE3E000F5B51 /* ASDK */,
|
||||
DB427DFE25BAA00100D1B89D /* Release */,
|
||||
);
|
||||
defaultConfigurationIsVisible = 0;
|
||||
|
@ -3985,6 +4358,7 @@
|
|||
isa = XCConfigurationList;
|
||||
buildConfigurations = (
|
||||
DB427E0025BAA00100D1B89D /* Debug */,
|
||||
DBCBCC102680BE3E000F5B51 /* ASDK */,
|
||||
DB427E0125BAA00100D1B89D /* Release */,
|
||||
);
|
||||
defaultConfigurationIsVisible = 0;
|
||||
|
@ -3994,6 +4368,7 @@
|
|||
isa = XCConfigurationList;
|
||||
buildConfigurations = (
|
||||
DB427E0325BAA00100D1B89D /* Debug */,
|
||||
DBCBCC112680BE3E000F5B51 /* ASDK */,
|
||||
DB427E0425BAA00100D1B89D /* Release */,
|
||||
);
|
||||
defaultConfigurationIsVisible = 0;
|
||||
|
@ -4003,6 +4378,7 @@
|
|||
isa = XCConfigurationList;
|
||||
buildConfigurations = (
|
||||
DB6804892637CD4C00430867 /* Debug */,
|
||||
DBCBCC152680BE3E000F5B51 /* ASDK */,
|
||||
DB68048A2637CD4C00430867 /* Release */,
|
||||
);
|
||||
defaultConfigurationIsVisible = 0;
|
||||
|
@ -4012,6 +4388,7 @@
|
|||
isa = XCConfigurationList;
|
||||
buildConfigurations = (
|
||||
DB89BA0625C10FD0008580ED /* Debug */,
|
||||
DBCBCC122680BE3E000F5B51 /* ASDK */,
|
||||
DB89BA0725C10FD0008580ED /* Release */,
|
||||
);
|
||||
defaultConfigurationIsVisible = 0;
|
||||
|
@ -4021,6 +4398,7 @@
|
|||
isa = XCConfigurationList;
|
||||
buildConfigurations = (
|
||||
DB89BA0A25C10FD0008580ED /* Debug */,
|
||||
DBCBCC132680BE3E000F5B51 /* ASDK */,
|
||||
DB89BA0B25C10FD0008580ED /* Release */,
|
||||
);
|
||||
defaultConfigurationIsVisible = 0;
|
||||
|
@ -4030,6 +4408,7 @@
|
|||
isa = XCConfigurationList;
|
||||
buildConfigurations = (
|
||||
DBF8AE1C263293E400C9C23C /* Debug */,
|
||||
DBCBCC142680BE3E000F5B51 /* ASDK */,
|
||||
DBF8AE1D263293E400C9C23C /* Release */,
|
||||
);
|
||||
defaultConfigurationIsVisible = 0;
|
||||
|
@ -4118,6 +4497,30 @@
|
|||
minimumVersion = 1.4.1;
|
||||
};
|
||||
};
|
||||
DBAC6481267D0B21007FE9FD /* XCRemoteSwiftPackageReference "DifferenceKit" */ = {
|
||||
isa = XCRemoteSwiftPackageReference;
|
||||
repositoryURL = "https://github.com/ra1028/DifferenceKit.git";
|
||||
requirement = {
|
||||
kind = exactVersion;
|
||||
version = 1.2.0;
|
||||
};
|
||||
};
|
||||
DBAC649C267DFE43007FE9FD /* XCRemoteSwiftPackageReference "DiffableDataSources" */ = {
|
||||
isa = XCRemoteSwiftPackageReference;
|
||||
repositoryURL = "https://github.com/MainasuK/DiffableDataSources.git";
|
||||
requirement = {
|
||||
branch = "feature/async-display-table";
|
||||
kind = branch;
|
||||
};
|
||||
};
|
||||
DBAC649F267E6D01007FE9FD /* XCRemoteSwiftPackageReference "Fuzi" */ = {
|
||||
isa = XCRemoteSwiftPackageReference;
|
||||
repositoryURL = "https://github.com/cezheng/Fuzi.git";
|
||||
requirement = {
|
||||
kind = upToNextMajorVersion;
|
||||
minimumVersion = 3.1.3;
|
||||
};
|
||||
};
|
||||
DBAEDE5D267A0B1500D25FF5 /* XCRemoteSwiftPackageReference "Nuke" */ = {
|
||||
isa = XCRemoteSwiftPackageReference;
|
||||
repositoryURL = "https://github.com/kean/Nuke.git";
|
||||
|
@ -4201,6 +4604,21 @@
|
|||
package = DB9A487C2603456B008B817C /* XCRemoteSwiftPackageReference "UITextView-Placeholder" */;
|
||||
productName = "UITextView+Placeholder";
|
||||
};
|
||||
DBAC6482267D0B21007FE9FD /* DifferenceKit */ = {
|
||||
isa = XCSwiftPackageProductDependency;
|
||||
package = DBAC6481267D0B21007FE9FD /* XCRemoteSwiftPackageReference "DifferenceKit" */;
|
||||
productName = DifferenceKit;
|
||||
};
|
||||
DBAC649D267DFE43007FE9FD /* DiffableDataSources */ = {
|
||||
isa = XCSwiftPackageProductDependency;
|
||||
package = DBAC649C267DFE43007FE9FD /* XCRemoteSwiftPackageReference "DiffableDataSources" */;
|
||||
productName = DiffableDataSources;
|
||||
};
|
||||
DBAC64A0267E6D02007FE9FD /* Fuzi */ = {
|
||||
isa = XCSwiftPackageProductDependency;
|
||||
package = DBAC649F267E6D01007FE9FD /* XCRemoteSwiftPackageReference "Fuzi" */;
|
||||
productName = Fuzi;
|
||||
};
|
||||
DBAEDE5E267A0B1500D25FF5 /* Nuke */ = {
|
||||
isa = XCSwiftPackageProductDependency;
|
||||
package = DBAEDE5D267A0B1500D25FF5 /* XCRemoteSwiftPackageReference "Nuke" */;
|
||||
|
|
|
@ -12,7 +12,12 @@
|
|||
<key>CoreDataStack.xcscheme_^#shared#^_</key>
|
||||
<dict>
|
||||
<key>orderHint</key>
|
||||
<integer>17</integer>
|
||||
<integer>31</integer>
|
||||
</dict>
|
||||
<key>Mastodon - ASDK.xcscheme_^#shared#^_</key>
|
||||
<dict>
|
||||
<key>orderHint</key>
|
||||
<integer>3</integer>
|
||||
</dict>
|
||||
<key>Mastodon - RTL.xcscheme_^#shared#^_</key>
|
||||
<dict>
|
||||
|
@ -32,7 +37,7 @@
|
|||
<key>NotificationService.xcscheme_^#shared#^_</key>
|
||||
<dict>
|
||||
<key>orderHint</key>
|
||||
<integer>16</integer>
|
||||
<integer>30</integer>
|
||||
</dict>
|
||||
</dict>
|
||||
<key>SuppressBuildableAutocreation</key>
|
||||
|
|
|
@ -15,8 +15,8 @@
|
|||
"repositoryURL": "https://github.com/Alamofire/Alamofire.git",
|
||||
"state": {
|
||||
"branch": null,
|
||||
"revision": "f96b619bcb2383b43d898402283924b80e2c4bae",
|
||||
"version": "5.4.3"
|
||||
"revision": "4d19ad82f80cc71ff829b941ded114c56f4f604c",
|
||||
"version": "5.4.2"
|
||||
}
|
||||
},
|
||||
{
|
||||
|
@ -46,6 +46,33 @@
|
|||
"version": "0.1.1"
|
||||
}
|
||||
},
|
||||
{
|
||||
"package": "DiffableDataSources",
|
||||
"repositoryURL": "https://github.com/MainasuK/DiffableDataSources.git",
|
||||
"state": {
|
||||
"branch": "feature/async-display-table",
|
||||
"revision": "73393a97690959d24387c95594c045c62d9c47cf",
|
||||
"version": null
|
||||
}
|
||||
},
|
||||
{
|
||||
"package": "DifferenceKit",
|
||||
"repositoryURL": "https://github.com/ra1028/DifferenceKit.git",
|
||||
"state": {
|
||||
"branch": null,
|
||||
"revision": "62745d7780deef4a023a792a1f8f763ec7bf9705",
|
||||
"version": "1.2.0"
|
||||
}
|
||||
},
|
||||
{
|
||||
"package": "Fuzi",
|
||||
"repositoryURL": "https://github.com/cezheng/Fuzi.git",
|
||||
"state": {
|
||||
"branch": null,
|
||||
"revision": "f08c8323da21e985f3772610753bcfc652c2103f",
|
||||
"version": "3.1.3"
|
||||
}
|
||||
},
|
||||
{
|
||||
"package": "KeychainAccess",
|
||||
"repositoryURL": "https://github.com/kishikawakatsumi/KeychainAccess.git",
|
||||
|
|
|
@ -48,6 +48,9 @@ extension SceneCoordinator {
|
|||
case mastodonResendEmail(viewModel: MastodonResendEmailViewModel)
|
||||
case mastodonWebView(viewModel:WebViewModel)
|
||||
|
||||
// ASDK
|
||||
case asyncHome
|
||||
|
||||
// compose
|
||||
case compose(viewModel: ComposeViewModel)
|
||||
|
||||
|
@ -236,6 +239,9 @@ private extension SceneCoordinator {
|
|||
let _viewController = WebViewController()
|
||||
_viewController.viewModel = viewModel
|
||||
viewController = _viewController
|
||||
case .asyncHome:
|
||||
let _viewController = AsyncHomeTimelineViewController()
|
||||
viewController = _viewController
|
||||
case .compose(let viewModel):
|
||||
let _viewController = ComposeViewController()
|
||||
_viewController.viewModel = viewModel
|
||||
|
|
|
@ -0,0 +1,81 @@
|
|||
//
|
||||
// ASTableNode.swift
|
||||
// Mastodon
|
||||
//
|
||||
// Created by Cirno MainasuK on 2021-6-19.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
import AsyncDisplayKit
|
||||
import DifferenceKit
|
||||
import DiffableDataSources
|
||||
|
||||
extension ASTableNode: ReloadableTableView {
|
||||
public func reload<C>(
|
||||
using stagedChangeset: StagedChangeset<C>,
|
||||
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<C>) -> 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()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,111 @@
|
|||
//
|
||||
// TableNodeDiffableDataSource.swift
|
||||
// Mastodon
|
||||
//
|
||||
// Created by Cirno MainasuK on 2021-6-19.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
import AsyncDisplayKit
|
||||
import DiffableDataSources
|
||||
|
||||
open class TableNodeDiffableDataSource<SectionIdentifierType: Hashable, ItemIdentifierType: Hashable>: 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<SectionIdentifierType, ItemIdentifierType>()
|
||||
|
||||
/// 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.dataSource = 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<SectionIdentifierType, ItemIdentifierType>, 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<SectionIdentifierType, ItemIdentifierType> {
|
||||
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
|
||||
}
|
||||
}
|
|
@ -10,6 +10,7 @@ import CoreData
|
|||
import CoreDataStack
|
||||
import Foundation
|
||||
import MastodonSDK
|
||||
import DifferenceKit
|
||||
|
||||
/// Note: update Equatable when change case
|
||||
enum Item {
|
||||
|
@ -158,3 +159,5 @@ extension Item: Hashable {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension Item: Differentiable { }
|
||||
|
|
|
@ -33,7 +33,7 @@ extension AutoCompleteSection {
|
|||
return cell
|
||||
case .emoji(let emoji):
|
||||
let cell = tableView.dequeueReusableCell(withIdentifier: String(describing: AutoCompleteTableViewCell.self), for: indexPath) as! AutoCompleteTableViewCell
|
||||
configureEmoji(cell: cell, emoji: emoji)
|
||||
configureEmoji(cell: cell, emoji: emoji, isFirst: indexPath.row == 0)
|
||||
return cell
|
||||
case .bottomLoader:
|
||||
let cell = tableView.dequeueReusableCell(withIdentifier: String(describing: TimelineBottomLoaderTableViewCell.self), for: indexPath) as! TimelineBottomLoaderTableViewCell
|
||||
|
@ -80,8 +80,10 @@ extension AutoCompleteSection {
|
|||
cell.configure(with: AvatarConfigurableViewConfiguration(avatarImageURL: URL(string: account.avatar)))
|
||||
}
|
||||
|
||||
private static func configureEmoji(cell: AutoCompleteTableViewCell, emoji: Mastodon.Entity.Emoji) {
|
||||
private static func configureEmoji(cell: AutoCompleteTableViewCell, emoji: Mastodon.Entity.Emoji, isFirst: Bool) {
|
||||
cell.titleLabel.text = ":" + emoji.shortcode + ":"
|
||||
// FIXME: handle spacer enter to complete emoji
|
||||
// cell.subtitleLabel.text = isFirst ? L10n.Scene.Compose.AutoComplete.spaceToAdd : " "
|
||||
cell.subtitleLabel.text = " "
|
||||
cell.avatarImageView.isHidden = false
|
||||
cell.configure(with: AvatarConfigurableViewConfiguration(avatarImageURL: URL(string: emoji.url)))
|
||||
|
|
|
@ -11,6 +11,7 @@ import CoreDataStack
|
|||
import os.log
|
||||
import UIKit
|
||||
import AVKit
|
||||
import AsyncDisplayKit
|
||||
import Nuke
|
||||
|
||||
protocol StatusCell: DisposeBagCollectable {
|
||||
|
@ -23,6 +24,33 @@ enum StatusSection: Equatable, Hashable {
|
|||
}
|
||||
|
||||
extension StatusSection {
|
||||
static func tableNodeDiffableDataSource(
|
||||
tableNode: ASTableNode,
|
||||
managedObjectContext: NSManagedObjectContext
|
||||
) -> TableNodeDiffableDataSource<StatusSection, Item> {
|
||||
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() }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
static func tableViewDiffableDataSource(
|
||||
for tableView: UITableView,
|
||||
dependency: NeedsDependency,
|
||||
|
@ -47,14 +75,18 @@ extension StatusSection {
|
|||
|
||||
// configure cell
|
||||
managedObjectContext.performAndWait {
|
||||
let timelineIndex = managedObjectContext.object(with: objectID) as! HomeTimelineIndex
|
||||
let timelineIndex = managedObjectContext.object(with: objectID) as? HomeTimelineIndex
|
||||
// note: force check optional for status
|
||||
// status maybe <uninitialized> here when delete in thread scene
|
||||
guard let status = timelineIndex?.status,
|
||||
let userID = timelineIndex?.userID else { return }
|
||||
StatusSection.configure(
|
||||
cell: cell,
|
||||
dependency: dependency,
|
||||
readableLayoutFrame: tableView.readableContentGuide.layoutFrame,
|
||||
timestampUpdatePublisher: timestampUpdatePublisher,
|
||||
status: timelineIndex.status,
|
||||
requestUserID: timelineIndex.userID,
|
||||
status: status,
|
||||
requestUserID: userID,
|
||||
statusItemAttribute: attribute
|
||||
)
|
||||
}
|
||||
|
@ -752,12 +784,13 @@ extension StatusSection {
|
|||
return L10n.Common.Controls.Timeline.Accessibility.countReblogs(status.favouritesCount.intValue)
|
||||
}()
|
||||
Publishers.CombineLatest(
|
||||
dependency.context.blockDomainService.blockedDomains,
|
||||
dependency.context.blockDomainService.blockedDomains.setFailureType(to: ManagedObjectObserver.Error.self),
|
||||
ManagedObjectObserver.observe(object: status.authorForUserProvider)
|
||||
.assertNoFailure()
|
||||
)
|
||||
)
|
||||
.receive(on: RunLoop.main)
|
||||
.sink { [weak dependency, weak cell] _, change in
|
||||
.sink(receiveCompletion: { _ in
|
||||
// do nothing
|
||||
}, receiveValue: { [weak dependency, weak cell] _, change in
|
||||
guard let cell = cell else { return }
|
||||
guard let dependency = dependency else { return }
|
||||
switch change.changeType {
|
||||
|
@ -769,7 +802,7 @@ extension StatusSection {
|
|||
break
|
||||
}
|
||||
StatusSection.setupStatusMoreButtonMenu(cell: cell, dependency: dependency, status: status)
|
||||
}
|
||||
})
|
||||
.store(in: &cell.disposeBag)
|
||||
self.setupStatusMoreButtonMenu(cell: cell, dependency: dependency, status: status)
|
||||
}
|
||||
|
|
|
@ -0,0 +1,24 @@
|
|||
//
|
||||
// NSDiffableDataSourceSnapshot.swift
|
||||
// Mastodon
|
||||
//
|
||||
// Created by Cirno MainasuK on 2021-6-19.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
|
||||
//extension NSDiffableDataSourceSnapshot {
|
||||
// func itemIdentifier(for indexPath: IndexPath) -> ItemIdentifierType? {
|
||||
// guard 0..<numberOfSections ~= indexPath.section else {
|
||||
// return nil
|
||||
// }
|
||||
//
|
||||
// let items = itemIdentifiers(inSection: sectionIdentifiers[indexPath.section])
|
||||
//
|
||||
// guard 0..<items.endIndex ~= indexPath.item else {
|
||||
// return nil
|
||||
// }
|
||||
//
|
||||
// return items[indexPath.item]
|
||||
// }
|
||||
//}
|
|
@ -367,7 +367,7 @@ internal enum L10n {
|
|||
internal static let noStatusFound = L10n.tr("Localizable", "Common.Controls.Timeline.Header.NoStatusFound")
|
||||
/// This account has been suspended.
|
||||
internal static let suspendedWarning = L10n.tr("Localizable", "Common.Controls.Timeline.Header.SuspendedWarning")
|
||||
/// %@'s account has been suspended.
|
||||
/// %@’s account has been suspended.
|
||||
internal static func userSuspendedWarning(_ p1: Any) -> String {
|
||||
return L10n.tr("Localizable", "Common.Controls.Timeline.Header.UserSuspendedWarning", String(describing: p1))
|
||||
}
|
||||
|
@ -404,7 +404,7 @@ internal enum L10n {
|
|||
internal enum Compose {
|
||||
/// Publish
|
||||
internal static let composeAction = L10n.tr("Localizable", "Scene.Compose.ComposeAction")
|
||||
/// Type or paste what's on your mind
|
||||
/// Type or paste what’s on your mind
|
||||
internal static let contentInputPlaceholder = L10n.tr("Localizable", "Scene.Compose.ContentInputPlaceholder")
|
||||
/// replying to %@
|
||||
internal static func replyingToUser(_ p1: Any) -> String {
|
||||
|
@ -435,7 +435,7 @@ internal enum L10n {
|
|||
internal static let removePoll = L10n.tr("Localizable", "Scene.Compose.Accessibility.RemovePoll")
|
||||
}
|
||||
internal enum Attachment {
|
||||
/// This %@ is broken and can't be\nuploaded to Mastodon.
|
||||
/// This %@ is broken and can’t be\nuploaded to Mastodon.
|
||||
internal static func attachmentBroken(_ p1: Any) -> String {
|
||||
return L10n.tr("Localizable", "Scene.Compose.Attachment.AttachmentBroken", String(describing: p1))
|
||||
}
|
||||
|
@ -457,6 +457,8 @@ internal enum L10n {
|
|||
internal static func singlePeopleTalking(_ p1: Int) -> String {
|
||||
return L10n.tr("Localizable", "Scene.Compose.AutoComplete.SinglePeopleTalking", p1)
|
||||
}
|
||||
/// Space to add
|
||||
internal static let spaceToAdd = L10n.tr("Localizable", "Scene.Compose.AutoComplete.SpaceToAdd")
|
||||
}
|
||||
internal enum ContentWarning {
|
||||
/// Write an accurate warning here...
|
||||
|
@ -756,7 +758,7 @@ internal enum L10n {
|
|||
internal static let passwordTooShort = L10n.tr("Localizable", "Scene.Register.Error.Special.PasswordTooShort")
|
||||
/// Username must only contain alphanumeric characters and underscores
|
||||
internal static let usernameInvalid = L10n.tr("Localizable", "Scene.Register.Error.Special.UsernameInvalid")
|
||||
/// Username is too long (can't be longer than 30 characters)
|
||||
/// Username is too long (can’t be longer than 30 characters)
|
||||
internal static let usernameTooLong = L10n.tr("Localizable", "Scene.Register.Error.Special.UsernameTooLong")
|
||||
}
|
||||
}
|
||||
|
@ -918,7 +920,7 @@ internal enum L10n {
|
|||
internal enum ServerRules {
|
||||
/// privacy policy
|
||||
internal static let privacyPolicy = L10n.tr("Localizable", "Scene.ServerRules.PrivacyPolicy")
|
||||
/// By continuing, you're subject to the terms of service and privacy policy for %@.
|
||||
/// By continuing, you’re subject to the terms of service and privacy policy for %@.
|
||||
internal static func prompt(_ p1: Any) -> String {
|
||||
return L10n.tr("Localizable", "Scene.ServerRules.Prompt", String(describing: p1))
|
||||
}
|
||||
|
|
|
@ -0,0 +1,17 @@
|
|||
//
|
||||
// MastodonStatusContent+Appearance.swift
|
||||
// Mastodon
|
||||
//
|
||||
// Created by Cirno MainasuK on 2021-6-20.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
|
||||
extension MastodonStatusContent {
|
||||
struct Appearance {
|
||||
let attributes: [NSAttributedString.Key: Any]
|
||||
let urlAttributes: [NSAttributedString.Key: Any]
|
||||
let hashtagAttributes: [NSAttributedString.Key: Any]
|
||||
let mentionAttributes: [NSAttributedString.Key: Any]
|
||||
}
|
||||
}
|
|
@ -0,0 +1,108 @@
|
|||
//
|
||||
// MastodonStatusContent+ParseResult.swift
|
||||
// Mastodon
|
||||
//
|
||||
// Created by Cirno MainasuK on 2021-6-20.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import ActiveLabel
|
||||
|
||||
extension MastodonStatusContent {
|
||||
struct ParseResult: Hashable {
|
||||
let document: String
|
||||
let original: String
|
||||
let trimmed: String
|
||||
let activeEntities: [ActiveEntity]
|
||||
|
||||
static func == (lhs: MastodonStatusContent.ParseResult, rhs: MastodonStatusContent.ParseResult) -> Bool {
|
||||
return lhs.document == rhs.document
|
||||
&& lhs.original == rhs.original
|
||||
&& lhs.trimmed == rhs.trimmed
|
||||
&& lhs.activeEntities.count == rhs.activeEntities.count // FIXME:
|
||||
}
|
||||
|
||||
func hash(into hasher: inout Hasher) {
|
||||
hasher.combine(document)
|
||||
hasher.combine(original)
|
||||
hasher.combine(trimmed)
|
||||
hasher.combine(activeEntities.count) // FIXME:
|
||||
}
|
||||
|
||||
func trimmedAttributedString(appearance: MastodonStatusContent.Appearance) -> NSAttributedString {
|
||||
let attributedString = NSMutableAttributedString(string: trimmed, attributes: appearance.attributes)
|
||||
for entity in activeEntities {
|
||||
switch entity.type {
|
||||
case .url:
|
||||
attributedString.addAttributes(appearance.urlAttributes, range: entity.range)
|
||||
case .hashtag:
|
||||
attributedString.addAttributes(appearance.hashtagAttributes, range: entity.range)
|
||||
case .mention:
|
||||
attributedString.addAttributes(appearance.mentionAttributes, range: entity.range)
|
||||
default:
|
||||
break
|
||||
}
|
||||
if let uri = entity.type.uri {
|
||||
attributedString.addAttributes([
|
||||
.link: uri
|
||||
], range: entity.range)
|
||||
}
|
||||
}
|
||||
return attributedString
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension ActiveEntityType {
|
||||
|
||||
static let appScheme = "mastodon"
|
||||
|
||||
init?(url: URL) {
|
||||
guard let scheme = url.scheme?.lowercased() else { return nil }
|
||||
guard scheme == ActiveEntityType.appScheme else {
|
||||
self = .url("", trimmed: "", url: url.absoluteString, userInfo: nil)
|
||||
return
|
||||
}
|
||||
|
||||
guard let components = URLComponents(url: url, resolvingAgainstBaseURL: true),
|
||||
let parameters = components.queryItems else { return nil }
|
||||
|
||||
if let hashtag = parameters.first(where: { $0.name == "hashtag" }), let encoded = hashtag.value, let value = String(base64Encoded: encoded) {
|
||||
self = .hashtag(value, userInfo: nil)
|
||||
return
|
||||
}
|
||||
if let mention = parameters.first(where: { $0.name == "mention" }), let encoded = mention.value, let value = String(base64Encoded: encoded) {
|
||||
self = .mention(value, userInfo: nil)
|
||||
return
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
var uri: URL? {
|
||||
switch self {
|
||||
case .url(_, _, let url, _):
|
||||
return URL(string: url)
|
||||
case .hashtag(let hashtag, _):
|
||||
return URL(string: "\(ActiveEntityType.appScheme)://meta?hashtag=\(hashtag.base64Encoded)")
|
||||
case .mention(let mention, _):
|
||||
return URL(string: "\(ActiveEntityType.appScheme)://meta?mention=\(mention.base64Encoded)")
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
extension String {
|
||||
fileprivate var base64Encoded: String {
|
||||
return Data(self.utf8).base64EncodedString()
|
||||
}
|
||||
|
||||
init?(base64Encoded: String) {
|
||||
guard let data = Data(base64Encoded: base64Encoded),
|
||||
let string = String(data: data, encoding: .utf8) else {
|
||||
return nil
|
||||
}
|
||||
self = string
|
||||
}
|
||||
}
|
|
@ -5,10 +5,10 @@
|
|||
// Created by MainasuK Cirno on 2021/2/1.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import UIKit
|
||||
import Combine
|
||||
import Kanna
|
||||
import ActiveLabel
|
||||
import Fuzi
|
||||
|
||||
enum MastodonStatusContent {
|
||||
|
||||
|
@ -125,30 +125,6 @@ extension String {
|
|||
}
|
||||
}
|
||||
|
||||
extension MastodonStatusContent {
|
||||
struct ParseResult: Hashable {
|
||||
let document: String
|
||||
let original: String
|
||||
let trimmed: String
|
||||
let activeEntities: [ActiveEntity]
|
||||
|
||||
static func == (lhs: MastodonStatusContent.ParseResult, rhs: MastodonStatusContent.ParseResult) -> Bool {
|
||||
return lhs.document == rhs.document
|
||||
&& lhs.original == rhs.original
|
||||
&& lhs.trimmed == rhs.trimmed
|
||||
&& lhs.activeEntities.count == rhs.activeEntities.count // FIXME:
|
||||
}
|
||||
|
||||
func hash(into hasher: inout Hasher) {
|
||||
hasher.combine(document)
|
||||
hasher.combine(original)
|
||||
hasher.combine(trimmed)
|
||||
hasher.combine(activeEntities.count) // FIXME:
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
extension MastodonStatusContent {
|
||||
|
||||
class Node {
|
||||
|
@ -165,7 +141,7 @@ extension MastodonStatusContent {
|
|||
}
|
||||
|
||||
let tagName: String?
|
||||
let classNames: Set<String>
|
||||
let attributes: [String : String]
|
||||
let href: String?
|
||||
let hrefEllipsis: String?
|
||||
|
||||
|
@ -175,56 +151,47 @@ extension MastodonStatusContent {
|
|||
level: Int,
|
||||
text: Substring,
|
||||
tagName: String?,
|
||||
className: String?,
|
||||
attributes: [String : String],
|
||||
href: String?,
|
||||
hrefEllipsis: String?,
|
||||
children: [Node]
|
||||
) {
|
||||
let _classNames: Set<String> = {
|
||||
guard let className = className else { return Set() }
|
||||
guard let className = attributes["class"] else { return Set() }
|
||||
return Set(className.components(separatedBy: " "))
|
||||
}()
|
||||
let _type: Type? = {
|
||||
if tagName == "a" && !_classNames.contains("mention") {
|
||||
return .url
|
||||
}
|
||||
|
||||
if _classNames.contains("mention") {
|
||||
if tagName == "a" {
|
||||
if _classNames.contains("u-url") {
|
||||
return .mention
|
||||
} else if _classNames.contains("hashtag") {
|
||||
}
|
||||
if _classNames.contains("hashtag") {
|
||||
return .hashtag
|
||||
}
|
||||
return .url
|
||||
} else {
|
||||
if _classNames.contains("emoji") {
|
||||
return .emoji
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
if _classNames.contains("emoji") {
|
||||
return .emoji
|
||||
}
|
||||
|
||||
return nil
|
||||
}()
|
||||
self.level = level
|
||||
self.type = _type
|
||||
self.text = text
|
||||
self.tagName = tagName
|
||||
self.classNames = _classNames
|
||||
self.attributes = attributes
|
||||
self.href = href
|
||||
self.hrefEllipsis = hrefEllipsis
|
||||
self.children = children
|
||||
}
|
||||
|
||||
static func parse(document: String) throws -> MastodonStatusContent.Node {
|
||||
let html = try HTML(html: document, encoding: .utf8)
|
||||
|
||||
// add `\r\n` explicit due to Kanna text missing it after convert to text
|
||||
// ref: https://github.com/tid-kijyun/Kanna/issues/150
|
||||
let brNodes = html.css("br").makeIterator()
|
||||
while let brNode = brNodes.next() {
|
||||
brNode.addNextSibling(try! HTML(html: "<span>\r\n</span>", encoding: .utf8).body!)
|
||||
}
|
||||
let document = document.replacingOccurrences(of: "<br>|<br />", with: "\r\n", options: .regularExpression, range: nil)
|
||||
let html = try HTMLDocument(string: document)
|
||||
|
||||
let body = html.body ?? nil
|
||||
let text = body?.text ?? ""
|
||||
let text = body?.stringValue ?? ""
|
||||
let level = 0
|
||||
let children: [MastodonStatusContent.Node] = body.flatMap { body in
|
||||
return Node.parse(element: body, parentText: text[...], parentLevel: level + 1)
|
||||
|
@ -232,8 +199,8 @@ extension MastodonStatusContent {
|
|||
let node = Node(
|
||||
level: level,
|
||||
text: text[...],
|
||||
tagName: body?.tagName,
|
||||
className: body?.className,
|
||||
tagName: body?.tag,
|
||||
attributes: body?.attributes ?? [:],
|
||||
href: nil,
|
||||
hrefEllipsis: nil,
|
||||
children: children
|
||||
|
@ -246,13 +213,11 @@ extension MastodonStatusContent {
|
|||
let parent = element
|
||||
let scanner = Scanner(string: String(parentText))
|
||||
scanner.charactersToBeSkipped = .none
|
||||
|
||||
var element = parent.at_css(":first-child")
|
||||
|
||||
var children: [Node] = []
|
||||
|
||||
while let _element = element {
|
||||
let _text = _element.text ?? ""
|
||||
|
||||
for _element in parent.children {
|
||||
let _text = _element.stringValue
|
||||
|
||||
// scan element text
|
||||
_ = scanner.scanUpToString(_text)
|
||||
let startIndexOffset = scanner.currentIndex.utf16Offset(in: scanner.string)
|
||||
|
@ -261,27 +226,26 @@ extension MastodonStatusContent {
|
|||
continue
|
||||
}
|
||||
let endIndexOffset = scanner.currentIndex.utf16Offset(in: scanner.string)
|
||||
|
||||
|
||||
// locate substring
|
||||
let startIndex = parentText.utf16.index(parentText.utf16.startIndex, offsetBy: startIndexOffset)
|
||||
let endIndex = parentText.utf16.index(parentText.utf16.startIndex, offsetBy: endIndexOffset)
|
||||
let text = Substring(parentText.utf16[startIndex..<endIndex])
|
||||
|
||||
|
||||
let href = _element["href"]
|
||||
let hrefEllipsis = href.flatMap { _ in _element.at_css(".ellipsis")?.text }
|
||||
|
||||
let hrefEllipsis = href.flatMap { _ in _element.firstChild(css: ".ellipsis")?.stringValue }
|
||||
|
||||
let level = parentLevel + 1
|
||||
let node = Node(
|
||||
level: level,
|
||||
text: text,
|
||||
tagName: _element.tagName,
|
||||
className: _element.className,
|
||||
tagName: _element.tag,
|
||||
attributes: _element.attributes,
|
||||
href: href,
|
||||
hrefEllipsis: hrefEllipsis,
|
||||
children: Node.parse(element: _element, parentText: text, parentLevel: level + 1)
|
||||
)
|
||||
children.append(node)
|
||||
element = _element.nextSibling
|
||||
}
|
||||
|
||||
return children
|
||||
|
@ -344,11 +308,8 @@ extension MastodonStatusContent.Node: CustomDebugStringConvertible {
|
|||
}
|
||||
}()
|
||||
let classNamesInfo: String = {
|
||||
guard !classNames.isEmpty else { return "" }
|
||||
let names = Array(classNames)
|
||||
.sorted()
|
||||
.joined(separator: ", ")
|
||||
return "@[\(names)]"
|
||||
guard let className = attributes["class"] else { return "" }
|
||||
return "@[\(className)]"
|
||||
}()
|
||||
let nodeDescription = String(
|
||||
format: "<%@>%@%@: %@",
|
||||
|
|
|
@ -0,0 +1,20 @@
|
|||
//
|
||||
// HomeTimelinePreference.swift
|
||||
// Mastodon
|
||||
//
|
||||
// Created by MainasuK Cirno on 2021-6-21.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
|
||||
extension UserDefaults {
|
||||
|
||||
@objc dynamic var preferAsyncHomeTimeline: Bool {
|
||||
get {
|
||||
register(defaults: [#function: false])
|
||||
return bool(forKey: #function)
|
||||
}
|
||||
set { self[#function] = newValue }
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,16 @@
|
|||
//
|
||||
// StatusProvider+StatusNodeDelegate.swift
|
||||
// Mastodon
|
||||
//
|
||||
// Created by Cirno MainasuK on 2021-6-20.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import ActiveLabel
|
||||
|
||||
// MARK: - StatusViewDelegate
|
||||
extension StatusNodeDelegate where Self: StatusProvider {
|
||||
func statusNode(_ node: StatusNode, statusContentTextNode: ASMetaEditableTextNode, didSelectActiveEntityType type: ActiveEntityType) {
|
||||
StatusProviderFacade.responseToStatusActiveLabelAction(provider: self, node: node, didSelectActiveEntityType: type)
|
||||
}
|
||||
}
|
|
@ -9,6 +9,7 @@ import UIKit
|
|||
import Combine
|
||||
import CoreData
|
||||
import CoreDataStack
|
||||
import AsyncDisplayKit
|
||||
|
||||
protocol StatusProvider: NeedsDependency & DisposeBagCollectable & UIViewController {
|
||||
// async
|
||||
|
@ -21,4 +22,12 @@ protocol StatusProvider: NeedsDependency & DisposeBagCollectable & UIViewControl
|
|||
var tableViewDiffableDataSource: UITableViewDiffableDataSource<StatusSection, Item>? { get }
|
||||
func item(for cell: UITableViewCell?, indexPath: IndexPath?) -> Item?
|
||||
func items(indexPaths: [IndexPath]) -> [Item]
|
||||
|
||||
func status(node: ASCellNode?, indexPath: IndexPath?) -> Status?
|
||||
}
|
||||
|
||||
extension StatusProvider {
|
||||
func status(node: ASCellNode?, indexPath: IndexPath?) -> Status? {
|
||||
fatalError("Needs implement this")
|
||||
}
|
||||
}
|
||||
|
|
|
@ -12,6 +12,7 @@ import CoreData
|
|||
import CoreDataStack
|
||||
import MastodonSDK
|
||||
import ActiveLabel
|
||||
import AsyncDisplayKit
|
||||
|
||||
enum StatusProviderFacade { }
|
||||
|
||||
|
@ -144,51 +145,85 @@ extension StatusProviderFacade {
|
|||
break
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
static func responseToStatusActiveLabelAction(provider: StatusProvider, node: ASCellNode, didSelectActiveEntityType type: ActiveEntityType) {
|
||||
switch type {
|
||||
case .hashtag(let text, _):
|
||||
let hashtagTimelienViewModel = HashtagTimelineViewModel(context: provider.context, hashtag: text)
|
||||
provider.coordinator.present(scene: .hashtagTimeline(viewModel: hashtagTimelienViewModel), from: provider, transition: .show)
|
||||
case .mention(let text, _):
|
||||
coordinateToStatusMentionProfileScene(for: .primary, provider: provider, node: node, mention: text)
|
||||
case .url(_, _, let url, _):
|
||||
guard let url = URL(string: url) else { return }
|
||||
if let domain = provider.context.authenticationService.activeMastodonAuthenticationBox.value?.domain, url.host == domain,
|
||||
url.pathComponents.count >= 4,
|
||||
url.pathComponents[0] == "/",
|
||||
url.pathComponents[1] == "web",
|
||||
url.pathComponents[2] == "statuses" {
|
||||
let statusID = url.pathComponents[3]
|
||||
let threadViewModel = RemoteThreadViewModel(context: provider.context, statusID: statusID)
|
||||
provider.coordinator.present(scene: .thread(viewModel: threadViewModel), from: nil, transition: .show)
|
||||
} else {
|
||||
provider.coordinator.present(scene: .safari(url: url), from: nil, transition: .safariPresent(animated: true, completion: nil))
|
||||
}
|
||||
default:
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
private static func coordinateToStatusMentionProfileScene(for target: Target, provider: StatusProvider, cell: UITableViewCell, mention: String) {
|
||||
guard let activeMastodonAuthenticationBox = provider.context.authenticationService.activeMastodonAuthenticationBox.value else { return }
|
||||
let domain = activeMastodonAuthenticationBox.domain
|
||||
|
||||
provider.status(for: cell, indexPath: nil)
|
||||
.sink { [weak provider] status in
|
||||
guard let provider = provider else { return }
|
||||
let _status: Status? = {
|
||||
switch target {
|
||||
case .primary: return status?.reblog ?? status
|
||||
case .secondary: return status
|
||||
}
|
||||
}()
|
||||
guard let status = _status else { return }
|
||||
|
||||
// cannot continue without meta
|
||||
guard let mentionMeta = (status.mentions ?? Set()).first(where: { $0.username == mention }) else { return }
|
||||
|
||||
let userID = mentionMeta.id
|
||||
|
||||
let profileViewModel: ProfileViewModel = {
|
||||
// check if self
|
||||
guard userID != activeMastodonAuthenticationBox.userID else {
|
||||
return MeProfileViewModel(context: provider.context)
|
||||
}
|
||||
|
||||
let request = MastodonUser.sortedFetchRequest
|
||||
request.fetchLimit = 1
|
||||
request.predicate = MastodonUser.predicate(domain: domain, id: userID)
|
||||
let mastodonUser = provider.context.managedObjectContext.safeFetch(request).first
|
||||
|
||||
if let mastodonUser = mastodonUser {
|
||||
return CachedProfileViewModel(context: provider.context, mastodonUser: mastodonUser)
|
||||
} else {
|
||||
return RemoteProfileViewModel(context: provider.context, userID: userID)
|
||||
}
|
||||
}()
|
||||
|
||||
DispatchQueue.main.async {
|
||||
provider.coordinator.present(scene: .profile(viewModel: profileViewModel), from: provider, transition: .show)
|
||||
}
|
||||
guard let status = status else { return }
|
||||
coordinateToStatusMentionProfileScene(for: target, provider: provider, status: status, mention: mention)
|
||||
}
|
||||
.store(in: &provider.disposeBag)
|
||||
}
|
||||
|
||||
private static func coordinateToStatusMentionProfileScene(for target: Target, provider: StatusProvider, status: Status, mention: String) {
|
||||
guard let activeMastodonAuthenticationBox = provider.context.authenticationService.activeMastodonAuthenticationBox.value else { return }
|
||||
let domain = activeMastodonAuthenticationBox.domain
|
||||
|
||||
let status: Status = {
|
||||
switch target {
|
||||
case .primary: return status.reblog ?? status
|
||||
case .secondary: return status
|
||||
}
|
||||
}()
|
||||
|
||||
// cannot continue without meta
|
||||
guard let mentionMeta = (status.mentions ?? Set()).first(where: { $0.username == mention }) else { return }
|
||||
|
||||
let userID = mentionMeta.id
|
||||
|
||||
let profileViewModel: ProfileViewModel = {
|
||||
// check if self
|
||||
guard userID != activeMastodonAuthenticationBox.userID else {
|
||||
return MeProfileViewModel(context: provider.context)
|
||||
}
|
||||
|
||||
let request = MastodonUser.sortedFetchRequest
|
||||
request.fetchLimit = 1
|
||||
request.predicate = MastodonUser.predicate(domain: domain, id: userID)
|
||||
let mastodonUser = provider.context.managedObjectContext.safeFetch(request).first
|
||||
|
||||
if let mastodonUser = mastodonUser {
|
||||
return CachedProfileViewModel(context: provider.context, mastodonUser: mastodonUser)
|
||||
} else {
|
||||
return RemoteProfileViewModel(context: provider.context, userID: userID)
|
||||
}
|
||||
}()
|
||||
|
||||
DispatchQueue.main.async {
|
||||
provider.coordinator.present(scene: .profile(viewModel: profileViewModel), from: provider, transition: .show)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension StatusProviderFacade {
|
||||
|
|
Binary file not shown.
|
@ -128,7 +128,7 @@ Please check your internet connection.";
|
|||
Your account looks like this to them.";
|
||||
"Common.Controls.Timeline.Header.NoStatusFound" = "No Status Found";
|
||||
"Common.Controls.Timeline.Header.SuspendedWarning" = "This account has been suspended.";
|
||||
"Common.Controls.Timeline.Header.UserSuspendedWarning" = "%@'s account has been suspended.";
|
||||
"Common.Controls.Timeline.Header.UserSuspendedWarning" = "%@’s account has been suspended.";
|
||||
"Common.Controls.Timeline.Loader.LoadMissingPosts" = "Load missing posts";
|
||||
"Common.Controls.Timeline.Loader.LoadingMissingPosts" = "Loading missing posts...";
|
||||
"Common.Controls.Timeline.Loader.ShowMoreReplies" = "Show more replies";
|
||||
|
@ -145,7 +145,7 @@ Your account looks like this to them.";
|
|||
"Scene.Compose.Accessibility.InputLimitRemainsCount" = "Input limit remains %ld";
|
||||
"Scene.Compose.Accessibility.PostVisibilityMenu" = "Post visibility menu";
|
||||
"Scene.Compose.Accessibility.RemovePoll" = "Remove poll";
|
||||
"Scene.Compose.Attachment.AttachmentBroken" = "This %@ is broken and can't be
|
||||
"Scene.Compose.Attachment.AttachmentBroken" = "This %@ is broken and can’t be
|
||||
uploaded to Mastodon.";
|
||||
"Scene.Compose.Attachment.DescriptionPhoto" = "Describe photo for low vision people...";
|
||||
"Scene.Compose.Attachment.DescriptionVideo" = "Describe what’s happening for low vision people...";
|
||||
|
@ -153,8 +153,9 @@ uploaded to Mastodon.";
|
|||
"Scene.Compose.Attachment.Video" = "video";
|
||||
"Scene.Compose.AutoComplete.MultiplePeopleTalking" = "%ld people talking";
|
||||
"Scene.Compose.AutoComplete.SinglePeopleTalking" = "%ld people talking";
|
||||
"Scene.Compose.AutoComplete.SpaceToAdd" = "Space to add";
|
||||
"Scene.Compose.ComposeAction" = "Publish";
|
||||
"Scene.Compose.ContentInputPlaceholder" = "Type or paste what's on your mind";
|
||||
"Scene.Compose.ContentInputPlaceholder" = "Type or paste what’s on your mind";
|
||||
"Scene.Compose.ContentWarning.Placeholder" = "Write an accurate warning here...";
|
||||
"Scene.Compose.Keyboard.AppendAttachmentEntry" = "Append Attachment - %@";
|
||||
"Scene.Compose.Keyboard.DiscardPost" = "Discard Post";
|
||||
|
@ -249,7 +250,7 @@ tap the link to confirm your account.";
|
|||
"Scene.Register.Error.Special.EmailInvalid" = "This is not a valid e-mail address";
|
||||
"Scene.Register.Error.Special.PasswordTooShort" = "Password is too short (must be at least 8 characters)";
|
||||
"Scene.Register.Error.Special.UsernameInvalid" = "Username must only contain alphanumeric characters and underscores";
|
||||
"Scene.Register.Error.Special.UsernameTooLong" = "Username is too long (can't be longer than 30 characters)";
|
||||
"Scene.Register.Error.Special.UsernameTooLong" = "Username is too long (can’t be longer than 30 characters)";
|
||||
"Scene.Register.Input.Avatar.Delete" = "Delete";
|
||||
"Scene.Register.Input.DisplayName.Placeholder" = "display name";
|
||||
"Scene.Register.Input.Email.Placeholder" = "email";
|
||||
|
@ -308,7 +309,7 @@ tap the link to confirm your account.";
|
|||
any server.";
|
||||
"Scene.ServerRules.Button.Confirm" = "I Agree";
|
||||
"Scene.ServerRules.PrivacyPolicy" = "privacy policy";
|
||||
"Scene.ServerRules.Prompt" = "By continuing, you're subject to the terms of service and privacy policy for %@.";
|
||||
"Scene.ServerRules.Prompt" = "By continuing, you’re subject to the terms of service and privacy policy for %@.";
|
||||
"Scene.ServerRules.Subtitle" = "These rules are set by the admins of %@.";
|
||||
"Scene.ServerRules.TermsOfService" = "terms of service";
|
||||
"Scene.ServerRules.Title" = "Some ground rules.";
|
||||
|
|
|
@ -128,7 +128,7 @@ Please check your internet connection.";
|
|||
Your account looks like this to them.";
|
||||
"Common.Controls.Timeline.Header.NoStatusFound" = "No Status Found";
|
||||
"Common.Controls.Timeline.Header.SuspendedWarning" = "This account has been suspended.";
|
||||
"Common.Controls.Timeline.Header.UserSuspendedWarning" = "%@'s account has been suspended.";
|
||||
"Common.Controls.Timeline.Header.UserSuspendedWarning" = "%@’s account has been suspended.";
|
||||
"Common.Controls.Timeline.Loader.LoadMissingPosts" = "Load missing posts";
|
||||
"Common.Controls.Timeline.Loader.LoadingMissingPosts" = "Loading missing posts...";
|
||||
"Common.Controls.Timeline.Loader.ShowMoreReplies" = "Show more replies";
|
||||
|
@ -145,7 +145,7 @@ Your account looks like this to them.";
|
|||
"Scene.Compose.Accessibility.InputLimitRemainsCount" = "Input limit remains %ld";
|
||||
"Scene.Compose.Accessibility.PostVisibilityMenu" = "Post visibility menu";
|
||||
"Scene.Compose.Accessibility.RemovePoll" = "Remove poll";
|
||||
"Scene.Compose.Attachment.AttachmentBroken" = "This %@ is broken and can't be
|
||||
"Scene.Compose.Attachment.AttachmentBroken" = "This %@ is broken and can’t be
|
||||
uploaded to Mastodon.";
|
||||
"Scene.Compose.Attachment.DescriptionPhoto" = "Describe photo for low vision people...";
|
||||
"Scene.Compose.Attachment.DescriptionVideo" = "Describe what’s happening for low vision people...";
|
||||
|
@ -153,8 +153,9 @@ uploaded to Mastodon.";
|
|||
"Scene.Compose.Attachment.Video" = "video";
|
||||
"Scene.Compose.AutoComplete.MultiplePeopleTalking" = "%ld people talking";
|
||||
"Scene.Compose.AutoComplete.SinglePeopleTalking" = "%ld people talking";
|
||||
"Scene.Compose.AutoComplete.SpaceToAdd" = "Space to add";
|
||||
"Scene.Compose.ComposeAction" = "Publish";
|
||||
"Scene.Compose.ContentInputPlaceholder" = "Type or paste what's on your mind";
|
||||
"Scene.Compose.ContentInputPlaceholder" = "Type or paste what’s on your mind";
|
||||
"Scene.Compose.ContentWarning.Placeholder" = "Write an accurate warning here...";
|
||||
"Scene.Compose.Keyboard.AppendAttachmentEntry" = "Append Attachment - %@";
|
||||
"Scene.Compose.Keyboard.DiscardPost" = "Discard Post";
|
||||
|
@ -249,7 +250,7 @@ tap the link to confirm your account.";
|
|||
"Scene.Register.Error.Special.EmailInvalid" = "This is not a valid e-mail address";
|
||||
"Scene.Register.Error.Special.PasswordTooShort" = "Password is too short (must be at least 8 characters)";
|
||||
"Scene.Register.Error.Special.UsernameInvalid" = "Username must only contain alphanumeric characters and underscores";
|
||||
"Scene.Register.Error.Special.UsernameTooLong" = "Username is too long (can't be longer than 30 characters)";
|
||||
"Scene.Register.Error.Special.UsernameTooLong" = "Username is too long (can’t be longer than 30 characters)";
|
||||
"Scene.Register.Input.Avatar.Delete" = "Delete";
|
||||
"Scene.Register.Input.DisplayName.Placeholder" = "display name";
|
||||
"Scene.Register.Input.Email.Placeholder" = "email";
|
||||
|
@ -308,7 +309,7 @@ tap the link to confirm your account.";
|
|||
any server.";
|
||||
"Scene.ServerRules.Button.Confirm" = "I Agree";
|
||||
"Scene.ServerRules.PrivacyPolicy" = "privacy policy";
|
||||
"Scene.ServerRules.Prompt" = "By continuing, you're subject to the terms of service and privacy policy for %@.";
|
||||
"Scene.ServerRules.Prompt" = "By continuing, you’re subject to the terms of service and privacy policy for %@.";
|
||||
"Scene.ServerRules.Subtitle" = "These rules are set by the admins of %@.";
|
||||
"Scene.ServerRules.TermsOfService" = "terms of service";
|
||||
"Scene.ServerRules.Title" = "Some ground rules.";
|
||||
|
|
|
@ -45,6 +45,8 @@ final class AutoCompleteTableViewCell: UITableViewCell {
|
|||
return label
|
||||
}()
|
||||
|
||||
let separatorLine = UIView.separatorLine
|
||||
|
||||
override func prepareForReuse() {
|
||||
super.prepareForReuse()
|
||||
avatarImageView.af.cancelImageRequest()
|
||||
|
@ -118,6 +120,15 @@ extension AutoCompleteTableViewCell {
|
|||
bottomPaddingView.bottomAnchor.constraint(equalTo: contentView.bottomAnchor),
|
||||
bottomPaddingView.heightAnchor.constraint(equalTo: topPaddingView.heightAnchor, multiplier: 1.0),
|
||||
])
|
||||
|
||||
separatorLine.translatesAutoresizingMaskIntoConstraints = false
|
||||
contentView.addSubview(separatorLine)
|
||||
NSLayoutConstraint.activate([
|
||||
separatorLine.leadingAnchor.constraint(equalTo: contentView.leadingAnchor),
|
||||
separatorLine.trailingAnchor.constraint(equalTo: contentView.trailingAnchor),
|
||||
separatorLine.bottomAnchor.constraint(equalTo: contentView.bottomAnchor),
|
||||
separatorLine.heightAnchor.constraint(equalToConstant: UIView.separatorLineHeight(of: contentView)).priority(.defaultHigh),
|
||||
])
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -55,6 +55,7 @@ final class ComposeViewController: UIViewController, NeedsDependency {
|
|||
collectionView.register(ComposeStatusPollExpiresOptionCollectionViewCell.self, forCellWithReuseIdentifier: String(describing: ComposeStatusPollExpiresOptionCollectionViewCell.self))
|
||||
collectionView.backgroundColor = Asset.Scene.Compose.background.color
|
||||
collectionView.alwaysBounceVertical = true
|
||||
collectionView.keyboardDismissMode = .onDrag
|
||||
return collectionView
|
||||
}()
|
||||
|
||||
|
@ -380,11 +381,11 @@ extension ComposeViewController {
|
|||
self.composeToolbarView.characterCountLabel.text = "\(count)"
|
||||
switch count {
|
||||
case _ where count < 0:
|
||||
self.composeToolbarView.characterCountLabel.font = .systemFont(ofSize: 24, weight: .bold)
|
||||
self.composeToolbarView.characterCountLabel.font = .monospacedDigitSystemFont(ofSize: 24, weight: .bold)
|
||||
self.composeToolbarView.characterCountLabel.textColor = Asset.Colors.danger.color
|
||||
self.composeToolbarView.characterCountLabel.accessibilityLabel = L10n.Scene.Compose.Accessibility.inputLimitExceedsCount(abs(count))
|
||||
default:
|
||||
self.composeToolbarView.characterCountLabel.font = .systemFont(ofSize: 15, weight: .regular)
|
||||
self.composeToolbarView.characterCountLabel.font = .monospacedDigitSystemFont(ofSize: 15, weight: .regular)
|
||||
self.composeToolbarView.characterCountLabel.textColor = Asset.Colors.Label.secondary.color
|
||||
self.composeToolbarView.characterCountLabel.accessibilityLabel = L10n.Scene.Compose.Accessibility.inputLimitRemainsCount(count)
|
||||
}
|
||||
|
|
|
@ -69,6 +69,9 @@ extension ComposeViewModel {
|
|||
snapshot.appendItems([.input(replyToStatusObjectID: nil, attribute: composeStatusAttribute)], toSection: .status)
|
||||
}
|
||||
diffableDataSource.apply(snapshot, animatingDifferences: false)
|
||||
|
||||
// some magic fix modal presentation animation issue
|
||||
collectionView.dataSource = diffableDataSource
|
||||
}
|
||||
|
||||
func setupCustomEmojiPickerDiffableDataSource(
|
||||
|
|
|
@ -191,22 +191,22 @@ extension ComposeToolbarView {
|
|||
|
||||
func image(interfaceStyle: UIUserInterfaceStyle) -> UIImage {
|
||||
switch self {
|
||||
case .public:
|
||||
case .public: return UIImage(systemName: "globe", withConfiguration: UIImage.SymbolConfiguration(pointSize: 19, weight: .medium))!
|
||||
// case .unlisted: return UIImage(systemName: "eye.slash", withConfiguration: UIImage.SymbolConfiguration(pointSize: 18, weight: .regular))!
|
||||
case .private:
|
||||
switch interfaceStyle {
|
||||
case .light: return UIImage(systemName: "person.3", withConfiguration: UIImage.SymbolConfiguration(pointSize: 15, weight: .medium))!
|
||||
default: return UIImage(systemName: "person.3.fill", withConfiguration: UIImage.SymbolConfiguration(pointSize: 15, weight: .medium))!
|
||||
}
|
||||
// case .unlisted: return UIImage(systemName: "eye.slash", withConfiguration: UIImage.SymbolConfiguration(pointSize: 18, weight: .regular))!
|
||||
case .private: return UIImage(systemName: "person.crop.circle.badge.plus", withConfiguration: UIImage.SymbolConfiguration(pointSize: 18, weight: .regular))!
|
||||
case .direct: return UIImage(systemName: "at", withConfiguration: UIImage.SymbolConfiguration(pointSize: 19, weight: .regular))!
|
||||
}
|
||||
}
|
||||
|
||||
func imageNameForTimeline() -> String {
|
||||
switch self {
|
||||
case .public: return "person.3"
|
||||
case .public: return "globe"
|
||||
// case .unlisted: return "eye.slash"
|
||||
case .private: return "person.crop.circle.badge.plus"
|
||||
case .private: return "person.3"
|
||||
case .direct: return "at"
|
||||
}
|
||||
}
|
||||
|
@ -259,7 +259,7 @@ extension ComposeToolbarView {
|
|||
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 }
|
||||
os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s: mediaSelectionType: .photoLibaray", ((#file as NSString).lastPathComponent), #line, #function)
|
||||
os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s: mediaSelectionType: .photoLibrary", ((#file as NSString).lastPathComponent), #line, #function)
|
||||
self.delegate?.composeToolbarView(self, cameraButtonDidPressed: self.mediaButton, mediaSelectionType: .photoLibrary)
|
||||
}
|
||||
children.append(photoLibraryAction)
|
||||
|
|
|
@ -0,0 +1,387 @@
|
|||
//
|
||||
// AsyncHomeTimelineViewController+DebugAction.swift
|
||||
// Mastodon
|
||||
//
|
||||
// Created by MainasuK Cirno on 2021-6-21.
|
||||
//
|
||||
|
||||
import os.log
|
||||
import UIKit
|
||||
import CoreData
|
||||
import CoreDataStack
|
||||
|
||||
#if DEBUG
|
||||
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)
|
||||
}),
|
||||
UIAction(title: "Toggle Home", image: nil, identifier: nil, discoverabilityTitle: nil, attributes: [], state: .off, handler: { [weak self] action in
|
||||
guard let self = self else { return }
|
||||
self.context.toggleHomePreference(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
|
|
@ -0,0 +1,113 @@
|
|||
//
|
||||
// AsyncHomeTimelineViewController+Provider.swift
|
||||
// Mastodon
|
||||
//
|
||||
// Created by MainasuK Cirno on 2021-6-21.
|
||||
//
|
||||
|
||||
import os.log
|
||||
import UIKit
|
||||
import Combine
|
||||
import CoreData
|
||||
import CoreDataStack
|
||||
import AsyncDisplayKit
|
||||
|
||||
// MARK: - StatusProvider
|
||||
extension AsyncHomeTimelineViewController: StatusProvider {
|
||||
|
||||
func status() -> Future<Status?, Never> {
|
||||
return Future { promise in promise(.success(nil)) }
|
||||
}
|
||||
|
||||
func status(for cell: UITableViewCell?, indexPath: IndexPath?) -> Future<Status?, Never> {
|
||||
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<Status?, Never> {
|
||||
return Future { promise in promise(.success(nil)) }
|
||||
}
|
||||
|
||||
var managedObjectContext: NSManagedObjectContext {
|
||||
return viewModel.fetchedResultsController.managedObjectContext
|
||||
}
|
||||
|
||||
var tableViewDiffableDataSource: UITableViewDiffableDataSource<StatusSection, Item>? {
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
extension AsyncHomeTimelineViewController: UserProvider {}
|
|
@ -0,0 +1,606 @@
|
|||
//
|
||||
// AsyncHomeTimelineViewController.swift
|
||||
// Mastodon
|
||||
//
|
||||
// Created by MainasuK Cirno on 2021-6-21.
|
||||
//
|
||||
|
||||
import os.log
|
||||
import UIKit
|
||||
import AVKit
|
||||
import Combine
|
||||
import CoreData
|
||||
import CoreDataStack
|
||||
import GameplayKit
|
||||
import MastodonSDK
|
||||
import AlamofireImage
|
||||
import AsyncDisplayKit
|
||||
|
||||
#if DEBUG
|
||||
import GDPerformanceView_Swift
|
||||
#endif
|
||||
|
||||
final class AsyncHomeTimelineViewController: ASDKViewController<ASTableNode>, NeedsDependency, MediaPreviewableViewController {
|
||||
|
||||
weak var context: AppContext! { willSet { precondition(!isViewLoaded) } }
|
||||
weak var coordinator: SceneCoordinator! { willSet { precondition(!isViewLoaded) } }
|
||||
|
||||
var disposeBag = Set<AnyCancellable>()
|
||||
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 tableView: UITableView = {
|
||||
// let tableView = ControlContainableTableView()
|
||||
// tableView.register(StatusTableViewCell.self, forCellReuseIdentifier: String(describing: StatusTableViewCell.self))
|
||||
// tableView.register(TimelineMiddleLoaderTableViewCell.self, forCellReuseIdentifier: String(describing: TimelineMiddleLoaderTableViewCell.self))
|
||||
// tableView.register(TimelineBottomLoaderTableViewCell.self, forCellReuseIdentifier: String(describing: TimelineBottomLoaderTableViewCell.self))
|
||||
// tableView.rowHeight = UITableView.automaticDimension
|
||||
// tableView.separatorStyle = .none
|
||||
// tableView.backgroundColor = .clear
|
||||
// return tableView
|
||||
//}()
|
||||
|
||||
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 = Asset.Colors.Background.secondarySystemBackground.color
|
||||
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
|
||||
PerformanceMonitor.shared().delegate = self
|
||||
|
||||
#else
|
||||
settingBarButtonItem.target = self
|
||||
settingBarButtonItem.action = #selector(AsyncHomeTimelineViewController.settingBarButtonItemPressed(_:))
|
||||
#endif
|
||||
settingBarButtonItem.menu = UIMenu(title: "Toggle Home", image: nil, identifier: nil, options: [], children: [
|
||||
UIAction(title: "Setting", image: nil, identifier: nil, discoverabilityTitle: nil, attributes: [], state: .off, handler: { [weak self] _ in
|
||||
guard let self = self else { return }
|
||||
self.settingBarButtonItemPressed(self.settingBarButtonItem)
|
||||
}),
|
||||
UIAction(title: "Toggle Home", image: nil, identifier: nil, discoverabilityTitle: nil, attributes: [], state: .off, handler: { [weak self] action in
|
||||
guard let self = self else { return }
|
||||
self.context.toggleHomePreference(action)
|
||||
let alertController = UIAlertController(title: "Please Restart App", message: nil, preferredStyle: .alert)
|
||||
let okAction = UIAlertAction(title: "OK", style: .default, handler: nil)
|
||||
alertController.addAction(okAction)
|
||||
self.coordinator.present(scene: .alertController(alertController: alertController), from: nil, transition: .alertController(animated: true, completion: nil))
|
||||
})
|
||||
])
|
||||
|
||||
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<NSNumber, NSValue> { 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)
|
||||
}
|
||||
}
|
||||
|
||||
#if DEBUG
|
||||
extension AsyncHomeTimelineViewController: PerformanceMonitorDelegate {
|
||||
func performanceMonitor(didReport performanceReport: PerformanceReport) {
|
||||
// print(performanceReport)
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
// 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.loadoldestStateMachine.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 { }
|
|
@ -0,0 +1,155 @@
|
|||
//
|
||||
// AsyncHomeTimelineViewModel+Diffable.swift
|
||||
// Mastodon
|
||||
//
|
||||
// Created by MainasuK Cirno on 2021-6-21.
|
||||
//
|
||||
|
||||
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<StatusSection, Item>()
|
||||
snapshot.appendSections([.main])
|
||||
diffableDataSource?.apply(snapshot)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// MARK: - NSFetchedResultsControllerDelegate
|
||||
extension AsyncHomeTimelineViewModel: NSFetchedResultsControllerDelegate {
|
||||
|
||||
func controllerWillChangeContent(_ controller: NSFetchedResultsController<NSFetchRequestResult>) {
|
||||
os_log("%{public}s[%{public}ld], %{public}s", ((#file as NSString).lastPathComponent), #line, #function)
|
||||
}
|
||||
|
||||
func controller(_ controller: NSFetchedResultsController<NSFetchRequestResult>, 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<StatusSection, Item>()
|
||||
newSnapshot.appendSections([.main])
|
||||
newSnapshot.appendItems(newTimelineItems, toSection: .main)
|
||||
|
||||
let endSnapshot = CACurrentMediaTime()
|
||||
|
||||
if shouldAddBottomLoader, !(self.loadoldestStateMachine.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<T> {
|
||||
let item: T
|
||||
let sourceIndexPath: IndexPath
|
||||
let targetIndexPath: IndexPath
|
||||
let offset: CGFloat
|
||||
}
|
||||
|
||||
private func calculateReloadSnapshotDifference<T: Hashable>(
|
||||
navigationBar: UINavigationBar,
|
||||
tableView: UITableView,
|
||||
oldSnapshot: DiffableDataSourceSnapshot<StatusSection, T>,
|
||||
newSnapshot: DiffableDataSourceSnapshot<StatusSection, T>
|
||||
) -> Difference<T>? {
|
||||
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
|
||||
)
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,130 @@
|
|||
//
|
||||
// AsyncHomeTimelineViewModel+LoadLatestState.swift
|
||||
// Mastodon
|
||||
//
|
||||
// Created by MainasuK Cirno on 2021-6-21.
|
||||
//
|
||||
//
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,108 @@
|
|||
//
|
||||
// AsyncHomeTimelineViewModel+LoadMiddleState.swift
|
||||
// Mastodon
|
||||
//
|
||||
// Created by MainasuK Cirno on 2021-6-21.
|
||||
//
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,113 @@
|
|||
//
|
||||
// AsyncHomeTimelineViewModel+LoadOldestState.swift
|
||||
// Mastodon
|
||||
//
|
||||
// Created by MainasuK Cirno on 2021-6-21.
|
||||
//
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,148 @@
|
|||
//
|
||||
// AsyncHomeTimelineViewModel.swift
|
||||
// Mastodon
|
||||
//
|
||||
// Created by MainasuK Cirno on 2021-6-21.
|
||||
//
|
||||
//
|
||||
|
||||
import os.log
|
||||
import func AVFoundation.AVMakeRect
|
||||
import UIKit
|
||||
import AVKit
|
||||
import Combine
|
||||
import CoreData
|
||||
import CoreDataStack
|
||||
import GameplayKit
|
||||
import AlamofireImage
|
||||
import DateToolsSwift
|
||||
import ActiveLabel
|
||||
import AsyncDisplayKit
|
||||
|
||||
final class AsyncHomeTimelineViewModel: NSObject {
|
||||
|
||||
var disposeBag = Set<AnyCancellable>()
|
||||
var observations = Set<NSKeyValueObservation>()
|
||||
|
||||
// input
|
||||
let context: AppContext
|
||||
let timelinePredicate = CurrentValueSubject<NSPredicate?, Never>(nil)
|
||||
let fetchedResultsController: NSFetchedResultsController<HomeTimelineIndex>
|
||||
let isFetchingLatestTimeline = CurrentValueSubject<Bool, Never>(false)
|
||||
let viewDidAppear = PassthroughSubject<Void, Never>()
|
||||
let homeTimelineNavigationBarTitleViewModel: HomeTimelineNavigationBarTitleViewModel
|
||||
|
||||
weak var tableNode: ASTableNode?
|
||||
weak var contentOffsetAdjustableTimelineViewControllerDelegate: ContentOffsetAdjustableTimelineViewControllerDelegate?
|
||||
//weak var tableView: UITableView?
|
||||
weak var timelineMiddleLoaderTableViewCellDelegate: TimelineMiddleLoaderTableViewCellDelegate?
|
||||
|
||||
let timelineIsEmpty = CurrentValueSubject<Bool, Never>(false)
|
||||
let homeTimelineNeedRefresh = PassthroughSubject<Void, Never>()
|
||||
|
||||
// output
|
||||
var diffableDataSource: TableNodeDiffableDataSource<StatusSection, Item>?
|
||||
|
||||
// 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<LoadLatestState?, Never>(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<LoadOldestState?, Never>(nil)
|
||||
// middle loader
|
||||
let loadMiddleSateMachineList = CurrentValueSubject<[NSManagedObjectID: GKStateMachine], Never>([:]) // TimelineIndex.objectID : middle loading state machine
|
||||
// var diffableDataSource: UITableViewDiffableDataSource<StatusSection, Item>?
|
||||
var cellFrameCache = NSCache<NSNumber, NSValue>()
|
||||
|
||||
|
||||
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 { }
|
|
@ -25,6 +25,10 @@ extension HomeTimelineViewController {
|
|||
guard let self = self else { return }
|
||||
self.showFLEXAction(action)
|
||||
}),
|
||||
UIAction(title: "Toggle Home", image: nil, identifier: nil, discoverabilityTitle: nil, attributes: [], state: .off, handler: { [weak self] action in
|
||||
guard let self = self else { return }
|
||||
self.context.toggleHomePreference(action)
|
||||
}),
|
||||
moveMenu,
|
||||
dropMenu,
|
||||
UIAction(title: "Show Welcome", image: UIImage(systemName: "figure.walk"), attributes: []) { [weak self] action in
|
||||
|
|
|
@ -103,7 +103,21 @@ extension HomeTimelineViewController {
|
|||
// long press to trigger debug menu
|
||||
settingBarButtonItem.menu = debugMenu
|
||||
PerformanceMonitor.shared().delegate = self
|
||||
|
||||
#elseif ASDK
|
||||
settingBarButtonItem.menu = UIMenu(title: "Toggle Home", image: nil, identifier: nil, options: [], children: [
|
||||
UIAction(title: "Setting", image: nil, identifier: nil, discoverabilityTitle: nil, attributes: [], state: .off, handler: { [weak self] _ in
|
||||
guard let self = self else { return }
|
||||
self.settingBarButtonItemPressed(self.settingBarButtonItem)
|
||||
}),
|
||||
UIAction(title: "Show Async Home", image: nil, identifier: nil, discoverabilityTitle: nil, attributes: [], state: .off, handler: { [weak self] action in
|
||||
guard let self = self else { return }
|
||||
self.context.toggleHomePreference(action)
|
||||
let alertController = UIAlertController(title: "Please Restart App", message: nil, preferredStyle: .alert)
|
||||
let okAction = UIAlertAction(title: "OK", style: .default, handler: nil)
|
||||
alertController.addAction(okAction)
|
||||
self.coordinator.present(scene: .alertController(alertController: alertController), from: nil, transition: .alertController(animated: true, completion: nil))
|
||||
})
|
||||
])
|
||||
#else
|
||||
settingBarButtonItem.target = self
|
||||
settingBarButtonItem.action = #selector(HomeTimelineViewController.settingBarButtonItemPressed(_:))
|
||||
|
@ -526,6 +540,10 @@ extension HomeTimelineViewController: StatusTableViewCellDelegate {
|
|||
|
||||
// MARK: - HomeTimelineNavigationBarTitleViewDelegate
|
||||
extension HomeTimelineViewController: HomeTimelineNavigationBarTitleViewDelegate {
|
||||
func homeTimelineNavigationBarTitleView(_ titleView: HomeTimelineNavigationBarTitleView, logoButtonDidPressed sender: UIButton) {
|
||||
scrollToTop(animated: true)
|
||||
}
|
||||
|
||||
func homeTimelineNavigationBarTitleView(_ titleView: HomeTimelineNavigationBarTitleView, buttonDidPressed sender: UIButton) {
|
||||
switch titleView.state {
|
||||
case .newPostButton:
|
||||
|
|
|
@ -9,6 +9,7 @@ import os.log
|
|||
import UIKit
|
||||
|
||||
protocol HomeTimelineNavigationBarTitleViewDelegate: AnyObject {
|
||||
func homeTimelineNavigationBarTitleView(_ titleView: HomeTimelineNavigationBarTitleView, logoButtonDidPressed sender: UIButton)
|
||||
func homeTimelineNavigationBarTitleView(_ titleView: HomeTimelineNavigationBarTitleView, buttonDidPressed sender: UIButton)
|
||||
}
|
||||
|
||||
|
@ -16,7 +17,7 @@ final class HomeTimelineNavigationBarTitleView: UIView {
|
|||
|
||||
let containerView = UIStackView()
|
||||
|
||||
let imageView = UIImageView()
|
||||
let logoButton = HighlightDimmableButton()
|
||||
let button = RoundedEdgesButton()
|
||||
let label = UILabel()
|
||||
|
||||
|
@ -25,7 +26,7 @@ final class HomeTimelineNavigationBarTitleView: UIView {
|
|||
weak var delegate: HomeTimelineNavigationBarTitleViewDelegate?
|
||||
|
||||
// output
|
||||
private(set) var state: HomeTimelineNavigationBarTitleViewModel.State = .logoImage
|
||||
private(set) var state: HomeTimelineNavigationBarTitleViewModel.State = .logo
|
||||
|
||||
override init(frame: CGRect) {
|
||||
super.init(frame: frame)
|
||||
|
@ -50,7 +51,7 @@ extension HomeTimelineNavigationBarTitleView {
|
|||
containerView.bottomAnchor.constraint(equalTo: bottomAnchor),
|
||||
])
|
||||
|
||||
containerView.addArrangedSubview(imageView)
|
||||
containerView.addArrangedSubview(logoButton)
|
||||
button.translatesAutoresizingMaskIntoConstraints = false
|
||||
containerView.addArrangedSubview(button)
|
||||
NSLayoutConstraint.activate([
|
||||
|
@ -58,12 +59,18 @@ extension HomeTimelineNavigationBarTitleView {
|
|||
])
|
||||
containerView.addArrangedSubview(label)
|
||||
|
||||
configure(state: .logoImage)
|
||||
configure(state: .logo)
|
||||
logoButton.addTarget(self, action: #selector(HomeTimelineNavigationBarTitleView.logoButtonDidPressed(_:)), for: .touchUpInside)
|
||||
button.addTarget(self, action: #selector(HomeTimelineNavigationBarTitleView.buttonDidPressed(_:)), for: .touchUpInside)
|
||||
}
|
||||
}
|
||||
|
||||
extension HomeTimelineNavigationBarTitleView {
|
||||
@objc private func logoButtonDidPressed(_ sender: UIButton) {
|
||||
os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s", ((#file as NSString).lastPathComponent), #line, #function)
|
||||
delegate?.homeTimelineNavigationBarTitleView(self, logoButtonDidPressed: sender)
|
||||
}
|
||||
|
||||
@objc private func buttonDidPressed(_ sender: UIButton) {
|
||||
os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s", ((#file as NSString).lastPathComponent), #line, #function)
|
||||
delegate?.homeTimelineNavigationBarTitleView(self, buttonDidPressed: sender)
|
||||
|
@ -73,7 +80,7 @@ extension HomeTimelineNavigationBarTitleView {
|
|||
extension HomeTimelineNavigationBarTitleView {
|
||||
|
||||
func resetContainer() {
|
||||
imageView.isHidden = true
|
||||
logoButton.isHidden = true
|
||||
button.isHidden = true
|
||||
label.isHidden = true
|
||||
}
|
||||
|
@ -90,11 +97,11 @@ extension HomeTimelineNavigationBarTitleView {
|
|||
resetContainer()
|
||||
|
||||
switch state {
|
||||
case .logoImage:
|
||||
imageView.tintColor = Asset.Colors.Label.primary.color
|
||||
imageView.image = Asset.Asset.mastodonTextLogo.image.withRenderingMode(.alwaysTemplate)
|
||||
imageView.contentMode = .center
|
||||
imageView.isHidden = false
|
||||
case .logo:
|
||||
logoButton.tintColor = Asset.Colors.Label.primary.color
|
||||
logoButton.setImage(Asset.Asset.mastodonTextLogo.image.withRenderingMode(.alwaysTemplate), for: .normal)
|
||||
logoButton.contentMode = .center
|
||||
logoButton.isHidden = false
|
||||
case .newPostButton:
|
||||
configureButton(
|
||||
title: L10n.Scene.HomeTimeline.NavigationBarState.newPosts,
|
||||
|
@ -173,7 +180,7 @@ struct HomeTimelineNavigationBarTitleView_Previews: PreviewProvider {
|
|||
Group {
|
||||
UIViewPreview(width: 375) {
|
||||
let titleView = HomeTimelineNavigationBarTitleView()
|
||||
titleView.configure(state: .logoImage)
|
||||
titleView.configure(state: .logo)
|
||||
return titleView
|
||||
}
|
||||
.previewLayout(.fixed(width: 375, height: 44))
|
||||
|
|
|
@ -22,7 +22,7 @@ final class HomeTimelineNavigationBarTitleViewModel {
|
|||
var networkErrorPublisher = PassthroughSubject<Void, Never>()
|
||||
|
||||
// output
|
||||
let state = CurrentValueSubject<State, Never>(.logoImage)
|
||||
let state = CurrentValueSubject<State, Never>(.logo)
|
||||
let hasNewPosts = CurrentValueSubject<Bool, Never>(false)
|
||||
let isOffline = CurrentValueSubject<Bool, Never>(false)
|
||||
let isPublishingPost = CurrentValueSubject<Bool, Never>(false)
|
||||
|
@ -75,7 +75,7 @@ final class HomeTimelineNavigationBarTitleViewModel {
|
|||
guard !isPublishingPost else { return .publishingPostLabel }
|
||||
guard !isOffline else { return .offlineButton }
|
||||
guard !hasNewPosts else { return .newPostButton }
|
||||
return .logoImage
|
||||
return .logo
|
||||
}
|
||||
.receive(on: DispatchQueue.main)
|
||||
.assign(to: \.value, on: state)
|
||||
|
@ -100,7 +100,7 @@ final class HomeTimelineNavigationBarTitleViewModel {
|
|||
extension HomeTimelineNavigationBarTitleViewModel {
|
||||
// state order by priority from low to high
|
||||
enum State: String {
|
||||
case logoImage
|
||||
case logo
|
||||
case newPostButton
|
||||
case offlineButton
|
||||
case publishingPostLabel
|
||||
|
|
|
@ -45,7 +45,11 @@ 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
|
||||
|
|
|
@ -0,0 +1,21 @@
|
|||
//
|
||||
// ASMetaEditableTextNode.swift
|
||||
// Mastodon
|
||||
//
|
||||
// Created by Cirno MainasuK on 2021-6-20.
|
||||
//
|
||||
|
||||
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
|
||||
}
|
||||
}
|
|
@ -0,0 +1,237 @@
|
|||
//
|
||||
// StatusNNode.swift
|
||||
// Mastodon
|
||||
//
|
||||
// Created by Cirno MainasuK on 2021-6-19.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
import Combine
|
||||
import AsyncDisplayKit
|
||||
import CoreDataStack
|
||||
import ActiveLabel
|
||||
import func AVFoundation.AVMakeRect
|
||||
|
||||
protocol StatusNodeDelegate: AnyObject {
|
||||
func statusNode(_ node: StatusNode, statusContentTextNode: ASMetaEditableTextNode, didSelectActiveEntityType type: ActiveEntityType)
|
||||
}
|
||||
|
||||
final class StatusNode: ASCellNode {
|
||||
|
||||
var disposeBag = Set<AnyCancellable>()
|
||||
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.slowedTimeAgoSinceNow, 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)
|
||||
])
|
||||
|
||||
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.dataSource = 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.slowedTimeAgoSinceNow, 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 verticalStack
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
//extension StatusNode: ASImageDownloaderProtocol {
|
||||
// func downloadImage(with URL: URL, callbackQueue: DispatchQueue, downloadProgress: ASImageDownloaderProgress?, completion: @escaping ASImageDownloaderCompletion) -> Any? {
|
||||
//
|
||||
// }
|
||||
//
|
||||
// func cancelImageDownload(forIdentifier downloadIdentifier: Any) {
|
||||
//
|
||||
// }
|
||||
//}
|
||||
|
||||
// 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.priviewURL
|
||||
default:
|
||||
assertionFailure()
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,37 @@
|
|||
//
|
||||
// TimelineBottomLoaderNode.swift
|
||||
// Mastodon
|
||||
//
|
||||
// Created by Cirno MainasuK on 2021-6-19.
|
||||
//
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,50 @@
|
|||
//
|
||||
// TimelineMiddleLoaderNode.swift
|
||||
// Mastodon
|
||||
//
|
||||
// Created by Cirno MainasuK on 2021-6-19.
|
||||
//
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
}
|
|
@ -16,16 +16,14 @@ struct MosaicImageViewModel {
|
|||
init(mediaAttachments: [Attachment]) {
|
||||
var metas: [MosaicMeta] = []
|
||||
for element in mediaAttachments where element.type == .image {
|
||||
// Display original on the iPad/Mac
|
||||
guard let previewURL = element.previewURL else { continue }
|
||||
let urlString = UIDevice.current.userInterfaceIdiom == .phone ? previewURL : element.url
|
||||
guard let meta = element.meta,
|
||||
let width = meta.original?.width,
|
||||
let height = meta.original?.height,
|
||||
let url = URL(string: urlString) else {
|
||||
let url = URL(string: element.url) else {
|
||||
continue
|
||||
}
|
||||
let mosaicMeta = MosaicMeta(
|
||||
priviewURL: element.previewURL.flatMap { URL(string: $0) },
|
||||
url: url,
|
||||
size: CGSize(width: width, height: height),
|
||||
blurhash: element.blurhash,
|
||||
|
@ -40,7 +38,8 @@ struct MosaicImageViewModel {
|
|||
|
||||
struct MosaicMeta {
|
||||
static let edgeMaxLength: CGFloat = 20
|
||||
|
||||
|
||||
let priviewURL: URL?
|
||||
let url: URL
|
||||
let size: CGSize
|
||||
let blurhash: String?
|
||||
|
|
|
@ -8,6 +8,8 @@
|
|||
import UIKit
|
||||
import Combine
|
||||
import CoreData
|
||||
import CoreDataStack
|
||||
import MastodonSDK
|
||||
|
||||
extension ThreadViewModel {
|
||||
|
||||
|
@ -41,13 +43,29 @@ extension ThreadViewModel {
|
|||
diffableDataSource?.apply(snapshot, animatingDifferences: false, completion: nil)
|
||||
|
||||
Publishers.CombineLatest3(
|
||||
rootItem.removeDuplicates(),
|
||||
ancestorItems.removeDuplicates(),
|
||||
descendantItems.removeDuplicates()
|
||||
)
|
||||
.receive(on: RunLoop.main)
|
||||
.sink { [weak self] rootItem, ancestorItems, descendantItems in
|
||||
guard let self = self else { return }
|
||||
var items: [Item] = []
|
||||
rootItem.flatMap { items.append($0) }
|
||||
items.append(contentsOf: ancestorItems)
|
||||
items.append(contentsOf: descendantItems)
|
||||
self.updateDeletedStatus(for: items)
|
||||
}
|
||||
.store(in: &disposeBag)
|
||||
|
||||
Publishers.CombineLatest4(
|
||||
rootItem,
|
||||
ancestorItems,
|
||||
descendantItems
|
||||
descendantItems,
|
||||
existStatusFetchedResultsController.objectIDs
|
||||
)
|
||||
.debounce(for: .milliseconds(100), scheduler: DispatchQueue.main) // some magic to avoid jitter
|
||||
.receive(on: DispatchQueue.main)
|
||||
.sink { [weak self] rootItem, ancestorItems, descendantItems in
|
||||
.debounce(for: .milliseconds(100), scheduler: RunLoop.main) // some magic to avoid jitter
|
||||
.sink { [weak self] rootItem, ancestorItems, descendantItems, existObjectIDs in
|
||||
guard let self = self else { return }
|
||||
guard let tableView = self.tableView,
|
||||
let navigationBar = self.contentOffsetAdjustableTimelineViewControllerDelegate?.navigationBar()
|
||||
|
@ -65,31 +83,42 @@ extension ThreadViewModel {
|
|||
if self.rootNode.value?.replyToID != nil, !(currentState is LoadThreadState.NoMore) {
|
||||
newSnapshot.appendItems([.topLoader], toSection: .main)
|
||||
}
|
||||
|
||||
let ancestorItems = ancestorItems.filter { item in
|
||||
guard case let .reply(statusObjectID, _) = item else { return false }
|
||||
return existObjectIDs.contains(statusObjectID)
|
||||
}
|
||||
newSnapshot.appendItems(ancestorItems, toSection: .main)
|
||||
|
||||
// root
|
||||
if let rootItem = rootItem {
|
||||
switch rootItem {
|
||||
case .root:
|
||||
newSnapshot.appendItems([rootItem], toSection: .main)
|
||||
default:
|
||||
break
|
||||
}
|
||||
if let rootItem = rootItem,
|
||||
case let .root(objectID, _) = rootItem,
|
||||
existObjectIDs.contains(objectID) {
|
||||
newSnapshot.appendItems([rootItem], toSection: .main)
|
||||
}
|
||||
|
||||
// leaf
|
||||
if !(currentState is LoadThreadState.NoMore) {
|
||||
newSnapshot.appendItems([.bottomLoader], toSection: .main)
|
||||
}
|
||||
|
||||
let descendantItems = descendantItems.filter { item in
|
||||
switch item {
|
||||
case .leaf(let statusObjectID, _):
|
||||
return existObjectIDs.contains(statusObjectID)
|
||||
default:
|
||||
return true
|
||||
}
|
||||
}
|
||||
newSnapshot.appendItems(descendantItems, toSection: .main)
|
||||
|
||||
// difference for first visiable item exclude .topLoader
|
||||
// difference for first visible item exclude .topLoader
|
||||
guard let difference = self.calculateReloadSnapshotDifference(navigationBar: navigationBar, tableView: tableView, oldSnapshot: oldSnapshot, newSnapshot: newSnapshot) else {
|
||||
diffableDataSource.apply(newSnapshot)
|
||||
return
|
||||
}
|
||||
|
||||
// addtional margin for .topLoader
|
||||
// additional margin for .topLoader
|
||||
let oldTopMargin: CGFloat = {
|
||||
let marginHeight = TimelineTopLoaderTableViewCell.cellHeight
|
||||
if oldSnapshot.itemIdentifiers.contains(.topLoader) {
|
||||
|
@ -184,3 +213,33 @@ extension ThreadViewModel {
|
|||
)
|
||||
}
|
||||
}
|
||||
|
||||
extension ThreadViewModel {
|
||||
private func updateDeletedStatus(for items: [Item]) {
|
||||
let parentManagedObjectContext = context.managedObjectContext
|
||||
let managedObjectContext = NSManagedObjectContext(concurrencyType: .privateQueueConcurrencyType)
|
||||
managedObjectContext.parent = parentManagedObjectContext
|
||||
managedObjectContext.perform {
|
||||
var statusIDs: [Status.ID] = []
|
||||
for item in items {
|
||||
switch item {
|
||||
case .root(let objectID, _):
|
||||
guard let status = managedObjectContext.object(with: objectID) as? Status else { continue }
|
||||
statusIDs.append(status.id)
|
||||
case .reply(let objectID, _):
|
||||
guard let status = managedObjectContext.object(with: objectID) as? Status else { continue }
|
||||
statusIDs.append(status.id)
|
||||
case .leaf(let objectID, _):
|
||||
guard let status = managedObjectContext.object(with: objectID) as? Status else { continue }
|
||||
statusIDs.append(status.id)
|
||||
default:
|
||||
continue
|
||||
}
|
||||
}
|
||||
DispatchQueue.main.async { [weak self] in
|
||||
guard let self = self else { return }
|
||||
self.existStatusFetchedResultsController.statusIDs.value = statusIDs
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -16,12 +16,14 @@ import MastodonSDK
|
|||
class ThreadViewModel {
|
||||
|
||||
var disposeBag = Set<AnyCancellable>()
|
||||
var rootItemObserver: AnyCancellable?
|
||||
|
||||
// input
|
||||
let context: AppContext
|
||||
let rootNode: CurrentValueSubject<RootNode?, Never>
|
||||
let rootItem: CurrentValueSubject<Item?, Never>
|
||||
let cellFrameCache = NSCache<NSNumber, NSValue>()
|
||||
let existStatusFetchedResultsController: StatusFetchedResultsController
|
||||
|
||||
weak var contentOffsetAdjustableTimelineViewControllerDelegate: ContentOffsetAdjustableTimelineViewControllerDelegate?
|
||||
weak var tableView: UITableView?
|
||||
|
@ -49,10 +51,20 @@ class ThreadViewModel {
|
|||
self.context = context
|
||||
self.rootNode = CurrentValueSubject(optionalStatus.flatMap { RootNode(domain: $0.domain, statusID: $0.id, replyToID: $0.inReplyToID) })
|
||||
self.rootItem = CurrentValueSubject(optionalStatus.flatMap { Item.root(statusObjectID: $0.objectID, attribute: Item.StatusAttribute()) })
|
||||
self.existStatusFetchedResultsController = StatusFetchedResultsController(managedObjectContext: context.managedObjectContext, domain: nil, additionalTweetPredicate: nil)
|
||||
self.navigationBarTitle = CurrentValueSubject(
|
||||
optionalStatus.flatMap { L10n.Scene.Thread.title($0.author.displayNameWithFallback) }
|
||||
)
|
||||
|
||||
// bind fetcher domain
|
||||
context.authenticationService.activeMastodonAuthenticationBox
|
||||
.receive(on: RunLoop.main)
|
||||
.sink { [weak self] box in
|
||||
guard let self = self else { return }
|
||||
self.existStatusFetchedResultsController.domain.value = box?.domain
|
||||
}
|
||||
.store(in: &disposeBag)
|
||||
|
||||
rootNode
|
||||
.receive(on: DispatchQueue.main)
|
||||
.sink { [weak self] rootNode in
|
||||
|
@ -79,8 +91,32 @@ class ThreadViewModel {
|
|||
.store(in: &disposeBag)
|
||||
}
|
||||
|
||||
// descendantNodes
|
||||
|
||||
rootItem
|
||||
.receive(on: DispatchQueue.main)
|
||||
.sink { [weak self] rootItem in
|
||||
guard let self = self else { return }
|
||||
guard case let .root(objectID, _) = rootItem else { return }
|
||||
self.context.managedObjectContext.perform {
|
||||
guard let status = self.context.managedObjectContext.object(with: objectID) as? Status else {
|
||||
return
|
||||
}
|
||||
self.rootItemObserver = ManagedObjectObserver.observe(object: status)
|
||||
.receive(on: DispatchQueue.main)
|
||||
.sink(receiveCompletion: { _ in
|
||||
// do nothing
|
||||
}, receiveValue: { [weak self] change in
|
||||
guard let self = self else { return }
|
||||
switch change.changeType {
|
||||
case .delete:
|
||||
self.rootItem.value = nil
|
||||
default:
|
||||
break
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
.store(in: &disposeBag)
|
||||
|
||||
ancestorNodes
|
||||
.receive(on: DispatchQueue.main)
|
||||
.compactMap { [weak self] nodes -> [Item]? in
|
||||
|
@ -276,4 +312,3 @@ extension ThreadViewModel {
|
|||
}
|
||||
|
||||
}
|
||||
|
||||
|
|
|
@ -117,6 +117,14 @@ extension APIService {
|
|||
}
|
||||
}()
|
||||
if let status = oldStatus {
|
||||
let homeTimelineIndexes = status.homeTimelineIndexes ?? Set()
|
||||
for homeTimelineIndex in homeTimelineIndexes {
|
||||
self.backgroundManagedObjectContext.delete(homeTimelineIndex)
|
||||
}
|
||||
let inNotifications = status.inNotifications ?? Set()
|
||||
for notification in inNotifications {
|
||||
self.backgroundManagedObjectContext.delete(notification)
|
||||
}
|
||||
self.backgroundManagedObjectContext.delete(status)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -44,6 +44,10 @@ class AppContext: ObservableObject {
|
|||
private var documentStoreSubscription: AnyCancellable!
|
||||
|
||||
let overrideTraitCollection = CurrentValueSubject<UITraitCollection?, Never>(nil)
|
||||
let timestampUpdatePublisher = Timer.publish(every: 1.0, on: .main, in: .common)
|
||||
.autoconnect()
|
||||
.share()
|
||||
.eraseToAnyPublisher()
|
||||
|
||||
init() {
|
||||
let _coreDataStack = CoreDataStack()
|
||||
|
@ -205,7 +209,11 @@ extension AppContext {
|
|||
}
|
||||
.eraseToAnyPublisher()
|
||||
}
|
||||
//
|
||||
// os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s: purge temporary directory success", ((#file as NSString).lastPathComponent), #line, #function)
|
||||
// }
|
||||
|
||||
}
|
||||
|
||||
extension AppContext {
|
||||
@objc func toggleHomePreference(_ action: UIAction) {
|
||||
UserDefaults.shared.preferAsyncHomeTimeline.toggle()
|
||||
}
|
||||
}
|
||||
|
|
|
@ -9,6 +9,7 @@ import os.log
|
|||
import UIKit
|
||||
import UserNotifications
|
||||
import AppShared
|
||||
import AsyncDisplayKit
|
||||
|
||||
#if DEBUG
|
||||
import GDPerformanceView_Swift
|
||||
|
@ -32,7 +33,10 @@ class AppDelegate: UIResponder, UIApplicationDelegate {
|
|||
application.registerForRemoteNotifications()
|
||||
|
||||
#if DEBUG
|
||||
PerformanceMonitor.shared().start()
|
||||
// PerformanceMonitor.shared().start()
|
||||
// ASDisplayNode.shouldShowRangeDebugOverlay = true
|
||||
// ASControlNode.enableHitTestDebug = true
|
||||
// ASImageNode.shouldShowImageScalingOverlay = true
|
||||
#endif
|
||||
|
||||
return true
|
||||
|
|
|
@ -0,0 +1,71 @@
|
|||
// ref: https://github.com/Adlai-Holler/ASDKPlaceholderTest/blob/eea9fa7cff2d16a57efb47d208422ea9b49a630a/ASDKPlaceholderTest/ASDisplayNodeSubclasses.swift
|
||||
|
||||
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()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -57,7 +57,7 @@ class NotificationService: UNNotificationServiceExtension {
|
|||
bestAttemptContent.title = notification.title
|
||||
bestAttemptContent.subtitle = ""
|
||||
bestAttemptContent.body = notification.body
|
||||
bestAttemptContent.sound = .default
|
||||
bestAttemptContent.sound = UNNotificationSound.init(named: UNNotificationSoundName(rawValue: "BoopSound.caf"))
|
||||
bestAttemptContent.userInfo["plaintext"] = plaintextData
|
||||
|
||||
UserDefaults.shared.notificationBadgeCount += 1
|
||||
|
|
3
Podfile
3
Podfile
|
@ -8,7 +8,8 @@ target 'Mastodon' do
|
|||
|
||||
# UI
|
||||
pod 'UITextField+Shake', '~> 1.2'
|
||||
|
||||
pod 'Texture', '~> 3.0.0'
|
||||
|
||||
# misc
|
||||
pod 'SwiftGen', '~> 6.4.0'
|
||||
pod 'DateToolsSwift', '~> 5.0.0'
|
||||
|
|
46
Podfile.lock
46
Podfile.lock
|
@ -4,7 +4,42 @@ PODS:
|
|||
- GDPerformanceView-Swift (2.1.1)
|
||||
- Kanna (5.2.4)
|
||||
- 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:
|
||||
|
@ -14,6 +49,7 @@ DEPENDENCIES:
|
|||
- Kanna (~> 5.2.2)
|
||||
- Keys (from `Pods/CocoaPodsKeys`)
|
||||
- SwiftGen (~> 6.4.0)
|
||||
- Texture (~> 3.0.0)
|
||||
- "UITextField+Shake (~> 1.2)"
|
||||
|
||||
SPEC REPOS:
|
||||
|
@ -22,7 +58,11 @@ SPEC REPOS:
|
|||
- FLEX
|
||||
- GDPerformanceView-Swift
|
||||
- Kanna
|
||||
- PINCache
|
||||
- PINOperation
|
||||
- PINRemoteImage
|
||||
- SwiftGen
|
||||
- Texture
|
||||
- "UITextField+Shake"
|
||||
|
||||
EXTERNAL SOURCES:
|
||||
|
@ -35,9 +75,13 @@ SPEC CHECKSUMS:
|
|||
GDPerformanceView-Swift: 22d964fe40b19e3d914dba2586237d064de8fd77
|
||||
Kanna: b9d00d7c11428308c7f95e1f1f84b8205f567a8f
|
||||
Keys: a576f4c9c1c641ca913a959a9c62ed3f215a8de9
|
||||
PINCache: 7a8fc1a691173d21dbddbf86cd515de6efa55086
|
||||
PINOperation: 00c935935f1e8cf0d1e2d6b542e75b88fc3e5e20
|
||||
PINRemoteImage: f1295b29f8c5e640e25335a1b2bd9d805171bd01
|
||||
SwiftGen: 67860cc7c3cfc2ed25b9b74cfd55495fc89f9108
|
||||
Texture: 2f109e937850d94d1d07232041c9c7313ccddb81
|
||||
"UITextField+Shake": 298ac5a0f239d731bdab999b19b628c956ca0ac3
|
||||
|
||||
PODFILE CHECKSUM: 257c550231fcd1336a29f7835aa331171bb66ebd
|
||||
PODFILE CHECKSUM: ffa234348a25b078905180858ee4119fec0712d1
|
||||
|
||||
COCOAPODS: 1.10.1
|
||||
|
|
Loading…
Reference in New Issue