Compare commits
No commits in common. "67a029180e2b6661a8310f09ceefa56abd9b7897" and "b45dc198111ddbb7d9a77bded1f94e5b91a5ffc1" have entirely different histories.
67a029180e
...
b45dc19811
|
@ -116,15 +116,16 @@
|
||||||
D63569E023908A8D003DD353 /* StatusState.swift in Sources */ = {isa = PBXBuildFile; fileRef = D60A4FFB238B726A008AC647 /* StatusState.swift */; };
|
D63569E023908A8D003DD353 /* StatusState.swift in Sources */ = {isa = PBXBuildFile; fileRef = D60A4FFB238B726A008AC647 /* StatusState.swift */; };
|
||||||
D63661C02381C144004B9E16 /* PreferencesNavigationController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D63661BF2381C144004B9E16 /* PreferencesNavigationController.swift */; };
|
D63661C02381C144004B9E16 /* PreferencesNavigationController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D63661BF2381C144004B9E16 /* PreferencesNavigationController.swift */; };
|
||||||
D6370B9C24421FF30092A7FF /* Tusker.xcdatamodeld in Sources */ = {isa = PBXBuildFile; fileRef = D6370B9A24421FF30092A7FF /* Tusker.xcdatamodeld */; };
|
D6370B9C24421FF30092A7FF /* Tusker.xcdatamodeld in Sources */ = {isa = PBXBuildFile; fileRef = D6370B9A24421FF30092A7FF /* Tusker.xcdatamodeld */; };
|
||||||
D63A8D0B2561C27F00D9DFFF /* ProfileStatusesViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D63A8D0A2561C27F00D9DFFF /* ProfileStatusesViewController.swift */; };
|
|
||||||
D63F9C6E241D2D85004C03CF /* CompositionAttachment.swift in Sources */ = {isa = PBXBuildFile; fileRef = D63F9C6D241D2D85004C03CF /* CompositionAttachment.swift */; };
|
D63F9C6E241D2D85004C03CF /* CompositionAttachment.swift in Sources */ = {isa = PBXBuildFile; fileRef = D63F9C6D241D2D85004C03CF /* CompositionAttachment.swift */; };
|
||||||
D6403CC224A6B72D00E81C55 /* VisualEffectImageButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6403CC124A6B72D00E81C55 /* VisualEffectImageButton.swift */; };
|
D6403CC224A6B72D00E81C55 /* VisualEffectImageButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6403CC124A6B72D00E81C55 /* VisualEffectImageButton.swift */; };
|
||||||
D640D76922BAF5E6004FBE69 /* DomainBlocks.plist in Resources */ = {isa = PBXBuildFile; fileRef = D640D76822BAF5E6004FBE69 /* DomainBlocks.plist */; };
|
D640D76922BAF5E6004FBE69 /* DomainBlocks.plist in Resources */ = {isa = PBXBuildFile; fileRef = D640D76822BAF5E6004FBE69 /* DomainBlocks.plist */; };
|
||||||
D6412B0324AFF6A600F5412E /* TabBarScrollableViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6412B0224AFF6A600F5412E /* TabBarScrollableViewController.swift */; };
|
D6412B0324AFF6A600F5412E /* TabBarScrollableViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6412B0224AFF6A600F5412E /* TabBarScrollableViewController.swift */; };
|
||||||
D6412B0524B0227D00F5412E /* ProfileViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6412B0424B0227D00F5412E /* ProfileViewController.swift */; };
|
D6412B0524B0227D00F5412E /* ProfileViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6412B0424B0227D00F5412E /* ProfileViewController.swift */; };
|
||||||
|
D6412B0724B0237700F5412E /* ProfileStatusesViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6412B0624B0237700F5412E /* ProfileStatusesViewController.swift */; };
|
||||||
D6412B0924B0291E00F5412E /* MyProfileViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6412B0824B0291E00F5412E /* MyProfileViewController.swift */; };
|
D6412B0924B0291E00F5412E /* MyProfileViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6412B0824B0291E00F5412E /* MyProfileViewController.swift */; };
|
||||||
D6412B0B24B0D4C600F5412E /* ProfileHeaderView.xib in Resources */ = {isa = PBXBuildFile; fileRef = D6412B0A24B0D4C600F5412E /* ProfileHeaderView.xib */; };
|
D6412B0B24B0D4C600F5412E /* ProfileHeaderView.xib in Resources */ = {isa = PBXBuildFile; fileRef = D6412B0A24B0D4C600F5412E /* ProfileHeaderView.xib */; };
|
||||||
D6412B0D24B0D4CF00F5412E /* ProfileHeaderView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6412B0C24B0D4CF00F5412E /* ProfileHeaderView.swift */; };
|
D6412B0D24B0D4CF00F5412E /* ProfileHeaderView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6412B0C24B0D4CF00F5412E /* ProfileHeaderView.swift */; };
|
||||||
|
D641C773213CAA25004B4513 /* NotificationsTableViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D641C772213CAA25004B4513 /* NotificationsTableViewController.swift */; };
|
||||||
D641C77F213DC78A004B4513 /* InlineTextAttachment.swift in Sources */ = {isa = PBXBuildFile; fileRef = D641C77E213DC78A004B4513 /* InlineTextAttachment.swift */; };
|
D641C77F213DC78A004B4513 /* InlineTextAttachment.swift in Sources */ = {isa = PBXBuildFile; fileRef = D641C77E213DC78A004B4513 /* InlineTextAttachment.swift */; };
|
||||||
D6434EB3215B1856001A919A /* XCBRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6434EB2215B1856001A919A /* XCBRequest.swift */; };
|
D6434EB3215B1856001A919A /* XCBRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6434EB2215B1856001A919A /* XCBRequest.swift */; };
|
||||||
D646C956213B365700269FB5 /* LargeImageExpandAnimationController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D646C955213B365700269FB5 /* LargeImageExpandAnimationController.swift */; };
|
D646C956213B365700269FB5 /* LargeImageExpandAnimationController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D646C955213B365700269FB5 /* LargeImageExpandAnimationController.swift */; };
|
||||||
|
@ -141,9 +142,6 @@
|
||||||
D64D0AB12128D9AE005A6F37 /* OnboardingViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D64D0AB02128D9AE005A6F37 /* OnboardingViewController.swift */; };
|
D64D0AB12128D9AE005A6F37 /* OnboardingViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D64D0AB02128D9AE005A6F37 /* OnboardingViewController.swift */; };
|
||||||
D64D8CA92463B494006B0BAA /* MultiThreadDictionary.swift in Sources */ = {isa = PBXBuildFile; fileRef = D64D8CA82463B494006B0BAA /* MultiThreadDictionary.swift */; };
|
D64D8CA92463B494006B0BAA /* MultiThreadDictionary.swift in Sources */ = {isa = PBXBuildFile; fileRef = D64D8CA82463B494006B0BAA /* MultiThreadDictionary.swift */; };
|
||||||
D64F80E2215875CC00BEF393 /* XCBActionType.swift in Sources */ = {isa = PBXBuildFile; fileRef = D64F80E1215875CC00BEF393 /* XCBActionType.swift */; };
|
D64F80E2215875CC00BEF393 /* XCBActionType.swift in Sources */ = {isa = PBXBuildFile; fileRef = D64F80E1215875CC00BEF393 /* XCBActionType.swift */; };
|
||||||
D65234C9256189D0001AF9CF /* TimelineLikeTableViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D65234C8256189D0001AF9CF /* TimelineLikeTableViewController.swift */; };
|
|
||||||
D65234D325618EFA001AF9CF /* TimelineTableViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D65234D225618EFA001AF9CF /* TimelineTableViewController.swift */; };
|
|
||||||
D65234E12561AA68001AF9CF /* NotificationsTableViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D65234E02561AA68001AF9CF /* NotificationsTableViewController.swift */; };
|
|
||||||
D6531DEE246B81C9000F9538 /* GifvAttachmentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6531DED246B81C9000F9538 /* GifvAttachmentView.swift */; };
|
D6531DEE246B81C9000F9538 /* GifvAttachmentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6531DED246B81C9000F9538 /* GifvAttachmentView.swift */; };
|
||||||
D6531DF0246B867E000F9538 /* GifvAttachmentViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6531DEF246B867E000F9538 /* GifvAttachmentViewController.swift */; };
|
D6531DF0246B867E000F9538 /* GifvAttachmentViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6531DEF246B867E000F9538 /* GifvAttachmentViewController.swift */; };
|
||||||
D6538945214D6D7500E3CEFC /* TableViewSwipeActionProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6538944214D6D7500E3CEFC /* TableViewSwipeActionProvider.swift */; };
|
D6538945214D6D7500E3CEFC /* TableViewSwipeActionProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6538944214D6D7500E3CEFC /* TableViewSwipeActionProvider.swift */; };
|
||||||
|
@ -300,6 +298,7 @@
|
||||||
D6F1F84D2193B56E00F5FE67 /* Cache.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6F1F84C2193B56E00F5FE67 /* Cache.swift */; };
|
D6F1F84D2193B56E00F5FE67 /* Cache.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6F1F84C2193B56E00F5FE67 /* Cache.swift */; };
|
||||||
D6F2E965249E8BFD005846BB /* CrashReporterViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6F2E963249E8BFD005846BB /* CrashReporterViewController.swift */; };
|
D6F2E965249E8BFD005846BB /* CrashReporterViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6F2E963249E8BFD005846BB /* CrashReporterViewController.swift */; };
|
||||||
D6F2E966249E8BFD005846BB /* CrashReporterViewController.xib in Resources */ = {isa = PBXBuildFile; fileRef = D6F2E964249E8BFD005846BB /* CrashReporterViewController.xib */; };
|
D6F2E966249E8BFD005846BB /* CrashReporterViewController.xib in Resources */ = {isa = PBXBuildFile; fileRef = D6F2E964249E8BFD005846BB /* CrashReporterViewController.xib */; };
|
||||||
|
D6F953EC212519E700CF0F2B /* TimelineTableViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6F953EB212519E700CF0F2B /* TimelineTableViewController.swift */; };
|
||||||
D6F953F021251A2900CF0F2B /* MastodonController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6F953EF21251A2900CF0F2B /* MastodonController.swift */; };
|
D6F953F021251A2900CF0F2B /* MastodonController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6F953EF21251A2900CF0F2B /* MastodonController.swift */; };
|
||||||
D6FF9860255C717400845181 /* AccountSwitchingContainerViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6FF985F255C717400845181 /* AccountSwitchingContainerViewController.swift */; };
|
D6FF9860255C717400845181 /* AccountSwitchingContainerViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6FF985F255C717400845181 /* AccountSwitchingContainerViewController.swift */; };
|
||||||
/* End PBXBuildFile section */
|
/* End PBXBuildFile section */
|
||||||
|
@ -467,15 +466,16 @@
|
||||||
D6333B782139AEFD00CE884A /* Date+TimeAgo.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Date+TimeAgo.swift"; sourceTree = "<group>"; };
|
D6333B782139AEFD00CE884A /* Date+TimeAgo.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Date+TimeAgo.swift"; sourceTree = "<group>"; };
|
||||||
D63661BF2381C144004B9E16 /* PreferencesNavigationController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PreferencesNavigationController.swift; sourceTree = "<group>"; };
|
D63661BF2381C144004B9E16 /* PreferencesNavigationController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PreferencesNavigationController.swift; sourceTree = "<group>"; };
|
||||||
D6370B9B24421FF30092A7FF /* Tusker.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = Tusker.xcdatamodel; sourceTree = "<group>"; };
|
D6370B9B24421FF30092A7FF /* Tusker.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = Tusker.xcdatamodel; sourceTree = "<group>"; };
|
||||||
D63A8D0A2561C27F00D9DFFF /* ProfileStatusesViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfileStatusesViewController.swift; sourceTree = "<group>"; };
|
|
||||||
D63F9C6D241D2D85004C03CF /* CompositionAttachment.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CompositionAttachment.swift; sourceTree = "<group>"; };
|
D63F9C6D241D2D85004C03CF /* CompositionAttachment.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CompositionAttachment.swift; sourceTree = "<group>"; };
|
||||||
D6403CC124A6B72D00E81C55 /* VisualEffectImageButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VisualEffectImageButton.swift; sourceTree = "<group>"; };
|
D6403CC124A6B72D00E81C55 /* VisualEffectImageButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VisualEffectImageButton.swift; sourceTree = "<group>"; };
|
||||||
D640D76822BAF5E6004FBE69 /* DomainBlocks.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = DomainBlocks.plist; sourceTree = "<group>"; };
|
D640D76822BAF5E6004FBE69 /* DomainBlocks.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = DomainBlocks.plist; sourceTree = "<group>"; };
|
||||||
D6412B0224AFF6A600F5412E /* TabBarScrollableViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TabBarScrollableViewController.swift; sourceTree = "<group>"; };
|
D6412B0224AFF6A600F5412E /* TabBarScrollableViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TabBarScrollableViewController.swift; sourceTree = "<group>"; };
|
||||||
D6412B0424B0227D00F5412E /* ProfileViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfileViewController.swift; sourceTree = "<group>"; };
|
D6412B0424B0227D00F5412E /* ProfileViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfileViewController.swift; sourceTree = "<group>"; };
|
||||||
|
D6412B0624B0237700F5412E /* ProfileStatusesViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfileStatusesViewController.swift; sourceTree = "<group>"; };
|
||||||
D6412B0824B0291E00F5412E /* MyProfileViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MyProfileViewController.swift; sourceTree = "<group>"; };
|
D6412B0824B0291E00F5412E /* MyProfileViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MyProfileViewController.swift; sourceTree = "<group>"; };
|
||||||
D6412B0A24B0D4C600F5412E /* ProfileHeaderView.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = ProfileHeaderView.xib; sourceTree = "<group>"; };
|
D6412B0A24B0D4C600F5412E /* ProfileHeaderView.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = ProfileHeaderView.xib; sourceTree = "<group>"; };
|
||||||
D6412B0C24B0D4CF00F5412E /* ProfileHeaderView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfileHeaderView.swift; sourceTree = "<group>"; };
|
D6412B0C24B0D4CF00F5412E /* ProfileHeaderView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfileHeaderView.swift; sourceTree = "<group>"; };
|
||||||
|
D641C772213CAA25004B4513 /* NotificationsTableViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationsTableViewController.swift; sourceTree = "<group>"; };
|
||||||
D641C77E213DC78A004B4513 /* InlineTextAttachment.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InlineTextAttachment.swift; sourceTree = "<group>"; };
|
D641C77E213DC78A004B4513 /* InlineTextAttachment.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InlineTextAttachment.swift; sourceTree = "<group>"; };
|
||||||
D6434EB2215B1856001A919A /* XCBRequest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = XCBRequest.swift; sourceTree = "<group>"; };
|
D6434EB2215B1856001A919A /* XCBRequest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = XCBRequest.swift; sourceTree = "<group>"; };
|
||||||
D646C955213B365700269FB5 /* LargeImageExpandAnimationController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LargeImageExpandAnimationController.swift; sourceTree = "<group>"; };
|
D646C955213B365700269FB5 /* LargeImageExpandAnimationController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LargeImageExpandAnimationController.swift; sourceTree = "<group>"; };
|
||||||
|
@ -492,9 +492,6 @@
|
||||||
D64D0AB02128D9AE005A6F37 /* OnboardingViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OnboardingViewController.swift; sourceTree = "<group>"; };
|
D64D0AB02128D9AE005A6F37 /* OnboardingViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OnboardingViewController.swift; sourceTree = "<group>"; };
|
||||||
D64D8CA82463B494006B0BAA /* MultiThreadDictionary.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MultiThreadDictionary.swift; sourceTree = "<group>"; };
|
D64D8CA82463B494006B0BAA /* MultiThreadDictionary.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MultiThreadDictionary.swift; sourceTree = "<group>"; };
|
||||||
D64F80E1215875CC00BEF393 /* XCBActionType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = XCBActionType.swift; sourceTree = "<group>"; };
|
D64F80E1215875CC00BEF393 /* XCBActionType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = XCBActionType.swift; sourceTree = "<group>"; };
|
||||||
D65234C8256189D0001AF9CF /* TimelineLikeTableViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineLikeTableViewController.swift; sourceTree = "<group>"; };
|
|
||||||
D65234D225618EFA001AF9CF /* TimelineTableViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineTableViewController.swift; sourceTree = "<group>"; };
|
|
||||||
D65234E02561AA68001AF9CF /* NotificationsTableViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationsTableViewController.swift; sourceTree = "<group>"; };
|
|
||||||
D6531DED246B81C9000F9538 /* GifvAttachmentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GifvAttachmentView.swift; sourceTree = "<group>"; };
|
D6531DED246B81C9000F9538 /* GifvAttachmentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GifvAttachmentView.swift; sourceTree = "<group>"; };
|
||||||
D6531DEF246B867E000F9538 /* GifvAttachmentViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GifvAttachmentViewController.swift; sourceTree = "<group>"; };
|
D6531DEF246B867E000F9538 /* GifvAttachmentViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GifvAttachmentViewController.swift; sourceTree = "<group>"; };
|
||||||
D6538944214D6D7500E3CEFC /* TableViewSwipeActionProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TableViewSwipeActionProvider.swift; sourceTree = "<group>"; };
|
D6538944214D6D7500E3CEFC /* TableViewSwipeActionProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TableViewSwipeActionProvider.swift; sourceTree = "<group>"; };
|
||||||
|
@ -657,6 +654,7 @@
|
||||||
D6F1F84C2193B56E00F5FE67 /* Cache.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Cache.swift; sourceTree = "<group>"; };
|
D6F1F84C2193B56E00F5FE67 /* Cache.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Cache.swift; sourceTree = "<group>"; };
|
||||||
D6F2E963249E8BFD005846BB /* CrashReporterViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CrashReporterViewController.swift; sourceTree = "<group>"; };
|
D6F2E963249E8BFD005846BB /* CrashReporterViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CrashReporterViewController.swift; sourceTree = "<group>"; };
|
||||||
D6F2E964249E8BFD005846BB /* CrashReporterViewController.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = CrashReporterViewController.xib; sourceTree = "<group>"; };
|
D6F2E964249E8BFD005846BB /* CrashReporterViewController.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = CrashReporterViewController.xib; sourceTree = "<group>"; };
|
||||||
|
D6F953EB212519E700CF0F2B /* TimelineTableViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineTableViewController.swift; sourceTree = "<group>"; };
|
||||||
D6F953EF21251A2900CF0F2B /* MastodonController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MastodonController.swift; sourceTree = "<group>"; };
|
D6F953EF21251A2900CF0F2B /* MastodonController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MastodonController.swift; sourceTree = "<group>"; };
|
||||||
D6FF985F255C717400845181 /* AccountSwitchingContainerViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountSwitchingContainerViewController.swift; sourceTree = "<group>"; };
|
D6FF985F255C717400845181 /* AccountSwitchingContainerViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountSwitchingContainerViewController.swift; sourceTree = "<group>"; };
|
||||||
/* End PBXFileReference section */
|
/* End PBXFileReference section */
|
||||||
|
@ -975,7 +973,7 @@
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
D6BC9DD6232D7811002CA326 /* TimelinesPageViewController.swift */,
|
D6BC9DD6232D7811002CA326 /* TimelinesPageViewController.swift */,
|
||||||
D65234D225618EFA001AF9CF /* TimelineTableViewController.swift */,
|
D6F953EB212519E700CF0F2B /* TimelineTableViewController.swift */,
|
||||||
D6945C3123AC4D36005C403C /* HashtagTimelineViewController.swift */,
|
D6945C3123AC4D36005C403C /* HashtagTimelineViewController.swift */,
|
||||||
D6945C3723AC739F005C403C /* InstanceTimelineViewController.swift */,
|
D6945C3723AC739F005C403C /* InstanceTimelineViewController.swift */,
|
||||||
);
|
);
|
||||||
|
@ -1007,7 +1005,7 @@
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
D6412B0424B0227D00F5412E /* ProfileViewController.swift */,
|
D6412B0424B0227D00F5412E /* ProfileViewController.swift */,
|
||||||
D63A8D0A2561C27F00D9DFFF /* ProfileStatusesViewController.swift */,
|
D6412B0624B0237700F5412E /* ProfileStatusesViewController.swift */,
|
||||||
D6412B0824B0291E00F5412E /* MyProfileViewController.swift */,
|
D6412B0824B0291E00F5412E /* MyProfileViewController.swift */,
|
||||||
);
|
);
|
||||||
path = Profile;
|
path = Profile;
|
||||||
|
@ -1025,7 +1023,7 @@
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
D6BC9DB0232C61BC002CA326 /* NotificationsPageViewController.swift */,
|
D6BC9DB0232C61BC002CA326 /* NotificationsPageViewController.swift */,
|
||||||
D65234E02561AA68001AF9CF /* NotificationsTableViewController.swift */,
|
D641C772213CAA25004B4513 /* NotificationsTableViewController.swift */,
|
||||||
);
|
);
|
||||||
path = Notifications;
|
path = Notifications;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
|
@ -1374,7 +1372,6 @@
|
||||||
D6412B0224AFF6A600F5412E /* TabBarScrollableViewController.swift */,
|
D6412B0224AFF6A600F5412E /* TabBarScrollableViewController.swift */,
|
||||||
D65C6BF425478A9C00A6E89C /* BackgroundableViewController.swift */,
|
D65C6BF425478A9C00A6E89C /* BackgroundableViewController.swift */,
|
||||||
D6B81F3B2560365300F6E31D /* RefreshableViewController.swift */,
|
D6B81F3B2560365300F6E31D /* RefreshableViewController.swift */,
|
||||||
D65234C8256189D0001AF9CF /* TimelineLikeTableViewController.swift */,
|
|
||||||
);
|
);
|
||||||
path = Utilities;
|
path = Utilities;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
|
@ -1854,7 +1851,6 @@
|
||||||
D6BC9DB3232D4C07002CA326 /* WellnessPrefsView.swift in Sources */,
|
D6BC9DB3232D4C07002CA326 /* WellnessPrefsView.swift in Sources */,
|
||||||
D64BC18F23C18B9D000D0238 /* FollowRequestNotificationTableViewCell.swift in Sources */,
|
D64BC18F23C18B9D000D0238 /* FollowRequestNotificationTableViewCell.swift in Sources */,
|
||||||
D693DE5923FE24310061E07D /* InteractivePushTransition.swift in Sources */,
|
D693DE5923FE24310061E07D /* InteractivePushTransition.swift in Sources */,
|
||||||
D65234C9256189D0001AF9CF /* TimelineLikeTableViewController.swift in Sources */,
|
|
||||||
D6A3BC8A2321F79B00FD64D5 /* AccountTableViewCell.swift in Sources */,
|
D6A3BC8A2321F79B00FD64D5 /* AccountTableViewCell.swift in Sources */,
|
||||||
D66A77BB233838DC0058F1EC /* UIFont+Traits.swift in Sources */,
|
D66A77BB233838DC0058F1EC /* UIFont+Traits.swift in Sources */,
|
||||||
D68FEC4F232C5BC300C84F23 /* SegmentedPageViewController.swift in Sources */,
|
D68FEC4F232C5BC300C84F23 /* SegmentedPageViewController.swift in Sources */,
|
||||||
|
@ -1869,6 +1865,7 @@
|
||||||
D6F953F021251A2900CF0F2B /* MastodonController.swift in Sources */,
|
D6F953F021251A2900CF0F2B /* MastodonController.swift in Sources */,
|
||||||
0411610022B442870030A9B7 /* LoadingLargeImageViewController.swift in Sources */,
|
0411610022B442870030A9B7 /* LoadingLargeImageViewController.swift in Sources */,
|
||||||
D6A4DCE525537C7A00D9DE31 /* FastSwitchingAccountView.swift in Sources */,
|
D6A4DCE525537C7A00D9DE31 /* FastSwitchingAccountView.swift in Sources */,
|
||||||
|
D6412B0724B0237700F5412E /* ProfileStatusesViewController.swift in Sources */,
|
||||||
D6A4DCCC2553667800D9DE31 /* FastAccountSwitcherViewController.swift in Sources */,
|
D6A4DCCC2553667800D9DE31 /* FastAccountSwitcherViewController.swift in Sources */,
|
||||||
D611C2CF232DC61100C86A49 /* HashtagTableViewCell.swift in Sources */,
|
D611C2CF232DC61100C86A49 /* HashtagTableViewCell.swift in Sources */,
|
||||||
D627944F23A9C99800D38C68 /* EditListAccountsViewController.swift in Sources */,
|
D627944F23A9C99800D38C68 /* EditListAccountsViewController.swift in Sources */,
|
||||||
|
@ -1890,7 +1887,6 @@
|
||||||
D6CA6A92249FAD8900AD45C1 /* AudioSessionHelper.swift in Sources */,
|
D6CA6A92249FAD8900AD45C1 /* AudioSessionHelper.swift in Sources */,
|
||||||
D6C143FD25354FD0007DC240 /* EmojiCollectionViewCell.swift in Sources */,
|
D6C143FD25354FD0007DC240 /* EmojiCollectionViewCell.swift in Sources */,
|
||||||
D60D2B8223844C71001B87A3 /* BaseStatusTableViewCell.swift in Sources */,
|
D60D2B8223844C71001B87A3 /* BaseStatusTableViewCell.swift in Sources */,
|
||||||
D63A8D0B2561C27F00D9DFFF /* ProfileStatusesViewController.swift in Sources */,
|
|
||||||
D60E2F272442372B005F8713 /* StatusMO.swift in Sources */,
|
D60E2F272442372B005F8713 /* StatusMO.swift in Sources */,
|
||||||
D60E2F2C24423EAD005F8713 /* LazilyDecoding.swift in Sources */,
|
D60E2F2C24423EAD005F8713 /* LazilyDecoding.swift in Sources */,
|
||||||
D677284A24ECBDF400C732D3 /* ComposeCurrentAccount.swift in Sources */,
|
D677284A24ECBDF400C732D3 /* ComposeCurrentAccount.swift in Sources */,
|
||||||
|
@ -1992,7 +1988,6 @@
|
||||||
D6FF9860255C717400845181 /* AccountSwitchingContainerViewController.swift in Sources */,
|
D6FF9860255C717400845181 /* AccountSwitchingContainerViewController.swift in Sources */,
|
||||||
D670F8B62537DC890046588A /* EmojiPickerWrapper.swift in Sources */,
|
D670F8B62537DC890046588A /* EmojiPickerWrapper.swift in Sources */,
|
||||||
D6B81F442560390300F6E31D /* MenuController.swift in Sources */,
|
D6B81F442560390300F6E31D /* MenuController.swift in Sources */,
|
||||||
D65234E12561AA68001AF9CF /* NotificationsTableViewController.swift in Sources */,
|
|
||||||
D67C57AF21E28EAD00C3118B /* Array+Uniques.swift in Sources */,
|
D67C57AF21E28EAD00C3118B /* Array+Uniques.swift in Sources */,
|
||||||
D6945C3223AC4D36005C403C /* HashtagTimelineViewController.swift in Sources */,
|
D6945C3223AC4D36005C403C /* HashtagTimelineViewController.swift in Sources */,
|
||||||
D647D92824257BEB0005044F /* AttachmentPreviewViewController.swift in Sources */,
|
D647D92824257BEB0005044F /* AttachmentPreviewViewController.swift in Sources */,
|
||||||
|
@ -2002,6 +1997,7 @@
|
||||||
D681E4D7246E32290053414F /* StatusActivityItemSource.swift in Sources */,
|
D681E4D7246E32290053414F /* StatusActivityItemSource.swift in Sources */,
|
||||||
D646C95A213B5D0500269FB5 /* LargeImageInteractionController.swift in Sources */,
|
D646C95A213B5D0500269FB5 /* LargeImageInteractionController.swift in Sources */,
|
||||||
D6A3BC7C232195C600FD64D5 /* ActionNotificationGroupTableViewCell.swift in Sources */,
|
D6A3BC7C232195C600FD64D5 /* ActionNotificationGroupTableViewCell.swift in Sources */,
|
||||||
|
D6F953EC212519E700CF0F2B /* TimelineTableViewController.swift in Sources */,
|
||||||
D62275A824F1CA2800B82A16 /* ComposeReplyContentView.swift in Sources */,
|
D62275A824F1CA2800B82A16 /* ComposeReplyContentView.swift in Sources */,
|
||||||
D677284824ECBCB100C732D3 /* ComposeView.swift in Sources */,
|
D677284824ECBCB100C732D3 /* ComposeView.swift in Sources */,
|
||||||
D68232F72464F4FD00325FB8 /* ComposeDrawingViewController.swift in Sources */,
|
D68232F72464F4FD00325FB8 /* ComposeDrawingViewController.swift in Sources */,
|
||||||
|
@ -2011,12 +2007,12 @@
|
||||||
D68015402401A6BA00D6103B /* ComposingPrefsView.swift in Sources */,
|
D68015402401A6BA00D6103B /* ComposingPrefsView.swift in Sources */,
|
||||||
D67895BC24671E6D00D4CD9E /* PKDrawing+Render.swift in Sources */,
|
D67895BC24671E6D00D4CD9E /* PKDrawing+Render.swift in Sources */,
|
||||||
04D14BB022B34A2800642648 /* GalleryViewController.swift in Sources */,
|
04D14BB022B34A2800642648 /* GalleryViewController.swift in Sources */,
|
||||||
|
D641C773213CAA25004B4513 /* NotificationsTableViewController.swift in Sources */,
|
||||||
D6C1B2082545D1EC00DAAA66 /* StatusCardView.swift in Sources */,
|
D6C1B2082545D1EC00DAAA66 /* StatusCardView.swift in Sources */,
|
||||||
D6412B0524B0227D00F5412E /* ProfileViewController.swift in Sources */,
|
D6412B0524B0227D00F5412E /* ProfileViewController.swift in Sources */,
|
||||||
D64BC18A23C16487000D0238 /* UnpinStatusActivity.swift in Sources */,
|
D64BC18A23C16487000D0238 /* UnpinStatusActivity.swift in Sources */,
|
||||||
D64D8CA92463B494006B0BAA /* MultiThreadDictionary.swift in Sources */,
|
D64D8CA92463B494006B0BAA /* MultiThreadDictionary.swift in Sources */,
|
||||||
D6757A7C2157E01900721E32 /* XCBManager.swift in Sources */,
|
D6757A7C2157E01900721E32 /* XCBManager.swift in Sources */,
|
||||||
D65234D325618EFA001AF9CF /* TimelineTableViewController.swift in Sources */,
|
|
||||||
D68E6F5F253C9B2D001A1B4C /* BaseEmojiLabel.swift in Sources */,
|
D68E6F5F253C9B2D001A1B4C /* BaseEmojiLabel.swift in Sources */,
|
||||||
D6F1F84D2193B56E00F5FE67 /* Cache.swift in Sources */,
|
D6F1F84D2193B56E00F5FE67 /* Cache.swift in Sources */,
|
||||||
D6F0B12B24A3071C001E48C3 /* MainSplitViewController.swift in Sources */,
|
D6F0B12B24A3071C001E48C3 /* MainSplitViewController.swift in Sources */,
|
||||||
|
|
|
@ -58,7 +58,8 @@ class ListTimelineViewController: TimelineTableViewController {
|
||||||
dismiss(animated: true)
|
dismiss(animated: true)
|
||||||
|
|
||||||
// todo: show loading indicator
|
// todo: show loading indicator
|
||||||
reloadInitialItems()
|
timelineSegments = []
|
||||||
|
loadInitialStatuses()
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -53,11 +53,7 @@ class MainSplitViewController: UISplitViewController {
|
||||||
primaryBackgroundStyle = .sidebar
|
primaryBackgroundStyle = .sidebar
|
||||||
|
|
||||||
setViewController(EnhancedNavigationViewController(), for: .secondary)
|
setViewController(EnhancedNavigationViewController(), for: .secondary)
|
||||||
// don't unnecesarily construct a content VC unless the we're in actually split mode
|
|
||||||
// when we change from compact -> split for the first time, the VC will be transferred anyways
|
|
||||||
if traitCollection.horizontalSizeClass != .compact {
|
|
||||||
select(item: .tab(.timelines))
|
select(item: .tab(.timelines))
|
||||||
}
|
|
||||||
|
|
||||||
tabBarViewController = MainTabBarViewController(mastodonController: mastodonController)
|
tabBarViewController = MainTabBarViewController(mastodonController: mastodonController)
|
||||||
setViewController(tabBarViewController, for: .compact)
|
setViewController(tabBarViewController, for: .compact)
|
||||||
|
|
|
@ -9,7 +9,7 @@
|
||||||
import UIKit
|
import UIKit
|
||||||
import Pachyderm
|
import Pachyderm
|
||||||
|
|
||||||
class NotificationsTableViewController: TimelineLikeTableViewController<NotificationGroup> {
|
class NotificationsTableViewController: EnhancedTableViewController {
|
||||||
|
|
||||||
private let statusCell = "statusCell"
|
private let statusCell = "statusCell"
|
||||||
private let actionGroupCell = "actionGroupCell"
|
private let actionGroupCell = "actionGroupCell"
|
||||||
|
@ -20,112 +20,125 @@ class NotificationsTableViewController: TimelineLikeTableViewController<Notifica
|
||||||
weak var mastodonController: MastodonController!
|
weak var mastodonController: MastodonController!
|
||||||
|
|
||||||
private let excludedTypes: [Pachyderm.Notification.Kind]
|
private let excludedTypes: [Pachyderm.Notification.Kind]
|
||||||
private let groupTypes = [Pachyderm.Notification.Kind.favourite, .reblog, .follow]
|
private let groupTypes = [Notification.Kind.favourite, .reblog, .follow]
|
||||||
|
|
||||||
|
private var loaded = false
|
||||||
|
|
||||||
|
private var groups: [NotificationGroup] = []
|
||||||
|
|
||||||
|
private let pageSize = 20
|
||||||
private var newer: RequestRange?
|
private var newer: RequestRange?
|
||||||
private var older: RequestRange?
|
private var older: RequestRange?
|
||||||
|
|
||||||
|
private var lastLastVisibleRow: IndexPath?
|
||||||
|
|
||||||
init(allowedTypes: [Pachyderm.Notification.Kind], mastodonController: MastodonController) {
|
init(allowedTypes: [Pachyderm.Notification.Kind], mastodonController: MastodonController) {
|
||||||
self.excludedTypes = Array(Set(Pachyderm.Notification.Kind.allCases).subtracting(allowedTypes))
|
self.excludedTypes = Array(Set(Pachyderm.Notification.Kind.allCases).subtracting(allowedTypes))
|
||||||
self.mastodonController = mastodonController
|
self.mastodonController = mastodonController
|
||||||
|
|
||||||
super.init()
|
super.init(style: .plain)
|
||||||
|
|
||||||
|
#if !targetEnvironment(macCatalyst)
|
||||||
|
self.refreshControl = UIRefreshControl()
|
||||||
|
refreshControl!.addTarget(self, action: #selector(refreshNotifications), for: .valueChanged)
|
||||||
|
#endif
|
||||||
|
|
||||||
|
addKeyCommand(MenuController.refreshCommand(discoverabilityTitle: NSLocalizedString("Refresh Notifications", comment: "refresh notifications command discoverability title")))
|
||||||
}
|
}
|
||||||
|
|
||||||
required init?(coder: NSCoder) {
|
required init?(coder aDecoder: NSCoder) {
|
||||||
fatalError("init(coder:) has not been implemented")
|
fatalError("init(coder:) has not been implemented")
|
||||||
}
|
}
|
||||||
|
|
||||||
override class func refreshCommandTitle() -> String {
|
|
||||||
return NSLocalizedString("Refresh Notifications", comment: "refresh notifications command discoverability title")
|
|
||||||
}
|
|
||||||
|
|
||||||
override func viewDidLoad() {
|
override func viewDidLoad() {
|
||||||
super.viewDidLoad()
|
super.viewDidLoad()
|
||||||
|
|
||||||
|
tableView.rowHeight = UITableView.automaticDimension
|
||||||
|
tableView.estimatedRowHeight = 140
|
||||||
|
|
||||||
tableView.register(UINib(nibName: "TimelineStatusTableViewCell", bundle: .main), forCellReuseIdentifier: statusCell)
|
tableView.register(UINib(nibName: "TimelineStatusTableViewCell", bundle: .main), forCellReuseIdentifier: statusCell)
|
||||||
tableView.register(UINib(nibName: "ActionNotificationGroupTableViewCell", bundle: .main), forCellReuseIdentifier: actionGroupCell)
|
tableView.register(UINib(nibName: "ActionNotificationGroupTableViewCell", bundle: .main), forCellReuseIdentifier: actionGroupCell)
|
||||||
tableView.register(UINib(nibName: "FollowNotificationGroupTableViewCell", bundle: .main), forCellReuseIdentifier: followGroupCell)
|
tableView.register(UINib(nibName: "FollowNotificationGroupTableViewCell", bundle: .main), forCellReuseIdentifier: followGroupCell)
|
||||||
tableView.register(UINib(nibName: "FollowRequestNotificationTableViewCell", bundle: .main), forCellReuseIdentifier: followRequestCell)
|
tableView.register(UINib(nibName: "FollowRequestNotificationTableViewCell", bundle: .main), forCellReuseIdentifier: followRequestCell)
|
||||||
tableView.register(UINib(nibName: "BasicTableViewCell", bundle: .main), forCellReuseIdentifier: unknownCell)
|
tableView.register(UINib(nibName: "BasicTableViewCell", bundle: .main), forCellReuseIdentifier: unknownCell)
|
||||||
|
|
||||||
|
tableView.prefetchDataSource = self
|
||||||
}
|
}
|
||||||
|
|
||||||
override func loadInitialItems(completion: @escaping ([NotificationGroup]) -> Void) {
|
override func viewWillAppear(_ animated: Bool) {
|
||||||
|
super.viewWillAppear(animated)
|
||||||
|
|
||||||
|
if !loaded {
|
||||||
|
loaded = true
|
||||||
|
|
||||||
let request = Client.getNotifications(excludeTypes: excludedTypes)
|
let request = Client.getNotifications(excludeTypes: excludedTypes)
|
||||||
mastodonController.run(request) { (response) in
|
mastodonController.run(request) { result in
|
||||||
guard case let .success(notifications, pagination) = response else { fatalError() }
|
guard case let .success(notifications, pagination) = result else { fatalError() }
|
||||||
|
|
||||||
let groups = NotificationGroup.createGroups(notifications: notifications, only: self.groupTypes)
|
let groups = NotificationGroup.createGroups(notifications: notifications, only: self.groupTypes)
|
||||||
|
|
||||||
|
self.groups.append(contentsOf: groups)
|
||||||
|
|
||||||
self.newer = pagination?.newer
|
self.newer = pagination?.newer
|
||||||
self.older = pagination?.older
|
self.older = pagination?.older
|
||||||
|
|
||||||
self.mastodonController.persistentContainer.addAll(notifications: notifications) {
|
self.mastodonController.persistentContainer.addAll(notifications: notifications) {
|
||||||
completion(groups)
|
DispatchQueue.main.async {
|
||||||
|
self.tableView.reloadData()
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override func loadOlder(completion: @escaping ([NotificationGroup]) -> Void) {
|
override func viewWillDisappear(_ animated: Bool) {
|
||||||
guard let older = older else {
|
super.viewWillDisappear(animated)
|
||||||
completion([])
|
|
||||||
|
pruneOffscreenRows()
|
||||||
|
}
|
||||||
|
|
||||||
|
private func pruneOffscreenRows() {
|
||||||
|
guard let lastVisibleRow = lastLastVisibleRow else {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
let lastRowIndex = groups.count - 1
|
||||||
|
|
||||||
let request = Client.getNotifications(excludeTypes: excludedTypes, range: older)
|
if lastVisibleRow.row < lastRowIndex - pageSize {
|
||||||
mastodonController.run(request) { (response) in
|
// if there are more than 20 rows below the lats visible one
|
||||||
guard case let .success(newNotifications, pagination) = response else { fatalError() }
|
|
||||||
|
|
||||||
self.older = pagination?.older
|
let rowIndicesToRemove = (lastVisibleRow.row + pageSize)..<groups.count
|
||||||
|
|
||||||
let groups = NotificationGroup.createGroups(notifications: newNotifications, only: self.groupTypes)
|
let groupsToRemove = groups[rowIndicesToRemove]
|
||||||
|
for group in groupsToRemove {
|
||||||
self.mastodonController.persistentContainer.addAll(notifications: newNotifications) {
|
for notification in group.notifications {
|
||||||
completion(groups)
|
if let id = notification.status?.id {
|
||||||
|
mastodonController.persistentContainer.status(for: id)?.decrementReferenceCount()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override func loadNewer(completion: @escaping ([NotificationGroup]) -> Void) {
|
groups.removeSubrange(rowIndicesToRemove)
|
||||||
guard let newer = newer else {
|
|
||||||
completion([])
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
let request = Client.getNotifications(excludeTypes: excludedTypes, range: newer)
|
let removedIndexPaths = rowIndicesToRemove.map { IndexPath(row: $0, section: 0) }
|
||||||
mastodonController.run(request) { (response) in
|
UIView.performWithoutAnimation {
|
||||||
guard case let .success(newNotifications, pagination) = response else { fatalError() }
|
tableView.deleteRows(at: removedIndexPaths, with: .none)
|
||||||
|
|
||||||
self.newer = pagination?.newer
|
|
||||||
|
|
||||||
let groups = NotificationGroup.createGroups(notifications: newNotifications, only: self.groupTypes)
|
|
||||||
|
|
||||||
self.mastodonController.persistentContainer.addAll(notifications: newNotifications) {
|
|
||||||
completion(groups)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private func dismissNotificationsInGroup(at indexPath: IndexPath, completion: (() -> Void)? = nil) {
|
// MARK: - Table view data source
|
||||||
let group = DispatchGroup()
|
|
||||||
item(for: indexPath).notifications
|
override func numberOfSections(in tableView: UITableView) -> Int {
|
||||||
.map { Pachyderm.Notification.dismiss(id: $0.id) }
|
return 1
|
||||||
.forEach { (request) in
|
|
||||||
group.enter()
|
|
||||||
mastodonController.run(request) { (_) in
|
|
||||||
group.leave()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
group.notify(queue: .main) {
|
|
||||||
self.sections[indexPath.section].remove(at: indexPath.row)
|
|
||||||
self.tableView.deleteRows(at: [indexPath], with: .automatic)
|
|
||||||
completion?()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - UITableViewDataSource
|
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
|
||||||
|
return groups.count
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
|
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
|
||||||
let group = item(for: indexPath)
|
let group = groups[indexPath.row]
|
||||||
|
|
||||||
switch group.kind {
|
switch group.kind {
|
||||||
case .mention:
|
case .mention:
|
||||||
|
@ -163,7 +176,45 @@ class NotificationsTableViewController: TimelineLikeTableViewController<Notifica
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - UITableViewDelegate
|
// MARK: - Table view delegate
|
||||||
|
|
||||||
|
override func tableView(_ tableView: UITableView, willDisplay cell: UITableViewCell, forRowAt indexPath: IndexPath) {
|
||||||
|
lastLastVisibleRow = tableView.indexPathsForVisibleRows?.last
|
||||||
|
|
||||||
|
if indexPath.row == groups.count - 1 {
|
||||||
|
guard let older = older else { return }
|
||||||
|
|
||||||
|
let request = Client.getNotifications(excludeTypes: excludedTypes, range: older)
|
||||||
|
mastodonController.run(request) { result in
|
||||||
|
guard case let .success(newNotifications, pagination) = result else { fatalError() }
|
||||||
|
|
||||||
|
let groups = NotificationGroup.createGroups(notifications: newNotifications, only: self.groupTypes)
|
||||||
|
|
||||||
|
let newIndexPaths = (self.groups.count..<(self.groups.count + groups.count)).map {
|
||||||
|
IndexPath(row: $0, section: 0)
|
||||||
|
}
|
||||||
|
self.groups.append(contentsOf: groups)
|
||||||
|
|
||||||
|
self.older = pagination?.older
|
||||||
|
|
||||||
|
self.mastodonController.persistentContainer.addAll(notifications: newNotifications) {
|
||||||
|
DispatchQueue.main.async {
|
||||||
|
UIView.performWithoutAnimation {
|
||||||
|
self.tableView.insertRows(at: newIndexPaths, with: .automatic)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override func tableView(_ tableView: UITableView, canEditRowAt indexPath: IndexPath) -> Bool {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
override func tableView(_ tableView: UITableView, leadingSwipeActionsConfigurationForRowAt indexPath: IndexPath) -> UISwipeActionsConfiguration? {
|
||||||
|
return (tableView.cellForRow(at: indexPath) as? TableViewSwipeActionProvider)?.leadingSwipeActionsConfiguration()
|
||||||
|
}
|
||||||
|
|
||||||
override func tableView(_ tableView: UITableView, trailingSwipeActionsConfigurationForRowAt indexPath: IndexPath) -> UISwipeActionsConfiguration? {
|
override func tableView(_ tableView: UITableView, trailingSwipeActionsConfigurationForRowAt indexPath: IndexPath) -> UISwipeActionsConfiguration? {
|
||||||
let dismissAction = UIContextualAction(style: .destructive, title: NSLocalizedString("Dismiss", comment: "dismiss notification swipe action title")) { (action, view, completion) in
|
let dismissAction = UIContextualAction(style: .destructive, title: NSLocalizedString("Dismiss", comment: "dismiss notification swipe action title")) { (action, view, completion) in
|
||||||
|
@ -172,9 +223,7 @@ class NotificationsTableViewController: TimelineLikeTableViewController<Notifica
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
dismissAction.image = UIImage(systemName: "clear.fill")
|
dismissAction.image = UIImage(systemName: "clear.fill")
|
||||||
|
|
||||||
let cellConfiguration = (tableView.cellForRow(at: indexPath) as? TableViewSwipeActionProvider)?.trailingSwipeActionsConfiguration()
|
let cellConfiguration = (tableView.cellForRow(at: indexPath) as? TableViewSwipeActionProvider)?.trailingSwipeActionsConfiguration()
|
||||||
|
|
||||||
let config: UISwipeActionsConfiguration
|
let config: UISwipeActionsConfiguration
|
||||||
if let cellConfiguration = cellConfiguration {
|
if let cellConfiguration = cellConfiguration {
|
||||||
config = UISwipeActionsConfiguration(actions: cellConfiguration.actions + [dismissAction])
|
config = UISwipeActionsConfiguration(actions: cellConfiguration.actions + [dismissAction])
|
||||||
|
@ -188,27 +237,78 @@ class NotificationsTableViewController: TimelineLikeTableViewController<Notifica
|
||||||
|
|
||||||
override func getSuggestedContextMenuActions(tableView: UITableView, indexPath: IndexPath, point: CGPoint) -> [UIAction] {
|
override func getSuggestedContextMenuActions(tableView: UITableView, indexPath: IndexPath, point: CGPoint) -> [UIAction] {
|
||||||
return [
|
return [
|
||||||
UIAction(title: "Dismiss Notification", image: UIImage(systemName: "clear.fill"), identifier: .init("dismissnotification"), handler: { (_) in
|
UIAction(title: "Dismiss Notification", image: UIImage(systemName: "clear.fill"), identifier: .init("dismissnotification"), discoverabilityTitle: nil, attributes: [], state: .off, handler: { (_) in
|
||||||
self.dismissNotificationsInGroup(at: indexPath)
|
self.dismissNotificationsInGroup(at: indexPath)
|
||||||
})
|
})
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func dismissNotificationsInGroup(at indexPath: IndexPath, completion: (() -> Void)? = nil) {
|
||||||
|
let group = DispatchGroup()
|
||||||
|
groups[indexPath.row].notifications
|
||||||
|
.map { Pachyderm.Notification.dismiss(id: $0.id) }
|
||||||
|
.forEach { (request) in
|
||||||
|
group.enter()
|
||||||
|
mastodonController.run(request) { (response) in
|
||||||
|
group.leave()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
group.notify(queue: .main) {
|
||||||
|
self.groups.remove(at: indexPath.row)
|
||||||
|
self.tableView.deleteRows(at: [indexPath], with: .automatic)
|
||||||
|
completion?()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@objc func refreshNotifications() {
|
||||||
|
guard let newer = newer else { return }
|
||||||
|
|
||||||
|
let request = Client.getNotifications(excludeTypes: excludedTypes, range: newer)
|
||||||
|
mastodonController.run(request) { result in
|
||||||
|
guard case let .success(newNotifications, pagination) = result else { fatalError() }
|
||||||
|
|
||||||
|
let groups = NotificationGroup.createGroups(notifications: newNotifications, only: self.groupTypes)
|
||||||
|
|
||||||
|
self.groups.insert(contentsOf: groups, at: 0)
|
||||||
|
|
||||||
|
if let newer = pagination?.newer {
|
||||||
|
self.newer = newer
|
||||||
|
}
|
||||||
|
|
||||||
|
self.mastodonController.persistentContainer.addAll(notifications: newNotifications) {
|
||||||
|
DispatchQueue.main.async {
|
||||||
|
let newIndexPaths = (0..<groups.count).map {
|
||||||
|
IndexPath(row: $0, section: 0)
|
||||||
|
}
|
||||||
|
UIView.performWithoutAnimation {
|
||||||
|
self.tableView.insertRows(at: newIndexPaths, with: .automatic)
|
||||||
|
}
|
||||||
|
|
||||||
|
self.refreshControl?.endRefreshing()
|
||||||
|
|
||||||
|
// maintain the current position in the list (don't scroll to top)
|
||||||
|
self.tableView.scrollToRow(at: IndexPath(row: newNotifications.count, section: 0), at: .top, animated: false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
extension NotificationsTableViewController: TuskerNavigationDelegate {
|
|
||||||
var apiController: MastodonController { mastodonController }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
extension NotificationsTableViewController: StatusTableViewCellDelegate {
|
extension NotificationsTableViewController: StatusTableViewCellDelegate {
|
||||||
|
var apiController: MastodonController { mastodonController }
|
||||||
func statusCellCollapsedStateChanged(_ cell: BaseStatusTableViewCell) {
|
func statusCellCollapsedStateChanged(_ cell: BaseStatusTableViewCell) {
|
||||||
cellHeightChanged()
|
// causes the table view to recalculate the cell heights
|
||||||
|
tableView.beginUpdates()
|
||||||
|
tableView.endUpdates()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
extension NotificationsTableViewController: UITableViewDataSourcePrefetching {
|
extension NotificationsTableViewController: UITableViewDataSourcePrefetching {
|
||||||
func tableView(_ tableView: UITableView, prefetchRowsAt indexPaths: [IndexPath]) {
|
func tableView(_ tableView: UITableView, prefetchRowsAt indexPaths: [IndexPath]) {
|
||||||
for indexPath in indexPaths {
|
for indexPath in indexPaths {
|
||||||
for notification in item(for: indexPath).notifications {
|
for notification in groups[indexPath.row].notifications {
|
||||||
|
// todo: this account object could be stale
|
||||||
_ = ImageCache.avatars.get(notification.account.avatar, completion: nil)
|
_ = ImageCache.avatars.get(notification.account.avatar, completion: nil)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -216,9 +316,21 @@ extension NotificationsTableViewController: UITableViewDataSourcePrefetching {
|
||||||
|
|
||||||
func tableView(_ tableView: UITableView, cancelPrefetchingForRowsAt indexPaths: [IndexPath]) {
|
func tableView(_ tableView: UITableView, cancelPrefetchingForRowsAt indexPaths: [IndexPath]) {
|
||||||
for indexPath in indexPaths {
|
for indexPath in indexPaths {
|
||||||
for notification in item(for: indexPath).notifications {
|
for notification in groups[indexPath.row].notifications {
|
||||||
ImageCache.avatars.cancelWithoutCallback(notification.account.avatar)
|
ImageCache.avatars.cancelWithoutCallback(notification.account.avatar)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
extension NotificationsTableViewController: RefreshableViewController {
|
||||||
|
func refresh() {
|
||||||
|
refreshNotifications()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension NotificationsTableViewController: BackgroundableViewController {
|
||||||
|
func sceneDidEnterBackground() {
|
||||||
|
pruneOffscreenRows()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -9,7 +9,7 @@
|
||||||
import UIKit
|
import UIKit
|
||||||
import Pachyderm
|
import Pachyderm
|
||||||
|
|
||||||
class ProfileStatusesViewController: TimelineLikeTableViewController<TimelineEntry> {
|
class ProfileStatusesViewController: EnhancedTableViewController {
|
||||||
|
|
||||||
weak var mastodonController: MastodonController!
|
weak var mastodonController: MastodonController!
|
||||||
|
|
||||||
|
@ -19,15 +19,20 @@ class ProfileStatusesViewController: TimelineLikeTableViewController<TimelineEnt
|
||||||
|
|
||||||
let kind: Kind
|
let kind: Kind
|
||||||
|
|
||||||
|
private var pinnedStatuses: [(id: String, state: StatusState)] = []
|
||||||
|
private var timelineSegments: [[(id: String, state: StatusState)]] = []
|
||||||
|
|
||||||
private var older: RequestRange?
|
private var older: RequestRange?
|
||||||
private var newer: RequestRange?
|
private var newer: RequestRange?
|
||||||
|
|
||||||
|
var loaded = false
|
||||||
|
|
||||||
init(accountID: String?, kind: Kind, mastodonController: MastodonController) {
|
init(accountID: String?, kind: Kind, mastodonController: MastodonController) {
|
||||||
self.accountID = accountID
|
self.accountID = accountID
|
||||||
self.kind = kind
|
self.kind = kind
|
||||||
self.mastodonController = mastodonController
|
self.mastodonController = mastodonController
|
||||||
|
|
||||||
super.init()
|
super.init(style: .plain)
|
||||||
}
|
}
|
||||||
|
|
||||||
required init?(coder: NSCoder) {
|
required init?(coder: NSCoder) {
|
||||||
|
@ -37,117 +42,76 @@ class ProfileStatusesViewController: TimelineLikeTableViewController<TimelineEnt
|
||||||
override func viewDidLoad() {
|
override func viewDidLoad() {
|
||||||
super.viewDidLoad()
|
super.viewDidLoad()
|
||||||
|
|
||||||
|
view.backgroundColor = .systemBackground
|
||||||
|
|
||||||
|
#if !targetEnvironment(macCatalyst)
|
||||||
|
refreshControl = UIRefreshControl()
|
||||||
|
refreshControl!.addTarget(self, action: #selector(refreshStatuses), for: .valueChanged)
|
||||||
|
#endif
|
||||||
|
|
||||||
|
addKeyCommand(MenuController.refreshCommand(discoverabilityTitle: NSLocalizedString("Refresh Statuses", comment: "refresh statuses command discoverability title")))
|
||||||
|
|
||||||
|
tableView.rowHeight = UITableView.automaticDimension
|
||||||
|
tableView.estimatedRowHeight = 140
|
||||||
|
|
||||||
tableView.register(UINib(nibName: "TimelineStatusTableViewCell", bundle: .main), forCellReuseIdentifier: "statusCell")
|
tableView.register(UINib(nibName: "TimelineStatusTableViewCell", bundle: .main), forCellReuseIdentifier: "statusCell")
|
||||||
|
|
||||||
|
tableView.prefetchDataSource = self
|
||||||
|
}
|
||||||
|
|
||||||
|
override func viewWillAppear(_ animated: Bool) {
|
||||||
|
super.viewWillAppear(animated)
|
||||||
|
|
||||||
|
if !loaded,
|
||||||
|
let accountID = accountID,
|
||||||
|
let account = mastodonController.persistentContainer.account(for: accountID) {
|
||||||
|
updateUI(account: account)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func updateUI(account: AccountMO) {
|
func updateUI(account: AccountMO) {
|
||||||
loadInitial()
|
guard !loaded else { return }
|
||||||
}
|
loaded = true
|
||||||
|
|
||||||
override class func refreshCommandTitle() -> String {
|
if kind == .statuses {
|
||||||
return NSLocalizedString("Refresh Statuses", comment: "refresh statuses command discoverability title")
|
|
||||||
}
|
|
||||||
|
|
||||||
override func headerSectionsCount() -> Int {
|
|
||||||
return 1
|
|
||||||
}
|
|
||||||
|
|
||||||
override func loadInitial() {
|
|
||||||
guard accountID != nil else {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if !loaded {
|
|
||||||
loadPinnedStatuses()
|
|
||||||
}
|
|
||||||
|
|
||||||
super.loadInitial()
|
|
||||||
}
|
|
||||||
|
|
||||||
private func loadPinnedStatuses() {
|
|
||||||
guard kind == .statuses else {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
getPinnedStatuses { (response) in
|
getPinnedStatuses { (response) in
|
||||||
guard case let .success(statuses, _) = response,
|
guard case let .success(statuses, _) = response else {
|
||||||
!statuses.isEmpty else {
|
|
||||||
// todo: error message
|
// todo: error message
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
if statuses.isEmpty { return }
|
||||||
|
|
||||||
self.mastodonController.persistentContainer.addAll(statuses: statuses) {
|
self.mastodonController.persistentContainer.addAll(statuses: statuses) {
|
||||||
let items = statuses.map { ($0.id, StatusState.unknown) }
|
self.pinnedStatuses = statuses.map { ($0.id, .unknown) }
|
||||||
|
let indexPaths = (0..<statuses.count).map { IndexPath(row: $0, section: 0) }
|
||||||
DispatchQueue.main.async {
|
DispatchQueue.main.async {
|
||||||
UIView.performWithoutAnimation {
|
UIView.performWithoutAnimation {
|
||||||
if self.sections.count < 1 {
|
self.tableView.insertRows(at: indexPaths, with: .none)
|
||||||
self.sections.append(items)
|
|
||||||
self.tableView.insertSections(IndexSet(integer: 0), with: .none)
|
|
||||||
} else {
|
|
||||||
self.sections[0] = items
|
|
||||||
self.tableView.reloadSections(IndexSet(integer: 0), with: .none)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override func loadInitialItems(completion: @escaping ([TimelineEntry]) -> Void) {
|
|
||||||
getStatuses { (response) in
|
getStatuses { (response) in
|
||||||
guard case let .success(statuses, pagination) = response,
|
guard case let .success(statuses, pagination) = response else {
|
||||||
!statuses.isEmpty else {
|
|
||||||
// todo: error message
|
// todo: error message
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
if statuses.isEmpty { return }
|
||||||
|
|
||||||
|
self.mastodonController.persistentContainer.addAll(statuses: statuses) {
|
||||||
|
self.timelineSegments.append(statuses.map { ($0.id, .unknown) })
|
||||||
|
|
||||||
self.older = pagination?.older
|
self.older = pagination?.older
|
||||||
self.newer = pagination?.newer
|
self.newer = pagination?.newer
|
||||||
|
|
||||||
self.mastodonController.persistentContainer.addAll(statuses: statuses) {
|
DispatchQueue.main.async {
|
||||||
completion(statuses.map { ($0.id, .unknown) })
|
UIView.performWithoutAnimation {
|
||||||
|
self.tableView.insertSections(IndexSet(integer: 1), with: .none)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override func loadOlder(completion: @escaping ([TimelineEntry]) -> Void) {
|
|
||||||
guard let older = older else {
|
|
||||||
completion([])
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
getStatuses(for: older) { (response) in
|
|
||||||
guard case let .success(statuses, pagination) = response else {
|
|
||||||
// todo: error message
|
|
||||||
completion([])
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
self.older = pagination?.older
|
|
||||||
|
|
||||||
self.mastodonController.persistentContainer.addAll(statuses: statuses) {
|
|
||||||
completion(statuses.map { ($0.id, .unknown) })
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override func loadNewer(completion: @escaping ([TimelineEntry]) -> Void) {
|
|
||||||
guard let newer = newer else {
|
|
||||||
completion([])
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
getStatuses(for: newer) { (response) in
|
|
||||||
guard case let .success(statuses, pagination) = response else {
|
|
||||||
// todo: error message
|
|
||||||
completion([])
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
self.newer = pagination?.newer
|
|
||||||
|
|
||||||
self.mastodonController.persistentContainer.addAll(statuses: statuses) {
|
|
||||||
completion(statuses.map { ($0.id, .unknown) })
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -169,19 +133,47 @@ class ProfileStatusesViewController: TimelineLikeTableViewController<TimelineEnt
|
||||||
mastodonController.run(request, completion: completion)
|
mastodonController.run(request, completion: completion)
|
||||||
}
|
}
|
||||||
|
|
||||||
override func refresh() {
|
// MARK: Interaction
|
||||||
super.refresh()
|
|
||||||
|
|
||||||
if kind == .statuses {
|
@objc func refreshStatuses() {
|
||||||
getPinnedStatuses { (response) in
|
guard let newer = newer else { return }
|
||||||
guard case let .success(newPinnedStatues, _) = response else {
|
|
||||||
|
getStatuses(for: newer) { (response) in
|
||||||
|
guard case let .success(newStatuses, pagination) = response else {
|
||||||
// todo: error message
|
// todo: error message
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
self.mastodonController.persistentContainer.addAll(statuses: newPinnedStatues) {
|
self.mastodonController.persistentContainer.addAll(statuses: newStatuses) {
|
||||||
let oldPinnedStatuses = self.sections[0]
|
// if there's no newer request range (because no statuses were returned),
|
||||||
let pinnedStatues = newPinnedStatues.map { (status) -> TimelineEntry in
|
// we don't want to change the current newer pagination, so that we can
|
||||||
|
// continue to load statuses newer than whatever was last loaded
|
||||||
|
if let newer = pagination?.newer {
|
||||||
|
self.newer = newer
|
||||||
|
}
|
||||||
|
|
||||||
|
let indexPaths = (0..<newStatuses.count).map { IndexPath(row: $0, section: 1) }
|
||||||
|
DispatchQueue.main.async {
|
||||||
|
self.timelineSegments[0].insert(contentsOf: newStatuses.map { ($0.id, .unknown) }, at: 0)
|
||||||
|
UIView.performWithoutAnimation {
|
||||||
|
self.tableView.insertRows(at: indexPaths, with: .none)
|
||||||
|
}
|
||||||
|
|
||||||
|
self.refreshControl?.endRefreshing()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if kind == .statuses {
|
||||||
|
getPinnedStatuses { (response) in
|
||||||
|
guard case let .success(newPinnedStatuses, _) = response else {
|
||||||
|
// todo: error message
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
self.mastodonController.persistentContainer.addAll(statuses: newPinnedStatuses) {
|
||||||
|
let oldPinnedStatuses = self.pinnedStatuses
|
||||||
|
let pinnedStatuses = newPinnedStatuses.map { (status) -> (id: String, state: StatusState) in
|
||||||
let state: StatusState
|
let state: StatusState
|
||||||
if let (_, oldState) = oldPinnedStatuses.first(where: { $0.id == status.id }) {
|
if let (_, oldState) = oldPinnedStatuses.first(where: { $0.id == status.id }) {
|
||||||
state = oldState
|
state = oldState
|
||||||
|
@ -191,11 +183,7 @@ class ProfileStatusesViewController: TimelineLikeTableViewController<TimelineEnt
|
||||||
return (status.id, state)
|
return (status.id, state)
|
||||||
}
|
}
|
||||||
DispatchQueue.main.async {
|
DispatchQueue.main.async {
|
||||||
if self.sections.count < 1 {
|
self.pinnedStatuses = pinnedStatuses
|
||||||
self.sections.append(pinnedStatues)
|
|
||||||
} else {
|
|
||||||
self.sections[0] = pinnedStatues
|
|
||||||
}
|
|
||||||
UIView.performWithoutAnimation {
|
UIView.performWithoutAnimation {
|
||||||
self.tableView.reloadSections(IndexSet(integer: 0), with: .none)
|
self.tableView.reloadSections(IndexSet(integer: 0), with: .none)
|
||||||
}
|
}
|
||||||
|
@ -205,19 +193,83 @@ class ProfileStatusesViewController: TimelineLikeTableViewController<TimelineEnt
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - UITableViewDatasource
|
// MARK: Table view data source
|
||||||
|
|
||||||
|
override func numberOfSections(in tableView: UITableView) -> Int {
|
||||||
|
// 1 for pinned, rest for timeline
|
||||||
|
return 1 + timelineSegments.count
|
||||||
|
}
|
||||||
|
|
||||||
|
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
|
||||||
|
if section == 0 {
|
||||||
|
return pinnedStatuses.count
|
||||||
|
} else {
|
||||||
|
return timelineSegments[section - 1].count
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
|
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
|
||||||
let cell = tableView.dequeueReusableCell(withIdentifier: "statusCell", for: indexPath) as! TimelineStatusTableViewCell
|
let cell = tableView.dequeueReusableCell(withIdentifier: "statusCell", for: indexPath) as! TimelineStatusTableViewCell
|
||||||
|
|
||||||
cell.delegate = self
|
cell.delegate = self
|
||||||
cell.showPinned = indexPath.section == 0
|
|
||||||
|
|
||||||
let (id, state) = item(for: indexPath)
|
if indexPath.section == 0 {
|
||||||
|
cell.showPinned = true
|
||||||
|
let (id, state) = pinnedStatuses[indexPath.row]
|
||||||
cell.updateUI(statusID: id, state: state)
|
cell.updateUI(statusID: id, state: state)
|
||||||
|
} else {
|
||||||
|
cell.showPinned = false
|
||||||
|
let (id, state) = timelineSegments[indexPath.section - 1][indexPath.row]
|
||||||
|
cell.updateUI(statusID: id, state: state)
|
||||||
|
}
|
||||||
|
|
||||||
return cell
|
return cell
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override func tableView(_ tableView: UITableView, willDisplay cell: UITableViewCell, forRowAt indexPath: IndexPath) {
|
||||||
|
// load older statuses if at bottom
|
||||||
|
if timelineSegments.count > 0,
|
||||||
|
indexPath.section == timelineSegments.count,
|
||||||
|
indexPath.row == timelineSegments[indexPath.section - 1].count - 1 {
|
||||||
|
guard let older = older else { return }
|
||||||
|
|
||||||
|
getStatuses(for: older) { (response) in
|
||||||
|
guard case let .success(newStatuses, pagination) = response else {
|
||||||
|
// todo: error message
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
self.mastodonController.persistentContainer.addAll(statuses: newStatuses) {
|
||||||
|
// if there is no older request range, we want to set ours to nil
|
||||||
|
// otherwise we would end up loading the same statuses again
|
||||||
|
self.older = pagination?.older
|
||||||
|
|
||||||
|
DispatchQueue.main.async {
|
||||||
|
let start = self.timelineSegments[indexPath.section - 1].count
|
||||||
|
let indexPaths = (0..<newStatuses.count).map { IndexPath(row: start + $0, section: indexPath.section) }
|
||||||
|
self.timelineSegments[indexPath.section - 1].append(contentsOf: newStatuses.map { ($0.id, .unknown) })
|
||||||
|
UIView.performWithoutAnimation {
|
||||||
|
self.tableView.insertRows(at: indexPaths, with: .none)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override func tableView(_ tableView: UITableView, canEditRowAt indexPath: IndexPath) -> Bool {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
override func tableView(_ tableView: UITableView, leadingSwipeActionsConfigurationForRowAt indexPath: IndexPath) -> UISwipeActionsConfiguration? {
|
||||||
|
return (tableView.cellForRow(at: indexPath) as? TableViewSwipeActionProvider)?.leadingSwipeActionsConfiguration()
|
||||||
|
}
|
||||||
|
|
||||||
|
override func tableView(_ tableView: UITableView, trailingSwipeActionsConfigurationForRowAt indexPath: IndexPath) -> UISwipeActionsConfiguration? {
|
||||||
|
return (tableView.cellForRow(at: indexPath) as? TableViewSwipeActionProvider)?.trailingSwipeActionsConfiguration()
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
extension ProfileStatusesViewController {
|
extension ProfileStatusesViewController {
|
||||||
|
@ -226,25 +278,26 @@ extension ProfileStatusesViewController {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
extension ProfileStatusesViewController: TuskerNavigationDelegate {
|
|
||||||
var apiController: MastodonController { mastodonController }
|
|
||||||
}
|
|
||||||
|
|
||||||
extension ProfileStatusesViewController: StatusTableViewCellDelegate {
|
extension ProfileStatusesViewController: StatusTableViewCellDelegate {
|
||||||
|
var apiController: MastodonController { mastodonController }
|
||||||
|
|
||||||
func statusCellCollapsedStateChanged(_ cell: BaseStatusTableViewCell) {
|
func statusCellCollapsedStateChanged(_ cell: BaseStatusTableViewCell) {
|
||||||
cellHeightChanged()
|
// causes the table view to recalculate the cell heights
|
||||||
|
tableView.beginUpdates()
|
||||||
|
tableView.endUpdates()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
extension ProfileStatusesViewController: UITableViewDataSourcePrefetching {
|
extension ProfileStatusesViewController: UITableViewDataSourcePrefetching {
|
||||||
func tableView(_ tableView: UITableView, prefetchRowsAt indexPaths: [IndexPath]) {
|
func tableView(_ tableView: UITableView, prefetchRowsAt indexPaths: [IndexPath]) {
|
||||||
for indexPath in indexPaths {
|
for indexPath in indexPaths {
|
||||||
let statusID = item(for: indexPath).id
|
let statusID: String
|
||||||
|
if indexPath.section == 0 {
|
||||||
guard let status = mastodonController.persistentContainer.status(for: statusID) else {
|
statusID = pinnedStatuses[indexPath.row].id
|
||||||
continue
|
} else {
|
||||||
|
statusID = timelineSegments[indexPath.section - 1][indexPath.row].id
|
||||||
}
|
}
|
||||||
|
guard let status = mastodonController.persistentContainer.status(for: statusID) else { continue }
|
||||||
_ = ImageCache.avatars.get(status.account.avatar, completion: nil)
|
_ = ImageCache.avatars.get(status.account.avatar, completion: nil)
|
||||||
for attachment in status.attachments where attachment.kind == .image {
|
for attachment in status.attachments where attachment.kind == .image {
|
||||||
_ = ImageCache.attachments.get(attachment.url, completion: nil)
|
_ = ImageCache.attachments.get(attachment.url, completion: nil)
|
||||||
|
@ -254,11 +307,13 @@ extension ProfileStatusesViewController: UITableViewDataSourcePrefetching {
|
||||||
|
|
||||||
func tableView(_ tableView: UITableView, cancelPrefetchingForRowsAt indexPaths: [IndexPath]) {
|
func tableView(_ tableView: UITableView, cancelPrefetchingForRowsAt indexPaths: [IndexPath]) {
|
||||||
for indexPath in indexPaths {
|
for indexPath in indexPaths {
|
||||||
let statusID = item(for: indexPath).id
|
let statusID: String
|
||||||
|
if indexPath.section == 0 {
|
||||||
guard let status = mastodonController.persistentContainer.status(for: statusID) else {
|
statusID = pinnedStatuses[indexPath.row].id
|
||||||
continue
|
} else {
|
||||||
|
statusID = timelineSegments[indexPath.section - 1][indexPath.row].id
|
||||||
}
|
}
|
||||||
|
guard let status = mastodonController.persistentContainer.status(for: statusID) else { continue }
|
||||||
ImageCache.avatars.cancelWithoutCallback(status.account.avatar)
|
ImageCache.avatars.cancelWithoutCallback(status.account.avatar)
|
||||||
for attachment in status.attachments where attachment.kind == .image {
|
for attachment in status.attachments where attachment.kind == .image {
|
||||||
ImageCache.avatars.cancelWithoutCallback(attachment.url)
|
ImageCache.avatars.cancelWithoutCallback(attachment.url)
|
||||||
|
@ -266,3 +321,9 @@ extension ProfileStatusesViewController: UITableViewDataSourcePrefetching {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
extension ProfileStatusesViewController: RefreshableViewController {
|
||||||
|
func refresh() {
|
||||||
|
refreshStatuses()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
//
|
//
|
||||||
// TimelineTableViewController.swift
|
// StatusesTableViewController.swift
|
||||||
// Tusker
|
// Tusker
|
||||||
//
|
//
|
||||||
// Created by Shadowfacts on 8/15/18.
|
// Created by Shadowfacts on 8/15/18.
|
||||||
|
@ -9,29 +9,41 @@
|
||||||
import UIKit
|
import UIKit
|
||||||
import Pachyderm
|
import Pachyderm
|
||||||
|
|
||||||
typealias TimelineEntry = (id: String, state: StatusState)
|
class TimelineTableViewController: EnhancedTableViewController, StatusTableViewCellDelegate {
|
||||||
|
|
||||||
class TimelineTableViewController: TimelineLikeTableViewController<TimelineEntry> {
|
var timeline: Timeline!
|
||||||
|
|
||||||
let timeline: Timeline
|
|
||||||
weak var mastodonController: MastodonController!
|
weak var mastodonController: MastodonController!
|
||||||
|
|
||||||
|
private var loaded = false
|
||||||
|
|
||||||
|
var timelineSegments: [[(id: String, state: StatusState)]] = []
|
||||||
|
|
||||||
|
private let pageSize = 20
|
||||||
private var newer: RequestRange?
|
private var newer: RequestRange?
|
||||||
private var older: RequestRange?
|
private var older: RequestRange?
|
||||||
|
|
||||||
|
private var lastLastVisibleRow: IndexPath?
|
||||||
|
|
||||||
init(for timeline: Timeline, mastodonController: MastodonController) {
|
init(for timeline: Timeline, mastodonController: MastodonController) {
|
||||||
self.timeline = timeline
|
self.timeline = timeline
|
||||||
self.mastodonController = mastodonController
|
self.mastodonController = mastodonController
|
||||||
|
|
||||||
super.init()
|
super.init(style: .plain)
|
||||||
|
|
||||||
title = timeline.title
|
title = timeline.title
|
||||||
tabBarItem.image = timeline.tabBarImage
|
tabBarItem.image = timeline.tabBarImage
|
||||||
|
|
||||||
|
#if !targetEnvironment(macCatalyst)
|
||||||
|
self.refreshControl = UIRefreshControl()
|
||||||
|
refreshControl!.addTarget(self, action: #selector(refreshStatuses), for: .valueChanged)
|
||||||
|
#endif
|
||||||
|
|
||||||
|
addKeyCommand(MenuController.refreshCommand(discoverabilityTitle: NSLocalizedString("Refresh Statuses", comment: "refresh statuses command discoverability title")))
|
||||||
|
|
||||||
userActivity = UserActivityManager.showTimelineActivity(timeline: timeline)
|
userActivity = UserActivityManager.showTimelineActivity(timeline: timeline)
|
||||||
}
|
}
|
||||||
|
|
||||||
required init?(coder: NSCoder) {
|
required init?(coder aDecoder: NSCoder) {
|
||||||
fatalError("init(coder:) has not been implemented")
|
fatalError("init(coder:) has not been implemented")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -40,89 +52,120 @@ class TimelineTableViewController: TimelineLikeTableViewController<TimelineEntry
|
||||||
// decrement reference counts of any statuses we still have
|
// decrement reference counts of any statuses we still have
|
||||||
// if the app is currently being quit, this will not affect the persisted data because
|
// if the app is currently being quit, this will not affect the persisted data because
|
||||||
// the persistent container would already have been saved in SceneDelegate.sceneDidEnterBackground(_:)
|
// the persistent container would already have been saved in SceneDelegate.sceneDidEnterBackground(_:)
|
||||||
for section in sections {
|
for segment in timelineSegments {
|
||||||
for (id, _) in section {
|
for (id, _) in segment {
|
||||||
persistentContainer.status(for: id)?.decrementReferenceCount()
|
persistentContainer.status(for: id)?.decrementReferenceCount()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func statusID(for indexPath: IndexPath) -> String {
|
||||||
|
return timelineSegments[indexPath.section][indexPath.row].id
|
||||||
|
}
|
||||||
|
|
||||||
override func viewDidLoad() {
|
override func viewDidLoad() {
|
||||||
super.viewDidLoad()
|
super.viewDidLoad()
|
||||||
|
|
||||||
tableView.register(UINib(nibName: "TimelineStatusTableViewCell", bundle: .main), forCellReuseIdentifier: "statusCell")
|
tableView.rowHeight = UITableView.automaticDimension
|
||||||
|
tableView.estimatedRowHeight = 140
|
||||||
|
|
||||||
|
tableView.register(UINib(nibName: "TimelineStatusTableViewCell", bundle: nil), forCellReuseIdentifier: "statusCell")
|
||||||
|
|
||||||
|
tableView.prefetchDataSource = self
|
||||||
}
|
}
|
||||||
|
|
||||||
override class func refreshCommandTitle() -> String {
|
override func viewWillAppear(_ animated: Bool) {
|
||||||
return NSLocalizedString("Refresh Statuses", comment: "refresh status command discoverability title")
|
super.viewWillAppear(animated)
|
||||||
|
|
||||||
|
loadInitialStatuses()
|
||||||
}
|
}
|
||||||
|
|
||||||
override func willRemoveRows(at indexPaths: [IndexPath]) {
|
override func viewWillDisappear(_ animated: Bool) {
|
||||||
for indexPath in indexPaths {
|
super.viewWillDisappear(animated)
|
||||||
let id = item(for: indexPath).id
|
|
||||||
|
pruneOffscreenRows()
|
||||||
|
}
|
||||||
|
|
||||||
|
func loadInitialStatuses() {
|
||||||
|
guard !loaded else { return }
|
||||||
|
loaded = true
|
||||||
|
|
||||||
|
let request = Client.getStatuses(timeline: timeline)
|
||||||
|
mastodonController.run(request) { response in
|
||||||
|
guard case let .success(statuses, pagination) = response else { fatalError() }
|
||||||
|
// todo: possible race condition here? we update the underlying data before waiting to reload the table view
|
||||||
|
self.timelineSegments.insert(statuses.map { ($0.id, .unknown) }, at: 0)
|
||||||
|
self.newer = pagination?.newer
|
||||||
|
self.older = pagination?.older
|
||||||
|
self.mastodonController.persistentContainer.addAll(statuses: statuses) {
|
||||||
|
DispatchQueue.main.async {
|
||||||
|
self.tableView.reloadData()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func pruneOffscreenRows() {
|
||||||
|
guard let lastVisibleRow = lastLastVisibleRow else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
let lastSectionIndex = timelineSegments.count - 1
|
||||||
|
|
||||||
|
if lastVisibleRow.section < lastSectionIndex {
|
||||||
|
// if there is a section below the last visible one
|
||||||
|
|
||||||
|
let sectionsToRemove = (lastVisibleRow.section + 1)...lastSectionIndex
|
||||||
|
|
||||||
|
for section in sectionsToRemove {
|
||||||
|
for (id, _) in timelineSegments.remove(at: section) {
|
||||||
mastodonController.persistentContainer.status(for: id)?.decrementReferenceCount()
|
mastodonController.persistentContainer.status(for: id)?.decrementReferenceCount()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override func loadInitialItems(completion: @escaping ([TimelineEntry]) -> Void) {
|
UIView.performWithoutAnimation {
|
||||||
let request = Client.getStatuses(timeline: timeline)
|
tableView.deleteSections(IndexSet(sectionsToRemove), with: .none)
|
||||||
|
}
|
||||||
|
|
||||||
mastodonController?.run(request) { (response) in
|
} else if lastVisibleRow.section == lastSectionIndex {
|
||||||
guard case let .success(statuses, pagination) = response else { fatalError() }
|
let lastSection = timelineSegments.last!
|
||||||
|
let lastRowIndex = lastSection.count - 1
|
||||||
|
|
||||||
self.newer = pagination?.newer
|
if lastVisibleRow.row < lastRowIndex - 20 {
|
||||||
self.older = pagination?.older
|
// if there are more than 20 rows in the current section below the last visible one
|
||||||
|
|
||||||
|
let rowIndicesInLastSectionToRemove = (lastVisibleRow.row + 20)..<lastSection.count
|
||||||
|
|
||||||
|
let statusesToRemove = lastSection[rowIndicesInLastSectionToRemove]
|
||||||
|
for (id, _) in statusesToRemove {
|
||||||
|
mastodonController.persistentContainer.status(for: id)?.decrementReferenceCount()
|
||||||
|
}
|
||||||
|
|
||||||
|
timelineSegments[lastSectionIndex].removeSubrange(rowIndicesInLastSectionToRemove)
|
||||||
|
|
||||||
|
let removedIndexPaths = rowIndicesInLastSectionToRemove.map { IndexPath(row: $0, section: lastSectionIndex) }
|
||||||
|
UIView.performWithoutAnimation {
|
||||||
|
tableView.deleteRows(at: removedIndexPaths, with: .none)
|
||||||
|
}
|
||||||
|
|
||||||
self.mastodonController?.persistentContainer.addAll(statuses: statuses) {
|
|
||||||
completion(statuses.map { ($0.id, .unknown) })
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override func loadOlder(completion: @escaping ([TimelineEntry]) -> Void) {
|
// MARK: - Table view data source
|
||||||
guard let older = older else {
|
|
||||||
completion([])
|
override func numberOfSections(in tableView: UITableView) -> Int {
|
||||||
return
|
return timelineSegments.count
|
||||||
}
|
}
|
||||||
|
|
||||||
let request = Client.getStatuses(timeline: timeline, range: older)
|
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
|
||||||
|
return timelineSegments[section].count
|
||||||
mastodonController.run(request) { (response) in
|
|
||||||
guard case let .success(statuses, pagination) = response else { fatalError() }
|
|
||||||
|
|
||||||
self.older = pagination?.older
|
|
||||||
|
|
||||||
self.mastodonController?.persistentContainer.addAll(statuses: statuses) {
|
|
||||||
completion(statuses.map { ($0.id, .unknown) })
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override func loadNewer(completion: @escaping ([TimelineEntry]) -> Void) {
|
|
||||||
guard let newer = newer else {
|
|
||||||
completion([])
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
let request = Client.getStatuses(timeline: timeline, range: newer)
|
|
||||||
|
|
||||||
mastodonController.run(request) { (response) in
|
|
||||||
guard case let .success(statuses, pagination) = response else { fatalError() }
|
|
||||||
|
|
||||||
self.newer = pagination?.newer
|
|
||||||
|
|
||||||
self.mastodonController?.persistentContainer.addAll(statuses: statuses) {
|
|
||||||
completion(statuses.map { ($0.id, .unknown) })
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// MARK: - UITableViewDataSource
|
|
||||||
|
|
||||||
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
|
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
|
||||||
guard let cell = tableView.dequeueReusableCell(withIdentifier: "statusCell", for: indexPath) as? TimelineStatusTableViewCell else { fatalError() }
|
guard let cell = tableView.dequeueReusableCell(withIdentifier: "statusCell", for: indexPath) as? TimelineStatusTableViewCell else { fatalError() }
|
||||||
|
|
||||||
let (id, state) = item(for: indexPath)
|
let (id, state) = timelineSegments[indexPath.section][indexPath.row]
|
||||||
cell.delegate = self
|
cell.delegate = self
|
||||||
|
|
||||||
cell.updateUI(statusID: id, state: state)
|
cell.updateUI(statusID: id, state: state)
|
||||||
|
@ -130,24 +173,104 @@ class TimelineTableViewController: TimelineLikeTableViewController<TimelineEntry
|
||||||
return cell
|
return cell
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// MARK: - Table view delegate
|
||||||
|
|
||||||
|
override func tableView(_ tableView: UITableView, willDisplay cell: UITableViewCell, forRowAt indexPath: IndexPath) {
|
||||||
|
// this assumes that indexPathsForVisibleRows is always in order
|
||||||
|
lastLastVisibleRow = tableView.indexPathsForVisibleRows?.last
|
||||||
|
|
||||||
|
// load older statuses, if necessary
|
||||||
|
if indexPath.section == timelineSegments.count - 1,
|
||||||
|
indexPath.row == timelineSegments[indexPath.section].count - 1 {
|
||||||
|
guard let older = older else { return }
|
||||||
|
|
||||||
|
let request = Client.getStatuses(timeline: timeline, range: older)
|
||||||
|
mastodonController.run(request) { response in
|
||||||
|
guard case let .success(newStatuses, pagination) = response else { fatalError() }
|
||||||
|
self.older = pagination?.older
|
||||||
|
self.mastodonController.persistentContainer.addAll(statuses: newStatuses) {
|
||||||
|
DispatchQueue.main.async {
|
||||||
|
let newRows = self.timelineSegments.last!.count..<(self.timelineSegments.last!.count + newStatuses.count)
|
||||||
|
let newIndexPaths = newRows.map { IndexPath(row: $0, section: self.timelineSegments.count - 1) }
|
||||||
|
self.timelineSegments[self.timelineSegments.count - 1].append(contentsOf: newStatuses.map { ($0.id, .unknown) })
|
||||||
|
|
||||||
|
UIView.performWithoutAnimation {
|
||||||
|
self.tableView.insertRows(at: newIndexPaths, with: .none)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
extension TimelineTableViewController: TuskerNavigationDelegate {
|
override func tableView(_ tableView: UITableView, canEditRowAt indexPath: IndexPath) -> Bool {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
override func tableView(_ tableView: UITableView, leadingSwipeActionsConfigurationForRowAt indexPath: IndexPath) -> UISwipeActionsConfiguration? {
|
||||||
|
return (tableView.cellForRow(at: indexPath) as? TableViewSwipeActionProvider)?.leadingSwipeActionsConfiguration()
|
||||||
|
}
|
||||||
|
|
||||||
|
override func tableView(_ tableView: UITableView, trailingSwipeActionsConfigurationForRowAt indexPath: IndexPath) -> UISwipeActionsConfiguration? {
|
||||||
|
return (tableView.cellForRow(at: indexPath) as? TableViewSwipeActionProvider)?.trailingSwipeActionsConfiguration()
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Interaction
|
||||||
|
|
||||||
|
@objc func refreshStatuses() {
|
||||||
|
guard let newer = newer else { return }
|
||||||
|
|
||||||
|
let request = Client.getStatuses(timeline: timeline, range: newer)
|
||||||
|
mastodonController.run(request) { response in
|
||||||
|
guard case let .success(newStatuses, pagination) = response else { fatalError() }
|
||||||
|
|
||||||
|
// If there is no new newer pagination, don't reset it, so that the user can continue refreshing for more recent statuses
|
||||||
|
// Otherwise, when no new statuses were loaded, it would get reset and the the user would be unable to refresh
|
||||||
|
if let newer = pagination?.newer {
|
||||||
|
self.newer = newer
|
||||||
|
}
|
||||||
|
|
||||||
|
self.mastodonController.persistentContainer.addAll(statuses: newStatuses) {
|
||||||
|
DispatchQueue.main.async {
|
||||||
|
self.timelineSegments[0].insert(contentsOf: newStatuses.map { ($0.id, .unknown) }, at: 0)
|
||||||
|
let newIndexPaths = (0..<newStatuses.count).map {
|
||||||
|
IndexPath(row: $0, section: 0)
|
||||||
|
}
|
||||||
|
UIView.performWithoutAnimation {
|
||||||
|
self.tableView.insertRows(at: newIndexPaths, with: .automatic)
|
||||||
|
}
|
||||||
|
|
||||||
|
self.refreshControl?.endRefreshing()
|
||||||
|
|
||||||
|
// maintain the current position in the list (don't scroll to the top)
|
||||||
|
self.tableView.scrollToRow(at: IndexPath(row: newStatuses.count, section: 0), at: .top, animated: false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@objc func composePressed(_ sender: Any) {
|
||||||
|
compose()
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - TuskerNavigationDelegate
|
||||||
|
|
||||||
var apiController: MastodonController { mastodonController }
|
var apiController: MastodonController { mastodonController }
|
||||||
|
|
||||||
|
// MARK: - StatusTableViewCellDelegate
|
||||||
|
|
||||||
|
func statusCellCollapsedStateChanged(_ cell: BaseStatusTableViewCell) {
|
||||||
|
// causes the table view to recalculate the cell heights
|
||||||
|
tableView.beginUpdates()
|
||||||
|
tableView.endUpdates()
|
||||||
}
|
}
|
||||||
|
|
||||||
extension TimelineTableViewController: StatusTableViewCellDelegate {
|
|
||||||
func statusCellCollapsedStateChanged(_ cell: BaseStatusTableViewCell) {
|
|
||||||
cellHeightChanged()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
extension TimelineTableViewController: UITableViewDataSourcePrefetching {
|
extension TimelineTableViewController: UITableViewDataSourcePrefetching {
|
||||||
func tableView(_ tableView: UITableView, prefetchRowsAt indexPaths: [IndexPath]) {
|
func tableView(_ tableView: UITableView, prefetchRowsAt indexPaths: [IndexPath]) {
|
||||||
for indexPath in indexPaths {
|
for indexPath in indexPaths {
|
||||||
guard let status = mastodonController.persistentContainer.status(for: item(for: indexPath).id) else {
|
guard let status = mastodonController.persistentContainer.status(for: statusID(for: indexPath)) else { continue }
|
||||||
continue
|
|
||||||
}
|
|
||||||
_ = ImageCache.avatars.get(status.account.avatar, completion: nil)
|
_ = ImageCache.avatars.get(status.account.avatar, completion: nil)
|
||||||
for attachment in status.attachments where attachment.kind == .image {
|
for attachment in status.attachments where attachment.kind == .image {
|
||||||
_ = ImageCache.attachments.get(attachment.url, completion: nil)
|
_ = ImageCache.attachments.get(attachment.url, completion: nil)
|
||||||
|
@ -159,11 +282,8 @@ extension TimelineTableViewController: UITableViewDataSourcePrefetching {
|
||||||
for indexPath in indexPaths {
|
for indexPath in indexPaths {
|
||||||
// todo: this means when removing cells, we can't cancel prefetching
|
// todo: this means when removing cells, we can't cancel prefetching
|
||||||
// is this an issue?
|
// is this an issue?
|
||||||
guard indexPath.section < sections.count,
|
guard indexPath.section < timelineSegments.count, indexPath.row < timelineSegments[indexPath.section].count,
|
||||||
indexPath.row < sections[indexPath.section].count,
|
let status = mastodonController.persistentContainer.status(for: statusID(for: indexPath)) else { continue }
|
||||||
let status = mastodonController.persistentContainer.status(for: item(for: indexPath).id) else {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
ImageCache.avatars.cancelWithoutCallback(status.account.avatar)
|
ImageCache.avatars.cancelWithoutCallback(status.account.avatar)
|
||||||
for attachment in status.attachments where attachment.kind == .image {
|
for attachment in status.attachments where attachment.kind == .image {
|
||||||
ImageCache.attachments.cancelWithoutCallback(attachment.url)
|
ImageCache.attachments.cancelWithoutCallback(attachment.url)
|
||||||
|
@ -171,3 +291,15 @@ extension TimelineTableViewController: UITableViewDataSourcePrefetching {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
extension TimelineTableViewController: RefreshableViewController {
|
||||||
|
func refresh() {
|
||||||
|
refreshStatuses()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension TimelineTableViewController: BackgroundableViewController {
|
||||||
|
func sceneDidEnterBackground() {
|
||||||
|
pruneOffscreenRows()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -1,243 +0,0 @@
|
||||||
//
|
|
||||||
// TimelineLikeTableViewController.swift
|
|
||||||
// Tusker
|
|
||||||
//
|
|
||||||
// Created by Shadowfacts on 11/15/20.
|
|
||||||
// Copyright © 2020 Shadowfacts. All rights reserved.
|
|
||||||
//
|
|
||||||
|
|
||||||
import UIKit
|
|
||||||
|
|
||||||
/// A table view controller that manages common functionality between timeline-like UIs.
|
|
||||||
// For example, this class handles loading new items when the user scrolls to the end,
|
|
||||||
// refreshing, and pruning offscreen rows automatically.
|
|
||||||
class TimelineLikeTableViewController<Item>: EnhancedTableViewController, RefreshableViewController {
|
|
||||||
|
|
||||||
private(set) var loaded = false
|
|
||||||
|
|
||||||
var sections: [[Item]] = []
|
|
||||||
|
|
||||||
private let pageSize = 20
|
|
||||||
|
|
||||||
private var lastLastVisibleRow: IndexPath?
|
|
||||||
|
|
||||||
init() {
|
|
||||||
super.init(style: .plain)
|
|
||||||
|
|
||||||
#if !targetEnvironment(macCatalyst)
|
|
||||||
self.refreshControl = UIRefreshControl()
|
|
||||||
self.refreshControl!.addTarget(self, action: #selector(refresh), for: .valueChanged)
|
|
||||||
#endif
|
|
||||||
|
|
||||||
addKeyCommand(MenuController.refreshCommand(discoverabilityTitle: Self.refreshCommandTitle()))
|
|
||||||
}
|
|
||||||
|
|
||||||
required init?(coder: NSCoder) {
|
|
||||||
fatalError("init(coder:) has not been implemented")
|
|
||||||
}
|
|
||||||
|
|
||||||
func item(for indexPath: IndexPath) -> Item {
|
|
||||||
return sections[indexPath.section][indexPath.row]
|
|
||||||
}
|
|
||||||
|
|
||||||
override func viewDidLoad() {
|
|
||||||
super.viewDidLoad()
|
|
||||||
|
|
||||||
tableView.rowHeight = UITableView.automaticDimension
|
|
||||||
tableView.estimatedRowHeight = 140
|
|
||||||
|
|
||||||
if let prefetchSource = self as? UITableViewDataSourcePrefetching {
|
|
||||||
tableView.prefetchDataSource = prefetchSource
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override func viewWillAppear(_ animated: Bool) {
|
|
||||||
super.viewWillAppear(animated)
|
|
||||||
|
|
||||||
loadInitial()
|
|
||||||
}
|
|
||||||
|
|
||||||
override func viewWillDisappear(_ animated: Bool) {
|
|
||||||
super.viewWillDisappear(animated)
|
|
||||||
|
|
||||||
pruneOffscreenRows()
|
|
||||||
}
|
|
||||||
|
|
||||||
func loadInitial() {
|
|
||||||
guard !loaded else { return }
|
|
||||||
loaded = true
|
|
||||||
|
|
||||||
loadInitialItems() { (items) in
|
|
||||||
guard items.count > 0 else { return }
|
|
||||||
DispatchQueue.main.async {
|
|
||||||
if self.sections.count < self.headerSectionsCount() {
|
|
||||||
self.sections.insert(contentsOf: Array(repeating: [], count: self.headerSectionsCount() - self.sections.count), at: 0)
|
|
||||||
}
|
|
||||||
|
|
||||||
self.sections.append(items)
|
|
||||||
|
|
||||||
self.tableView.reloadData()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func reloadInitialItems() {
|
|
||||||
loaded = false
|
|
||||||
sections = []
|
|
||||||
loadInitial()
|
|
||||||
}
|
|
||||||
|
|
||||||
func cellHeightChanged() {
|
|
||||||
// causes the table view to recalculate the cell heights
|
|
||||||
tableView.beginUpdates()
|
|
||||||
tableView.endUpdates()
|
|
||||||
}
|
|
||||||
|
|
||||||
class func refreshCommandTitle() -> String {
|
|
||||||
return "Refresh"
|
|
||||||
}
|
|
||||||
|
|
||||||
func loadInitialItems(completion: @escaping ([Item]) -> Void) {
|
|
||||||
fatalError("loadInitialItems(completion:) must be implemented by subclasses")
|
|
||||||
}
|
|
||||||
|
|
||||||
func loadOlder(completion: @escaping ([Item]) -> Void) {
|
|
||||||
fatalError("loadOlder(completion:) must be implemented by subclasses")
|
|
||||||
}
|
|
||||||
|
|
||||||
func loadNewer(completion: @escaping ([Item]) -> Void) {
|
|
||||||
fatalError("loadNewer(completion:) must be implemented by subclasses")
|
|
||||||
}
|
|
||||||
|
|
||||||
func willRemoveRows(at indexPaths: [IndexPath]) {
|
|
||||||
}
|
|
||||||
|
|
||||||
func headerSectionsCount() -> Int {
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
|
|
||||||
private func pruneOffscreenRows() {
|
|
||||||
guard let lastVisibleRow = lastLastVisibleRow else {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
let lastSectionIndex = sections.count - 1
|
|
||||||
|
|
||||||
if lastVisibleRow.section < lastSectionIndex {
|
|
||||||
// if there is a section below the last visible one
|
|
||||||
|
|
||||||
let sectionsToRemove = (lastVisibleRow.section + 1)...lastSectionIndex
|
|
||||||
|
|
||||||
let indexPathsToRemove = sectionsToRemove.flatMap { (section) in
|
|
||||||
sections[section].indices.map { (row) in
|
|
||||||
IndexPath(row: row, section: section)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
willRemoveRows(at: indexPathsToRemove)
|
|
||||||
|
|
||||||
sections.removeSubrange(sectionsToRemove)
|
|
||||||
|
|
||||||
UIView.performWithoutAnimation {
|
|
||||||
tableView.deleteSections(IndexSet(sectionsToRemove), with: .none)
|
|
||||||
}
|
|
||||||
} else if lastVisibleRow.section == lastSectionIndex {
|
|
||||||
let lastSection = sections.last!
|
|
||||||
let lastRowIndex = lastSection.count - 1
|
|
||||||
|
|
||||||
if lastVisibleRow.row < lastRowIndex - pageSize {
|
|
||||||
// if there are more than pageSize rows in the current section below the last visible one
|
|
||||||
|
|
||||||
let rowIndicesInLastSectionToRemove = (lastVisibleRow.row + 20)..<lastSection.count
|
|
||||||
|
|
||||||
let indexPathsToRemove = rowIndicesInLastSectionToRemove.map {
|
|
||||||
IndexPath(row: $0, section: lastSectionIndex)
|
|
||||||
}
|
|
||||||
willRemoveRows(at: indexPathsToRemove)
|
|
||||||
|
|
||||||
sections[lastSectionIndex].removeSubrange(rowIndicesInLastSectionToRemove)
|
|
||||||
|
|
||||||
UIView.performWithoutAnimation {
|
|
||||||
tableView.deleteRows(at: indexPathsToRemove, with: .none)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
// MARK: - UITableViewDataSource
|
|
||||||
override func numberOfSections(in tableView: UITableView) -> Int {
|
|
||||||
return sections.count
|
|
||||||
}
|
|
||||||
|
|
||||||
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
|
|
||||||
return sections[section].count
|
|
||||||
}
|
|
||||||
|
|
||||||
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
|
|
||||||
fatalError("tableView(_:cellForRowAt:) must be implemented by subclasses")
|
|
||||||
}
|
|
||||||
|
|
||||||
// MARK: - UITableViewDelegate
|
|
||||||
override func tableView(_ tableView: UITableView, willDisplay cell: UITableViewCell, forRowAt indexPath: IndexPath) {
|
|
||||||
// this assumes that indexPathsForVisibleRows is always in order
|
|
||||||
lastLastVisibleRow = tableView.indexPathsForVisibleRows?.last
|
|
||||||
|
|
||||||
if indexPath.section == sections.count - 1,
|
|
||||||
indexPath.row == sections[indexPath.section].count - 1 {
|
|
||||||
loadOlder() { (newItems) in
|
|
||||||
guard newItems.count > 0 else { return }
|
|
||||||
|
|
||||||
DispatchQueue.main.async {
|
|
||||||
let newRows = self.sections.last!.count..<(self.sections.last!.count + newItems.count)
|
|
||||||
let newIndexPaths = newRows.map { IndexPath(row: $0, section: self.sections.count - 1) }
|
|
||||||
|
|
||||||
self.sections[self.sections.count - 1].append(contentsOf: newItems)
|
|
||||||
|
|
||||||
UIView.performWithoutAnimation {
|
|
||||||
self.tableView.insertRows(at: newIndexPaths, with: .none)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override func tableView(_ tableView: UITableView, canEditRowAt indexPath: IndexPath) -> Bool {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
override func tableView(_ tableView: UITableView, leadingSwipeActionsConfigurationForRowAt indexPath: IndexPath) -> UISwipeActionsConfiguration? {
|
|
||||||
return (tableView.cellForRow(at: indexPath) as? TableViewSwipeActionProvider)?.leadingSwipeActionsConfiguration()
|
|
||||||
}
|
|
||||||
|
|
||||||
override func tableView(_ tableView: UITableView, trailingSwipeActionsConfigurationForRowAt indexPath: IndexPath) -> UISwipeActionsConfiguration? {
|
|
||||||
return (tableView.cellForRow(at: indexPath) as? TableViewSwipeActionProvider)?.trailingSwipeActionsConfiguration()
|
|
||||||
}
|
|
||||||
|
|
||||||
// MARK: - RefreshableViewController
|
|
||||||
func refresh() {
|
|
||||||
loadNewer() { (newItems) in
|
|
||||||
DispatchQueue.main.async {
|
|
||||||
self.refreshControl?.endRefreshing()
|
|
||||||
|
|
||||||
guard newItems.count > 0 else { return }
|
|
||||||
|
|
||||||
let firstNonHeaderSection = self.headerSectionsCount()
|
|
||||||
|
|
||||||
self.sections[firstNonHeaderSection].insert(contentsOf: newItems, at: 0)
|
|
||||||
|
|
||||||
let newIndexPaths = (0..<newItems.count).map { IndexPath(row: $0, section: firstNonHeaderSection) }
|
|
||||||
UIView.performWithoutAnimation {
|
|
||||||
self.tableView.insertRows(at: newIndexPaths, with: .none)
|
|
||||||
}
|
|
||||||
|
|
||||||
// maintain the current position in the list (don't scroll to top)
|
|
||||||
self.tableView.scrollToRow(at: IndexPath(row: newItems.count, section: firstNonHeaderSection), at: .top, animated: false)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
extension TimelineLikeTableViewController: BackgroundableViewController {
|
|
||||||
func sceneDidEnterBackground() {
|
|
||||||
pruneOffscreenRows()
|
|
||||||
}
|
|
||||||
}
|
|
Loading…
Reference in New Issue