From d9b21a0196942e6a608d30b996da00c1d3922d81 Mon Sep 17 00:00:00 2001 From: Shadowfacts Date: Wed, 31 Jul 2019 17:33:48 -0600 Subject: [PATCH] Represent timelines internally as segments Primarily in preparation for timeline position persistence and split timelines --- Pachyderm/Request/Request.swift | 22 ++----- Pachyderm/Request/RequestRange.swift | 2 +- Pachyderm/Response/Pagination.swift | 3 +- Pachyderm/TimelineSegment.swift | 57 +++++++++++++++++++ Tusker.xcodeproj/project.pbxproj | 5 +- .../TimelineTableViewController.swift | 28 ++++----- 6 files changed, 83 insertions(+), 34 deletions(-) create mode 100644 Pachyderm/TimelineSegment.swift diff --git a/Pachyderm/Request/Request.swift b/Pachyderm/Request/Request.swift index 56ee5e04..c21aaf67 100644 --- a/Pachyderm/Request/Request.swift +++ b/Pachyderm/Request/Request.swift @@ -40,25 +40,11 @@ extension Request { } set { let rangeParams = newValue.queryParameters - if let max = rangeParams.first(where: { $0.name == "max_id" }) { - if let i = queryParameters.firstIndex(where: { $0.name == "max_id" }) { - queryParameters[i] = max + for param in rangeParams { + if let i = queryParameters.firstIndex(where: { $0.name == param.name }) { + queryParameters[i] = param } else { - queryParameters.append(max) - } - } - if let since = rangeParams.first(where: { $0.name == "since_id" }) { - if let i = queryParameters.firstIndex(where: { $0.name == "since_id" }) { - queryParameters[i] = since - } else { - queryParameters.append(since) - } - } - if let count = rangeParams.first(where: { $0.name == "count" }) { - if let i = queryParameters.firstIndex(where: { $0.name == "count" }) { - queryParameters[i] = count - } else { - queryParameters.append(count) + queryParameters.append(param) } } } diff --git a/Pachyderm/Request/RequestRange.swift b/Pachyderm/Request/RequestRange.swift index 9e6d66b1..4b7a71b9 100644 --- a/Pachyderm/Request/RequestRange.swift +++ b/Pachyderm/Request/RequestRange.swift @@ -25,7 +25,7 @@ extension RequestRange { case let .before(id, count): return ["max_id" => id, "count" => count] case let .after(id, count): - return ["since_id" => id, "count" => count] + return ["min_id" => id, "count" => count] } } } diff --git a/Pachyderm/Response/Pagination.swift b/Pachyderm/Response/Pagination.swift index 07d86a54..a80bb3c8 100644 --- a/Pachyderm/Response/Pagination.swift +++ b/Pachyderm/Response/Pagination.swift @@ -52,10 +52,11 @@ extension Pagination { let components = URLComponents(string: validURL), let queryItems = components.queryItems else { return nil } + let min = queryItems.first { $0.name == "min_id" }?.value let since = queryItems.first { $0.name == "since_id" }?.value let max = queryItems.first { $0.name == "max_id" }?.value - guard let id = since ?? max else { return nil } + guard let id = min ?? since ?? max else { return nil } let limit = queryItems.first { $0.name == "limit" }.flatMap { $0.value }.flatMap { Int($0) } diff --git a/Pachyderm/TimelineSegment.swift b/Pachyderm/TimelineSegment.swift new file mode 100644 index 00000000..9cbc47b6 --- /dev/null +++ b/Pachyderm/TimelineSegment.swift @@ -0,0 +1,57 @@ +// +// TimelineSegment.swift +// Pachyderm +// +// Created by Shadowfacts on 7/29/19. +// Copyright © 2019 Shadowfacts. All rights reserved. +// + +import Foundation + +public struct TimelineSegment { + private var ids: [String] + + public init(objects: [Type]) { + self.ids = objects.map { $0.id } + } + + public mutating func insertAtBeginning(ids: [String]) { + self.ids.insert(contentsOf: ids, at: 0) + } + + public mutating func insertAtBeginning(objects: [Type]) { + insertAtBeginning(ids: objects.map { $0.id }) + } + + public mutating func append(ids: [String]) { + self.ids.append(contentsOf: ids) + } + + public mutating func append(objects: [Type]) { + append(ids: objects.map { $0.id }) + } +} + +extension TimelineSegment: RandomAccessCollection { + public typealias Index = Int + + public subscript(index: Int) -> String { + return ids[index] + } + + public var startIndex: Int { + return ids.startIndex + } + + public var endIndex: Int { + return ids.endIndex + } +} + +// todo: remove me when i update to beta 5, Identifiable is now part of Swift stdlib +public protocol Identifiable { + var id: String { get } +} + +extension Status: Identifiable {} +extension Notification: Identifiable {} diff --git a/Tusker.xcodeproj/project.pbxproj b/Tusker.xcodeproj/project.pbxproj index 7ef5203b..9f1bebb9 100644 --- a/Tusker.xcodeproj/project.pbxproj +++ b/Tusker.xcodeproj/project.pbxproj @@ -168,6 +168,7 @@ D6D4DDE5212518A200E1C4BB /* TuskerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6D4DDE4212518A200E1C4BB /* TuskerTests.swift */; }; D6D4DDF0212518A200E1C4BB /* TuskerUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6D4DDEF212518A200E1C4BB /* TuskerUITests.swift */; }; D6D58DF922074B74009C8DD9 /* LinkLabel.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6D58DF822074B74009C8DD9 /* LinkLabel.swift */; }; + D6DD353B22F25D2E00A9563A /* TimelineSegment.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6DD353A22F25D2E00A9563A /* TimelineSegment.swift */; }; D6E0DC8E216EDF1E00369478 /* Previewing.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6E0DC8D216EDF1E00369478 /* Previewing.swift */; }; D6E6F26321603F8B006A8599 /* CharacterCounter.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6E6F26221603F8B006A8599 /* CharacterCounter.swift */; }; D6E6F26521604242006A8599 /* CharacterCounterTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6E6F26421604242006A8599 /* CharacterCounterTests.swift */; }; @@ -405,6 +406,7 @@ D6D4DDEF212518A200E1C4BB /* TuskerUITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TuskerUITests.swift; sourceTree = ""; }; D6D4DDF1212518A200E1C4BB /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; D6D58DF822074B74009C8DD9 /* LinkLabel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LinkLabel.swift; sourceTree = ""; }; + D6DD353A22F25D2E00A9563A /* TimelineSegment.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = TimelineSegment.swift; path = ../../../../../../System/Volumes/Data/Users/shadowfacts/Dev/iOS/Tusker/Pachyderm/TimelineSegment.swift; sourceTree = ""; }; D6E0DC8D216EDF1E00369478 /* Previewing.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Previewing.swift; sourceTree = ""; }; D6E6F26221603F8B006A8599 /* CharacterCounter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CharacterCounter.swift; sourceTree = ""; }; D6E6F26421604242006A8599 /* CharacterCounterTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CharacterCounterTests.swift; sourceTree = ""; }; @@ -542,6 +544,7 @@ D61099C82144B13C00432DC2 /* Client.swift */, D6109A0A2145953C00432DC2 /* ClientModel.swift */, D6E6F26221603F8B006A8599 /* CharacterCounter.swift */, + D6DD353A22F25D2E00A9563A /* TimelineSegment.swift */, D61099D72144B74500432DC2 /* Extensions */, D61099CC2144B2C300432DC2 /* Request */, D61099DA2144BDB600432DC2 /* Response */, @@ -927,7 +930,6 @@ D6C7D27B22B6EBE200071952 /* Attachments */, D641C78B213DD92F004B4513 /* Profile Header */, D641C78C213DD937004B4513 /* Notifications */, - D6C693CB2161256B007D6A6D /* Silent Action Permissions */, ); path = Views; sourceTree = ""; @@ -1365,6 +1367,7 @@ D6109A0921458C4A00432DC2 /* Empty.swift in Sources */, D6285B4F21EA695800FE4B39 /* StatusContentType.swift in Sources */, D61099DF2144C11400432DC2 /* MastodonError.swift in Sources */, + D6DD353B22F25D2E00A9563A /* TimelineSegment.swift in Sources */, D61099D62144B4B200432DC2 /* FormAttachment.swift in Sources */, D6109A072145756700432DC2 /* LoginSettings.swift in Sources */, D61099ED2145664800432DC2 /* Filter.swift in Sources */, diff --git a/Tusker/Screens/Timeline/TimelineTableViewController.swift b/Tusker/Screens/Timeline/TimelineTableViewController.swift index 5be78ae1..92100d72 100644 --- a/Tusker/Screens/Timeline/TimelineTableViewController.swift +++ b/Tusker/Screens/Timeline/TimelineTableViewController.swift @@ -26,7 +26,7 @@ class TimelineTableViewController: EnhancedTableViewController { var timeline: Timeline! - var statusIDs: [String] = [] { + var timelineSegments: [TimelineSegment] = [] { didSet { DispatchQueue.main.async { self.tableView.reloadData() @@ -53,6 +53,10 @@ class TimelineTableViewController: EnhancedTableViewController { fatalError("init(coder:) has not been implemented") } + func statusID(for indexPath: IndexPath) -> String { + return timelineSegments[indexPath.section][indexPath.row] + } + override func viewDidLoad() { super.viewDidLoad() @@ -67,8 +71,8 @@ class TimelineTableViewController: EnhancedTableViewController { let request = MastodonController.client.getStatuses(timeline: timeline) MastodonController.client.run(request) { response in guard case let .success(statuses, pagination) = response else { fatalError() } - self.statusIDs = statuses.map { $0.id } MastodonCache.addAll(statuses: statuses) + self.timelineSegments.insert(TimelineSegment(objects: statuses), at: 0) self.newer = pagination?.newer self.older = pagination?.older } @@ -87,21 +91,18 @@ class TimelineTableViewController: EnhancedTableViewController { // MARK: - Table view data source override func numberOfSections(in tableView: UITableView) -> Int { - return 1 + return timelineSegments.count } override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { - return statusIDs.count + return timelineSegments[section].count } override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { guard let cell = tableView.dequeueReusableCell(withIdentifier: "statusCell", for: indexPath) as? StatusTableViewCell else { fatalError() } - let statusID = statusIDs[indexPath.row] - - cell.updateUI(for: statusID) - + cell.updateUI(for: statusID(for: indexPath)) cell.delegate = self return cell @@ -110,7 +111,8 @@ class TimelineTableViewController: EnhancedTableViewController { // MARK: - Table view delegate override func tableView(_ tableView: UITableView, willDisplay cell: UITableViewCell, forRowAt indexPath: IndexPath) { - if indexPath.row == statusIDs.count - 1 { + if indexPath.section == timelineSegments.count - 1, + indexPath.row == timelineSegments[indexPath.section].count - 1 { guard let older = older else { return } let request = MastodonController.client.getStatuses(timeline: timeline, range: older) @@ -118,7 +120,7 @@ class TimelineTableViewController: EnhancedTableViewController { guard case let .success(newStatuses, pagination) = response else { fatalError() } self.older = pagination?.older MastodonCache.addAll(statuses: newStatuses) - self.statusIDs.append(contentsOf: newStatuses.map { $0.id }) + self.timelineSegments[self.timelineSegments.count - 1].append(objects: newStatuses) } } } @@ -143,7 +145,7 @@ class TimelineTableViewController: EnhancedTableViewController { guard case let .success(newStatuses, pagination) = response else { fatalError() } self.newer = pagination?.newer MastodonCache.addAll(statuses: newStatuses) - self.statusIDs.insert(contentsOf: newStatuses.map { $0.id }, at: 0) + self.timelineSegments[0].insertAtBeginning(objects: newStatuses) DispatchQueue.main.async { self.refreshControl?.endRefreshing() @@ -164,7 +166,7 @@ extension TimelineTableViewController: StatusTableViewCellDelegate {} extension TimelineTableViewController: UITableViewDataSourcePrefetching { func tableView(_ tableView: UITableView, prefetchRowsAt indexPaths: [IndexPath]) { for indexPath in indexPaths { - guard let status = MastodonCache.status(for: statusIDs[indexPath.row]) else { continue } + guard let status = MastodonCache.status(for: statusID(for: indexPath)) else { continue } ImageCache.avatars.get(status.account.avatar, completion: nil) for attachment in status.attachments { ImageCache.attachments.get(attachment.url, completion: nil) @@ -174,7 +176,7 @@ extension TimelineTableViewController: UITableViewDataSourcePrefetching { func tableView(_ tableView: UITableView, cancelPrefetchingForRowsAt indexPaths: [IndexPath]) { for indexPath in indexPaths { - guard let status = MastodonCache.status(for: statusIDs[indexPath.row]) else { continue } + guard let status = MastodonCache.status(for: statusID(for: indexPath)) else { continue } ImageCache.avatars.cancel(status.account.avatar) for attachment in status.attachments { ImageCache.attachments.cancel(attachment.url)