forked from shadowfacts/Tusker
Extract common functionality into TimelineLikeTableViewController
This commit is contained in:
parent
b45dc19811
commit
dfad8740eb
|
@ -116,16 +116,15 @@
|
|||
D63569E023908A8D003DD353 /* StatusState.swift in Sources */ = {isa = PBXBuildFile; fileRef = D60A4FFB238B726A008AC647 /* StatusState.swift */; };
|
||||
D63661C02381C144004B9E16 /* PreferencesNavigationController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D63661BF2381C144004B9E16 /* PreferencesNavigationController.swift */; };
|
||||
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 */; };
|
||||
D6403CC224A6B72D00E81C55 /* VisualEffectImageButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6403CC124A6B72D00E81C55 /* VisualEffectImageButton.swift */; };
|
||||
D640D76922BAF5E6004FBE69 /* DomainBlocks.plist in Resources */ = {isa = PBXBuildFile; fileRef = D640D76822BAF5E6004FBE69 /* DomainBlocks.plist */; };
|
||||
D6412B0324AFF6A600F5412E /* TabBarScrollableViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6412B0224AFF6A600F5412E /* TabBarScrollableViewController.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 */; };
|
||||
D6412B0B24B0D4C600F5412E /* ProfileHeaderView.xib in Resources */ = {isa = PBXBuildFile; fileRef = D6412B0A24B0D4C600F5412E /* ProfileHeaderView.xib */; };
|
||||
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 */; };
|
||||
D6434EB3215B1856001A919A /* XCBRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6434EB2215B1856001A919A /* XCBRequest.swift */; };
|
||||
D646C956213B365700269FB5 /* LargeImageExpandAnimationController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D646C955213B365700269FB5 /* LargeImageExpandAnimationController.swift */; };
|
||||
|
@ -142,6 +141,9 @@
|
|||
D64D0AB12128D9AE005A6F37 /* OnboardingViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D64D0AB02128D9AE005A6F37 /* OnboardingViewController.swift */; };
|
||||
D64D8CA92463B494006B0BAA /* MultiThreadDictionary.swift in Sources */ = {isa = PBXBuildFile; fileRef = D64D8CA82463B494006B0BAA /* MultiThreadDictionary.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 */; };
|
||||
D6531DF0246B867E000F9538 /* GifvAttachmentViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6531DEF246B867E000F9538 /* GifvAttachmentViewController.swift */; };
|
||||
D6538945214D6D7500E3CEFC /* TableViewSwipeActionProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6538944214D6D7500E3CEFC /* TableViewSwipeActionProvider.swift */; };
|
||||
|
@ -298,7 +300,6 @@
|
|||
D6F1F84D2193B56E00F5FE67 /* Cache.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6F1F84C2193B56E00F5FE67 /* Cache.swift */; };
|
||||
D6F2E965249E8BFD005846BB /* CrashReporterViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6F2E963249E8BFD005846BB /* CrashReporterViewController.swift */; };
|
||||
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 */; };
|
||||
D6FF9860255C717400845181 /* AccountSwitchingContainerViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6FF985F255C717400845181 /* AccountSwitchingContainerViewController.swift */; };
|
||||
/* End PBXBuildFile section */
|
||||
|
@ -466,16 +467,15 @@
|
|||
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>"; };
|
||||
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>"; };
|
||||
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>"; };
|
||||
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>"; };
|
||||
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>"; };
|
||||
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>"; };
|
||||
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>"; };
|
||||
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>"; };
|
||||
|
@ -492,6 +492,9 @@
|
|||
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>"; };
|
||||
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>"; };
|
||||
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>"; };
|
||||
|
@ -654,7 +657,6 @@
|
|||
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>"; };
|
||||
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>"; };
|
||||
D6FF985F255C717400845181 /* AccountSwitchingContainerViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountSwitchingContainerViewController.swift; sourceTree = "<group>"; };
|
||||
/* End PBXFileReference section */
|
||||
|
@ -973,7 +975,7 @@
|
|||
isa = PBXGroup;
|
||||
children = (
|
||||
D6BC9DD6232D7811002CA326 /* TimelinesPageViewController.swift */,
|
||||
D6F953EB212519E700CF0F2B /* TimelineTableViewController.swift */,
|
||||
D65234D225618EFA001AF9CF /* TimelineTableViewController.swift */,
|
||||
D6945C3123AC4D36005C403C /* HashtagTimelineViewController.swift */,
|
||||
D6945C3723AC739F005C403C /* InstanceTimelineViewController.swift */,
|
||||
);
|
||||
|
@ -1005,7 +1007,7 @@
|
|||
isa = PBXGroup;
|
||||
children = (
|
||||
D6412B0424B0227D00F5412E /* ProfileViewController.swift */,
|
||||
D6412B0624B0237700F5412E /* ProfileStatusesViewController.swift */,
|
||||
D63A8D0A2561C27F00D9DFFF /* ProfileStatusesViewController.swift */,
|
||||
D6412B0824B0291E00F5412E /* MyProfileViewController.swift */,
|
||||
);
|
||||
path = Profile;
|
||||
|
@ -1023,7 +1025,7 @@
|
|||
isa = PBXGroup;
|
||||
children = (
|
||||
D6BC9DB0232C61BC002CA326 /* NotificationsPageViewController.swift */,
|
||||
D641C772213CAA25004B4513 /* NotificationsTableViewController.swift */,
|
||||
D65234E02561AA68001AF9CF /* NotificationsTableViewController.swift */,
|
||||
);
|
||||
path = Notifications;
|
||||
sourceTree = "<group>";
|
||||
|
@ -1372,6 +1374,7 @@
|
|||
D6412B0224AFF6A600F5412E /* TabBarScrollableViewController.swift */,
|
||||
D65C6BF425478A9C00A6E89C /* BackgroundableViewController.swift */,
|
||||
D6B81F3B2560365300F6E31D /* RefreshableViewController.swift */,
|
||||
D65234C8256189D0001AF9CF /* TimelineLikeTableViewController.swift */,
|
||||
);
|
||||
path = Utilities;
|
||||
sourceTree = "<group>";
|
||||
|
@ -1851,6 +1854,7 @@
|
|||
D6BC9DB3232D4C07002CA326 /* WellnessPrefsView.swift in Sources */,
|
||||
D64BC18F23C18B9D000D0238 /* FollowRequestNotificationTableViewCell.swift in Sources */,
|
||||
D693DE5923FE24310061E07D /* InteractivePushTransition.swift in Sources */,
|
||||
D65234C9256189D0001AF9CF /* TimelineLikeTableViewController.swift in Sources */,
|
||||
D6A3BC8A2321F79B00FD64D5 /* AccountTableViewCell.swift in Sources */,
|
||||
D66A77BB233838DC0058F1EC /* UIFont+Traits.swift in Sources */,
|
||||
D68FEC4F232C5BC300C84F23 /* SegmentedPageViewController.swift in Sources */,
|
||||
|
@ -1865,7 +1869,6 @@
|
|||
D6F953F021251A2900CF0F2B /* MastodonController.swift in Sources */,
|
||||
0411610022B442870030A9B7 /* LoadingLargeImageViewController.swift in Sources */,
|
||||
D6A4DCE525537C7A00D9DE31 /* FastSwitchingAccountView.swift in Sources */,
|
||||
D6412B0724B0237700F5412E /* ProfileStatusesViewController.swift in Sources */,
|
||||
D6A4DCCC2553667800D9DE31 /* FastAccountSwitcherViewController.swift in Sources */,
|
||||
D611C2CF232DC61100C86A49 /* HashtagTableViewCell.swift in Sources */,
|
||||
D627944F23A9C99800D38C68 /* EditListAccountsViewController.swift in Sources */,
|
||||
|
@ -1887,6 +1890,7 @@
|
|||
D6CA6A92249FAD8900AD45C1 /* AudioSessionHelper.swift in Sources */,
|
||||
D6C143FD25354FD0007DC240 /* EmojiCollectionViewCell.swift in Sources */,
|
||||
D60D2B8223844C71001B87A3 /* BaseStatusTableViewCell.swift in Sources */,
|
||||
D63A8D0B2561C27F00D9DFFF /* ProfileStatusesViewController.swift in Sources */,
|
||||
D60E2F272442372B005F8713 /* StatusMO.swift in Sources */,
|
||||
D60E2F2C24423EAD005F8713 /* LazilyDecoding.swift in Sources */,
|
||||
D677284A24ECBDF400C732D3 /* ComposeCurrentAccount.swift in Sources */,
|
||||
|
@ -1988,6 +1992,7 @@
|
|||
D6FF9860255C717400845181 /* AccountSwitchingContainerViewController.swift in Sources */,
|
||||
D670F8B62537DC890046588A /* EmojiPickerWrapper.swift in Sources */,
|
||||
D6B81F442560390300F6E31D /* MenuController.swift in Sources */,
|
||||
D65234E12561AA68001AF9CF /* NotificationsTableViewController.swift in Sources */,
|
||||
D67C57AF21E28EAD00C3118B /* Array+Uniques.swift in Sources */,
|
||||
D6945C3223AC4D36005C403C /* HashtagTimelineViewController.swift in Sources */,
|
||||
D647D92824257BEB0005044F /* AttachmentPreviewViewController.swift in Sources */,
|
||||
|
@ -1997,7 +2002,6 @@
|
|||
D681E4D7246E32290053414F /* StatusActivityItemSource.swift in Sources */,
|
||||
D646C95A213B5D0500269FB5 /* LargeImageInteractionController.swift in Sources */,
|
||||
D6A3BC7C232195C600FD64D5 /* ActionNotificationGroupTableViewCell.swift in Sources */,
|
||||
D6F953EC212519E700CF0F2B /* TimelineTableViewController.swift in Sources */,
|
||||
D62275A824F1CA2800B82A16 /* ComposeReplyContentView.swift in Sources */,
|
||||
D677284824ECBCB100C732D3 /* ComposeView.swift in Sources */,
|
||||
D68232F72464F4FD00325FB8 /* ComposeDrawingViewController.swift in Sources */,
|
||||
|
@ -2007,12 +2011,12 @@
|
|||
D68015402401A6BA00D6103B /* ComposingPrefsView.swift in Sources */,
|
||||
D67895BC24671E6D00D4CD9E /* PKDrawing+Render.swift in Sources */,
|
||||
04D14BB022B34A2800642648 /* GalleryViewController.swift in Sources */,
|
||||
D641C773213CAA25004B4513 /* NotificationsTableViewController.swift in Sources */,
|
||||
D6C1B2082545D1EC00DAAA66 /* StatusCardView.swift in Sources */,
|
||||
D6412B0524B0227D00F5412E /* ProfileViewController.swift in Sources */,
|
||||
D64BC18A23C16487000D0238 /* UnpinStatusActivity.swift in Sources */,
|
||||
D64D8CA92463B494006B0BAA /* MultiThreadDictionary.swift in Sources */,
|
||||
D6757A7C2157E01900721E32 /* XCBManager.swift in Sources */,
|
||||
D65234D325618EFA001AF9CF /* TimelineTableViewController.swift in Sources */,
|
||||
D68E6F5F253C9B2D001A1B4C /* BaseEmojiLabel.swift in Sources */,
|
||||
D6F1F84D2193B56E00F5FE67 /* Cache.swift in Sources */,
|
||||
D6F0B12B24A3071C001E48C3 /* MainSplitViewController.swift in Sources */,
|
||||
|
|
|
@ -58,8 +58,7 @@ class ListTimelineViewController: TimelineTableViewController {
|
|||
dismiss(animated: true)
|
||||
|
||||
// todo: show loading indicator
|
||||
timelineSegments = []
|
||||
loadInitialStatuses()
|
||||
reloadInitialItems()
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -9,7 +9,7 @@
|
|||
import UIKit
|
||||
import Pachyderm
|
||||
|
||||
class NotificationsTableViewController: EnhancedTableViewController {
|
||||
class NotificationsTableViewController: TimelineLikeTableViewController<NotificationGroup> {
|
||||
|
||||
private let statusCell = "statusCell"
|
||||
private let actionGroupCell = "actionGroupCell"
|
||||
|
@ -20,125 +20,112 @@ class NotificationsTableViewController: EnhancedTableViewController {
|
|||
weak var mastodonController: MastodonController!
|
||||
|
||||
private let excludedTypes: [Pachyderm.Notification.Kind]
|
||||
private let groupTypes = [Notification.Kind.favourite, .reblog, .follow]
|
||||
private let groupTypes = [Pachyderm.Notification.Kind.favourite, .reblog, .follow]
|
||||
|
||||
private var loaded = false
|
||||
|
||||
private var groups: [NotificationGroup] = []
|
||||
|
||||
private let pageSize = 20
|
||||
private var newer: RequestRange?
|
||||
private var older: RequestRange?
|
||||
|
||||
private var lastLastVisibleRow: IndexPath?
|
||||
|
||||
init(allowedTypes: [Pachyderm.Notification.Kind], mastodonController: MastodonController) {
|
||||
self.excludedTypes = Array(Set(Pachyderm.Notification.Kind.allCases).subtracting(allowedTypes))
|
||||
self.mastodonController = mastodonController
|
||||
|
||||
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")))
|
||||
super.init()
|
||||
}
|
||||
|
||||
required init?(coder aDecoder: NSCoder) {
|
||||
required init?(coder: NSCoder) {
|
||||
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() {
|
||||
super.viewDidLoad()
|
||||
|
||||
tableView.rowHeight = UITableView.automaticDimension
|
||||
tableView.estimatedRowHeight = 140
|
||||
|
||||
tableView.register(UINib(nibName: "TimelineStatusTableViewCell", bundle: .main), forCellReuseIdentifier: statusCell)
|
||||
tableView.register(UINib(nibName: "ActionNotificationGroupTableViewCell", bundle: .main), forCellReuseIdentifier: actionGroupCell)
|
||||
tableView.register(UINib(nibName: "FollowNotificationGroupTableViewCell", bundle: .main), forCellReuseIdentifier: followGroupCell)
|
||||
tableView.register(UINib(nibName: "FollowRequestNotificationTableViewCell", bundle: .main), forCellReuseIdentifier: followRequestCell)
|
||||
tableView.register(UINib(nibName: "BasicTableViewCell", bundle: .main), forCellReuseIdentifier: unknownCell)
|
||||
|
||||
tableView.prefetchDataSource = self
|
||||
}
|
||||
|
||||
override func viewWillAppear(_ animated: Bool) {
|
||||
super.viewWillAppear(animated)
|
||||
|
||||
if !loaded {
|
||||
loaded = true
|
||||
|
||||
override func loadInitialItems(completion: @escaping ([NotificationGroup]) -> Void) {
|
||||
let request = Client.getNotifications(excludeTypes: excludedTypes)
|
||||
mastodonController.run(request) { result in
|
||||
guard case let .success(notifications, pagination) = result else { fatalError() }
|
||||
mastodonController.run(request) { (response) in
|
||||
guard case let .success(notifications, pagination) = response else { fatalError() }
|
||||
|
||||
let groups = NotificationGroup.createGroups(notifications: notifications, only: self.groupTypes)
|
||||
|
||||
self.groups.append(contentsOf: groups)
|
||||
|
||||
self.newer = pagination?.newer
|
||||
self.older = pagination?.older
|
||||
|
||||
self.mastodonController.persistentContainer.addAll(notifications: notifications) {
|
||||
DispatchQueue.main.async {
|
||||
self.tableView.reloadData()
|
||||
}
|
||||
}
|
||||
completion(groups)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override func viewWillDisappear(_ animated: Bool) {
|
||||
super.viewWillDisappear(animated)
|
||||
|
||||
pruneOffscreenRows()
|
||||
}
|
||||
|
||||
private func pruneOffscreenRows() {
|
||||
guard let lastVisibleRow = lastLastVisibleRow else {
|
||||
override func loadOlder(completion: @escaping ([NotificationGroup]) -> Void) {
|
||||
guard let older = older else {
|
||||
completion([])
|
||||
return
|
||||
}
|
||||
let lastRowIndex = groups.count - 1
|
||||
|
||||
if lastVisibleRow.row < lastRowIndex - pageSize {
|
||||
// if there are more than 20 rows below the lats visible one
|
||||
let request = Client.getNotifications(excludeTypes: excludedTypes, range: older)
|
||||
mastodonController.run(request) { (response) in
|
||||
guard case let .success(newNotifications, pagination) = response else { fatalError() }
|
||||
|
||||
let rowIndicesToRemove = (lastVisibleRow.row + pageSize)..<groups.count
|
||||
self.older = pagination?.older
|
||||
|
||||
let groupsToRemove = groups[rowIndicesToRemove]
|
||||
for group in groupsToRemove {
|
||||
for notification in group.notifications {
|
||||
if let id = notification.status?.id {
|
||||
mastodonController.persistentContainer.status(for: id)?.decrementReferenceCount()
|
||||
let groups = NotificationGroup.createGroups(notifications: newNotifications, only: self.groupTypes)
|
||||
|
||||
self.mastodonController.persistentContainer.addAll(notifications: newNotifications) {
|
||||
completion(groups)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
groups.removeSubrange(rowIndicesToRemove)
|
||||
override func loadNewer(completion: @escaping ([NotificationGroup]) -> Void) {
|
||||
guard let newer = newer else {
|
||||
completion([])
|
||||
return
|
||||
}
|
||||
|
||||
let removedIndexPaths = rowIndicesToRemove.map { IndexPath(row: $0, section: 0) }
|
||||
UIView.performWithoutAnimation {
|
||||
tableView.deleteRows(at: removedIndexPaths, with: .none)
|
||||
let request = Client.getNotifications(excludeTypes: excludedTypes, range: newer)
|
||||
mastodonController.run(request) { (response) in
|
||||
guard case let .success(newNotifications, pagination) = response else { fatalError() }
|
||||
|
||||
self.newer = pagination?.newer
|
||||
|
||||
let groups = NotificationGroup.createGroups(notifications: newNotifications, only: self.groupTypes)
|
||||
|
||||
self.mastodonController.persistentContainer.addAll(notifications: newNotifications) {
|
||||
completion(groups)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Table view data source
|
||||
|
||||
override func numberOfSections(in tableView: UITableView) -> Int {
|
||||
return 1
|
||||
private func dismissNotificationsInGroup(at indexPath: IndexPath, completion: (() -> Void)? = nil) {
|
||||
let group = DispatchGroup()
|
||||
item(for: indexPath).notifications
|
||||
.map { Pachyderm.Notification.dismiss(id: $0.id) }
|
||||
.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?()
|
||||
}
|
||||
}
|
||||
|
||||
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
|
||||
return groups.count
|
||||
}
|
||||
|
||||
|
||||
// MARK: - UITableViewDataSource
|
||||
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
|
||||
let group = groups[indexPath.row]
|
||||
let group = item(for: indexPath)
|
||||
|
||||
switch group.kind {
|
||||
case .mention:
|
||||
|
@ -176,45 +163,7 @@ class NotificationsTableViewController: EnhancedTableViewController {
|
|||
}
|
||||
}
|
||||
|
||||
// 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()
|
||||
}
|
||||
// MARK: - UITableViewDelegate
|
||||
|
||||
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
|
||||
|
@ -223,7 +172,9 @@ class NotificationsTableViewController: EnhancedTableViewController {
|
|||
}
|
||||
}
|
||||
dismissAction.image = UIImage(systemName: "clear.fill")
|
||||
|
||||
let cellConfiguration = (tableView.cellForRow(at: indexPath) as? TableViewSwipeActionProvider)?.trailingSwipeActionsConfiguration()
|
||||
|
||||
let config: UISwipeActionsConfiguration
|
||||
if let cellConfiguration = cellConfiguration {
|
||||
config = UISwipeActionsConfiguration(actions: cellConfiguration.actions + [dismissAction])
|
||||
|
@ -237,78 +188,27 @@ class NotificationsTableViewController: EnhancedTableViewController {
|
|||
|
||||
override func getSuggestedContextMenuActions(tableView: UITableView, indexPath: IndexPath, point: CGPoint) -> [UIAction] {
|
||||
return [
|
||||
UIAction(title: "Dismiss Notification", image: UIImage(systemName: "clear.fill"), identifier: .init("dismissnotification"), discoverabilityTitle: nil, attributes: [], state: .off, handler: { (_) in
|
||||
UIAction(title: "Dismiss Notification", image: UIImage(systemName: "clear.fill"), identifier: .init("dismissnotification"), handler: { (_) in
|
||||
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 {
|
||||
var apiController: MastodonController { mastodonController }
|
||||
func statusCellCollapsedStateChanged(_ cell: BaseStatusTableViewCell) {
|
||||
// causes the table view to recalculate the cell heights
|
||||
tableView.beginUpdates()
|
||||
tableView.endUpdates()
|
||||
cellHeightChanged()
|
||||
}
|
||||
}
|
||||
|
||||
extension NotificationsTableViewController: UITableViewDataSourcePrefetching {
|
||||
func tableView(_ tableView: UITableView, prefetchRowsAt indexPaths: [IndexPath]) {
|
||||
for indexPath in indexPaths {
|
||||
for notification in groups[indexPath.row].notifications {
|
||||
// todo: this account object could be stale
|
||||
for notification in item(for: indexPath).notifications {
|
||||
_ = ImageCache.avatars.get(notification.account.avatar, completion: nil)
|
||||
}
|
||||
}
|
||||
|
@ -316,21 +216,9 @@ extension NotificationsTableViewController: UITableViewDataSourcePrefetching {
|
|||
|
||||
func tableView(_ tableView: UITableView, cancelPrefetchingForRowsAt indexPaths: [IndexPath]) {
|
||||
for indexPath in indexPaths {
|
||||
for notification in groups[indexPath.row].notifications {
|
||||
for notification in item(for: indexPath).notifications {
|
||||
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 Pachyderm
|
||||
|
||||
class ProfileStatusesViewController: EnhancedTableViewController {
|
||||
class ProfileStatusesViewController: TimelineLikeTableViewController<TimelineEntry> {
|
||||
|
||||
weak var mastodonController: MastodonController!
|
||||
|
||||
|
@ -19,20 +19,15 @@ class ProfileStatusesViewController: EnhancedTableViewController {
|
|||
|
||||
let kind: Kind
|
||||
|
||||
private var pinnedStatuses: [(id: String, state: StatusState)] = []
|
||||
private var timelineSegments: [[(id: String, state: StatusState)]] = []
|
||||
|
||||
private var older: RequestRange?
|
||||
private var newer: RequestRange?
|
||||
|
||||
var loaded = false
|
||||
|
||||
init(accountID: String?, kind: Kind, mastodonController: MastodonController) {
|
||||
self.accountID = accountID
|
||||
self.kind = kind
|
||||
self.mastodonController = mastodonController
|
||||
|
||||
super.init(style: .plain)
|
||||
super.init()
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
|
@ -42,76 +37,117 @@ class ProfileStatusesViewController: EnhancedTableViewController {
|
|||
override func 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.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) {
|
||||
guard !loaded else { return }
|
||||
loaded = true
|
||||
loadInitial()
|
||||
}
|
||||
|
||||
if kind == .statuses {
|
||||
override class func refreshCommandTitle() -> String {
|
||||
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
|
||||
guard case let .success(statuses, _) = response else {
|
||||
guard case let .success(statuses, _) = response,
|
||||
!statuses.isEmpty else {
|
||||
// todo: error message
|
||||
return
|
||||
}
|
||||
if statuses.isEmpty { return }
|
||||
|
||||
self.mastodonController.persistentContainer.addAll(statuses: statuses) {
|
||||
self.pinnedStatuses = statuses.map { ($0.id, .unknown) }
|
||||
let indexPaths = (0..<statuses.count).map { IndexPath(row: $0, section: 0) }
|
||||
let items = statuses.map { ($0.id, StatusState.unknown) }
|
||||
DispatchQueue.main.async {
|
||||
UIView.performWithoutAnimation {
|
||||
self.tableView.insertRows(at: indexPaths, with: .none)
|
||||
if self.sections.count < 1 {
|
||||
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
|
||||
guard case let .success(statuses, pagination) = response else {
|
||||
guard case let .success(statuses, pagination) = response,
|
||||
!statuses.isEmpty else {
|
||||
// todo: error message
|
||||
return
|
||||
}
|
||||
if statuses.isEmpty { return }
|
||||
|
||||
self.mastodonController.persistentContainer.addAll(statuses: statuses) {
|
||||
self.timelineSegments.append(statuses.map { ($0.id, .unknown) })
|
||||
|
||||
self.older = pagination?.older
|
||||
self.newer = pagination?.newer
|
||||
|
||||
DispatchQueue.main.async {
|
||||
UIView.performWithoutAnimation {
|
||||
self.tableView.insertSections(IndexSet(integer: 1), with: .none)
|
||||
self.mastodonController.persistentContainer.addAll(statuses: statuses) {
|
||||
completion(statuses.map { ($0.id, .unknown) })
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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) })
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -133,47 +169,19 @@ class ProfileStatusesViewController: EnhancedTableViewController {
|
|||
mastodonController.run(request, completion: completion)
|
||||
}
|
||||
|
||||
// MARK: Interaction
|
||||
|
||||
@objc func refreshStatuses() {
|
||||
guard let newer = newer else { return }
|
||||
|
||||
getStatuses(for: newer) { (response) in
|
||||
guard case let .success(newStatuses, pagination) = response else {
|
||||
// todo: error message
|
||||
return
|
||||
}
|
||||
|
||||
self.mastodonController.persistentContainer.addAll(statuses: newStatuses) {
|
||||
// if there's no newer request range (because no statuses were returned),
|
||||
// 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()
|
||||
}
|
||||
}
|
||||
}
|
||||
override func refresh() {
|
||||
super.refresh()
|
||||
|
||||
if kind == .statuses {
|
||||
getPinnedStatuses { (response) in
|
||||
guard case let .success(newPinnedStatuses, _) = response else {
|
||||
guard case let .success(newPinnedStatues, _) = 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
|
||||
self.mastodonController.persistentContainer.addAll(statuses: newPinnedStatues) {
|
||||
let oldPinnedStatuses = self.sections[0]
|
||||
let pinnedStatues = newPinnedStatues.map { (status) -> TimelineEntry in
|
||||
let state: StatusState
|
||||
if let (_, oldState) = oldPinnedStatuses.first(where: { $0.id == status.id }) {
|
||||
state = oldState
|
||||
|
@ -183,7 +191,11 @@ class ProfileStatusesViewController: EnhancedTableViewController {
|
|||
return (status.id, state)
|
||||
}
|
||||
DispatchQueue.main.async {
|
||||
self.pinnedStatuses = pinnedStatuses
|
||||
if self.sections.count < 1 {
|
||||
self.sections.append(pinnedStatues)
|
||||
} else {
|
||||
self.sections[0] = pinnedStatues
|
||||
}
|
||||
UIView.performWithoutAnimation {
|
||||
self.tableView.reloadSections(IndexSet(integer: 0), with: .none)
|
||||
}
|
||||
|
@ -193,83 +205,19 @@ class ProfileStatusesViewController: EnhancedTableViewController {
|
|||
}
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// MARK: - UITableViewDatasource
|
||||
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
|
||||
let cell = tableView.dequeueReusableCell(withIdentifier: "statusCell", for: indexPath) as! TimelineStatusTableViewCell
|
||||
|
||||
cell.delegate = self
|
||||
cell.showPinned = indexPath.section == 0
|
||||
|
||||
if indexPath.section == 0 {
|
||||
cell.showPinned = true
|
||||
let (id, state) = pinnedStatuses[indexPath.row]
|
||||
let (id, state) = item(for: indexPath)
|
||||
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
|
||||
}
|
||||
|
||||
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 {
|
||||
|
@ -278,26 +226,25 @@ extension ProfileStatusesViewController {
|
|||
}
|
||||
}
|
||||
|
||||
extension ProfileStatusesViewController: StatusTableViewCellDelegate {
|
||||
extension ProfileStatusesViewController: TuskerNavigationDelegate {
|
||||
var apiController: MastodonController { mastodonController }
|
||||
}
|
||||
|
||||
extension ProfileStatusesViewController: StatusTableViewCellDelegate {
|
||||
func statusCellCollapsedStateChanged(_ cell: BaseStatusTableViewCell) {
|
||||
// causes the table view to recalculate the cell heights
|
||||
tableView.beginUpdates()
|
||||
tableView.endUpdates()
|
||||
cellHeightChanged()
|
||||
}
|
||||
}
|
||||
|
||||
extension ProfileStatusesViewController: UITableViewDataSourcePrefetching {
|
||||
func tableView(_ tableView: UITableView, prefetchRowsAt indexPaths: [IndexPath]) {
|
||||
for indexPath in indexPaths {
|
||||
let statusID: String
|
||||
if indexPath.section == 0 {
|
||||
statusID = pinnedStatuses[indexPath.row].id
|
||||
} else {
|
||||
statusID = timelineSegments[indexPath.section - 1][indexPath.row].id
|
||||
let statusID = item(for: indexPath).id
|
||||
|
||||
guard let status = mastodonController.persistentContainer.status(for: statusID) else {
|
||||
continue
|
||||
}
|
||||
guard let status = mastodonController.persistentContainer.status(for: statusID) else { continue }
|
||||
|
||||
_ = ImageCache.avatars.get(status.account.avatar, completion: nil)
|
||||
for attachment in status.attachments where attachment.kind == .image {
|
||||
_ = ImageCache.attachments.get(attachment.url, completion: nil)
|
||||
|
@ -307,13 +254,11 @@ extension ProfileStatusesViewController: UITableViewDataSourcePrefetching {
|
|||
|
||||
func tableView(_ tableView: UITableView, cancelPrefetchingForRowsAt indexPaths: [IndexPath]) {
|
||||
for indexPath in indexPaths {
|
||||
let statusID: String
|
||||
if indexPath.section == 0 {
|
||||
statusID = pinnedStatuses[indexPath.row].id
|
||||
} else {
|
||||
statusID = timelineSegments[indexPath.section - 1][indexPath.row].id
|
||||
let statusID = item(for: indexPath).id
|
||||
|
||||
guard let status = mastodonController.persistentContainer.status(for: statusID) else {
|
||||
continue
|
||||
}
|
||||
guard let status = mastodonController.persistentContainer.status(for: statusID) else { continue }
|
||||
ImageCache.avatars.cancelWithoutCallback(status.account.avatar)
|
||||
for attachment in status.attachments where attachment.kind == .image {
|
||||
ImageCache.avatars.cancelWithoutCallback(attachment.url)
|
||||
|
@ -321,9 +266,3 @@ extension ProfileStatusesViewController: UITableViewDataSourcePrefetching {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension ProfileStatusesViewController: RefreshableViewController {
|
||||
func refresh() {
|
||||
refreshStatuses()
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
//
|
||||
// StatusesTableViewController.swift
|
||||
// TimelineTableViewController.swift
|
||||
// Tusker
|
||||
//
|
||||
// Created by Shadowfacts on 8/15/18.
|
||||
|
@ -9,41 +9,29 @@
|
|||
import UIKit
|
||||
import Pachyderm
|
||||
|
||||
class TimelineTableViewController: EnhancedTableViewController, StatusTableViewCellDelegate {
|
||||
typealias TimelineEntry = (id: String, state: StatusState)
|
||||
|
||||
var timeline: Timeline!
|
||||
class TimelineTableViewController: TimelineLikeTableViewController<TimelineEntry> {
|
||||
|
||||
let timeline: Timeline
|
||||
weak var mastodonController: MastodonController!
|
||||
|
||||
private var loaded = false
|
||||
|
||||
var timelineSegments: [[(id: String, state: StatusState)]] = []
|
||||
|
||||
private let pageSize = 20
|
||||
private var newer: RequestRange?
|
||||
private var older: RequestRange?
|
||||
|
||||
private var lastLastVisibleRow: IndexPath?
|
||||
|
||||
init(for timeline: Timeline, mastodonController: MastodonController) {
|
||||
self.timeline = timeline
|
||||
self.mastodonController = mastodonController
|
||||
|
||||
super.init(style: .plain)
|
||||
super.init()
|
||||
|
||||
title = timeline.title
|
||||
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)
|
||||
}
|
||||
|
||||
required init?(coder aDecoder: NSCoder) {
|
||||
required init?(coder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
|
@ -52,120 +40,89 @@ class TimelineTableViewController: EnhancedTableViewController, StatusTableViewC
|
|||
// decrement reference counts of any statuses we still have
|
||||
// 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(_:)
|
||||
for segment in timelineSegments {
|
||||
for (id, _) in segment {
|
||||
for section in sections {
|
||||
for (id, _) in section {
|
||||
persistentContainer.status(for: id)?.decrementReferenceCount()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func statusID(for indexPath: IndexPath) -> String {
|
||||
return timelineSegments[indexPath.section][indexPath.row].id
|
||||
}
|
||||
|
||||
override func viewDidLoad() {
|
||||
super.viewDidLoad()
|
||||
|
||||
tableView.rowHeight = UITableView.automaticDimension
|
||||
tableView.estimatedRowHeight = 140
|
||||
|
||||
tableView.register(UINib(nibName: "TimelineStatusTableViewCell", bundle: nil), forCellReuseIdentifier: "statusCell")
|
||||
|
||||
tableView.prefetchDataSource = self
|
||||
tableView.register(UINib(nibName: "TimelineStatusTableViewCell", bundle: .main), forCellReuseIdentifier: "statusCell")
|
||||
}
|
||||
|
||||
override func viewWillAppear(_ animated: Bool) {
|
||||
super.viewWillAppear(animated)
|
||||
|
||||
loadInitialStatuses()
|
||||
override class func refreshCommandTitle() -> String {
|
||||
return NSLocalizedString("Refresh Statuses", comment: "refresh status command discoverability title")
|
||||
}
|
||||
|
||||
override func viewWillDisappear(_ animated: Bool) {
|
||||
super.viewWillDisappear(animated)
|
||||
|
||||
pruneOffscreenRows()
|
||||
override func willRemoveRows(at indexPaths: [IndexPath]) {
|
||||
for indexPath in indexPaths {
|
||||
let id = item(for: indexPath).id
|
||||
mastodonController.persistentContainer.status(for: id)?.decrementReferenceCount()
|
||||
}
|
||||
}
|
||||
|
||||
func loadInitialStatuses() {
|
||||
guard !loaded else { return }
|
||||
loaded = true
|
||||
|
||||
override func loadInitialItems(completion: @escaping ([TimelineEntry]) -> Void) {
|
||||
let request = Client.getStatuses(timeline: timeline)
|
||||
mastodonController.run(request) { response in
|
||||
|
||||
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()
|
||||
}
|
||||
|
||||
self.mastodonController?.persistentContainer.addAll(statuses: statuses) {
|
||||
completion(statuses.map { ($0.id, .unknown) })
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func pruneOffscreenRows() {
|
||||
guard let lastVisibleRow = lastLastVisibleRow else {
|
||||
override func loadOlder(completion: @escaping ([TimelineEntry]) -> Void) {
|
||||
guard let older = older else {
|
||||
completion([])
|
||||
return
|
||||
}
|
||||
let lastSectionIndex = timelineSegments.count - 1
|
||||
|
||||
if lastVisibleRow.section < lastSectionIndex {
|
||||
// if there is a section below the last visible one
|
||||
let request = Client.getStatuses(timeline: timeline, range: older)
|
||||
|
||||
let sectionsToRemove = (lastVisibleRow.section + 1)...lastSectionIndex
|
||||
mastodonController.run(request) { (response) in
|
||||
guard case let .success(statuses, pagination) = response else { fatalError() }
|
||||
|
||||
for section in sectionsToRemove {
|
||||
for (id, _) in timelineSegments.remove(at: section) {
|
||||
mastodonController.persistentContainer.status(for: id)?.decrementReferenceCount()
|
||||
}
|
||||
}
|
||||
|
||||
UIView.performWithoutAnimation {
|
||||
tableView.deleteSections(IndexSet(sectionsToRemove), with: .none)
|
||||
}
|
||||
|
||||
} else if lastVisibleRow.section == lastSectionIndex {
|
||||
let lastSection = timelineSegments.last!
|
||||
let lastRowIndex = lastSection.count - 1
|
||||
|
||||
if lastVisibleRow.row < lastRowIndex - 20 {
|
||||
// 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.older = pagination?.older
|
||||
|
||||
self.mastodonController?.persistentContainer.addAll(statuses: statuses) {
|
||||
completion(statuses.map { ($0.id, .unknown) })
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Table view data source
|
||||
|
||||
override func numberOfSections(in tableView: UITableView) -> Int {
|
||||
return timelineSegments.count
|
||||
override func loadNewer(completion: @escaping ([TimelineEntry]) -> Void) {
|
||||
guard let newer = newer else {
|
||||
completion([])
|
||||
return
|
||||
}
|
||||
|
||||
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
|
||||
return timelineSegments[section].count
|
||||
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 {
|
||||
guard let cell = tableView.dequeueReusableCell(withIdentifier: "statusCell", for: indexPath) as? TimelineStatusTableViewCell else { fatalError() }
|
||||
|
||||
let (id, state) = timelineSegments[indexPath.section][indexPath.row]
|
||||
let (id, state) = item(for: indexPath)
|
||||
cell.delegate = self
|
||||
|
||||
cell.updateUI(statusID: id, state: state)
|
||||
|
@ -173,104 +130,24 @@ class TimelineTableViewController: EnhancedTableViewController, StatusTableViewC
|
|||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
extension TimelineTableViewController: TuskerNavigationDelegate {
|
||||
var apiController: MastodonController { mastodonController }
|
||||
}
|
||||
|
||||
// MARK: - StatusTableViewCellDelegate
|
||||
|
||||
extension TimelineTableViewController: StatusTableViewCellDelegate {
|
||||
func statusCellCollapsedStateChanged(_ cell: BaseStatusTableViewCell) {
|
||||
// causes the table view to recalculate the cell heights
|
||||
tableView.beginUpdates()
|
||||
tableView.endUpdates()
|
||||
cellHeightChanged()
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
extension TimelineTableViewController: UITableViewDataSourcePrefetching {
|
||||
func tableView(_ tableView: UITableView, prefetchRowsAt indexPaths: [IndexPath]) {
|
||||
for indexPath in indexPaths {
|
||||
guard let status = mastodonController.persistentContainer.status(for: statusID(for: indexPath)) else { continue }
|
||||
guard let status = mastodonController.persistentContainer.status(for: item(for: indexPath).id) else {
|
||||
continue
|
||||
}
|
||||
_ = ImageCache.avatars.get(status.account.avatar, completion: nil)
|
||||
for attachment in status.attachments where attachment.kind == .image {
|
||||
_ = ImageCache.attachments.get(attachment.url, completion: nil)
|
||||
|
@ -282,8 +159,11 @@ extension TimelineTableViewController: UITableViewDataSourcePrefetching {
|
|||
for indexPath in indexPaths {
|
||||
// todo: this means when removing cells, we can't cancel prefetching
|
||||
// is this an issue?
|
||||
guard indexPath.section < timelineSegments.count, indexPath.row < timelineSegments[indexPath.section].count,
|
||||
let status = mastodonController.persistentContainer.status(for: statusID(for: indexPath)) else { continue }
|
||||
guard indexPath.section < sections.count,
|
||||
indexPath.row < sections[indexPath.section].count,
|
||||
let status = mastodonController.persistentContainer.status(for: item(for: indexPath).id) else {
|
||||
continue
|
||||
}
|
||||
ImageCache.avatars.cancelWithoutCallback(status.account.avatar)
|
||||
for attachment in status.attachments where attachment.kind == .image {
|
||||
ImageCache.attachments.cancelWithoutCallback(attachment.url)
|
||||
|
@ -291,15 +171,3 @@ extension TimelineTableViewController: UITableViewDataSourcePrefetching {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension TimelineTableViewController: RefreshableViewController {
|
||||
func refresh() {
|
||||
refreshStatuses()
|
||||
}
|
||||
}
|
||||
|
||||
extension TimelineTableViewController: BackgroundableViewController {
|
||||
func sceneDidEnterBackground() {
|
||||
pruneOffscreenRows()
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,243 @@
|
|||
//
|
||||
// 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