Represent timelines internally as segments

Primarily in preparation for timeline position persistence and split
timelines
This commit is contained in:
Shadowfacts 2019-07-31 17:33:48 -06:00
parent 83d5731f3a
commit d9b21a0196
Signed by: shadowfacts
GPG Key ID: 94A5AB95422746E5
6 changed files with 83 additions and 34 deletions

View File

@ -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)
}
}
}

View File

@ -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]
}
}
}

View File

@ -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) }

View File

@ -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<Type: Identifiable> {
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 {}

View File

@ -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 = "<group>"; };
D6D4DDF1212518A200E1C4BB /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
D6D58DF822074B74009C8DD9 /* LinkLabel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LinkLabel.swift; sourceTree = "<group>"; };
D6DD353A22F25D2E00A9563A /* TimelineSegment.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = TimelineSegment.swift; path = ../../../../../../System/Volumes/Data/Users/shadowfacts/Dev/iOS/Tusker/Pachyderm/TimelineSegment.swift; sourceTree = "<group>"; };
D6E0DC8D216EDF1E00369478 /* Previewing.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Previewing.swift; sourceTree = "<group>"; };
D6E6F26221603F8B006A8599 /* CharacterCounter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CharacterCounter.swift; sourceTree = "<group>"; };
D6E6F26421604242006A8599 /* CharacterCounterTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CharacterCounterTests.swift; sourceTree = "<group>"; };
@ -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 = "<group>";
@ -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 */,

View File

@ -26,7 +26,7 @@ class TimelineTableViewController: EnhancedTableViewController {
var timeline: Timeline!
var statusIDs: [String] = [] {
var timelineSegments: [TimelineSegment<Status>] = [] {
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)