forked from shadowfacts/Tusker
Represent timelines internally as segments
Primarily in preparation for timeline position persistence and split timelines
This commit is contained in:
parent
83d5731f3a
commit
d9b21a0196
|
@ -40,25 +40,11 @@ extension Request {
|
||||||
}
|
}
|
||||||
set {
|
set {
|
||||||
let rangeParams = newValue.queryParameters
|
let rangeParams = newValue.queryParameters
|
||||||
if let max = rangeParams.first(where: { $0.name == "max_id" }) {
|
for param in rangeParams {
|
||||||
if let i = queryParameters.firstIndex(where: { $0.name == "max_id" }) {
|
if let i = queryParameters.firstIndex(where: { $0.name == param.name }) {
|
||||||
queryParameters[i] = max
|
queryParameters[i] = param
|
||||||
} else {
|
} else {
|
||||||
queryParameters.append(max)
|
queryParameters.append(param)
|
||||||
}
|
|
||||||
}
|
|
||||||
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)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -25,7 +25,7 @@ extension RequestRange {
|
||||||
case let .before(id, count):
|
case let .before(id, count):
|
||||||
return ["max_id" => id, "count" => count]
|
return ["max_id" => id, "count" => count]
|
||||||
case let .after(id, count):
|
case let .after(id, count):
|
||||||
return ["since_id" => id, "count" => count]
|
return ["min_id" => id, "count" => count]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -52,10 +52,11 @@ extension Pagination {
|
||||||
let components = URLComponents(string: validURL),
|
let components = URLComponents(string: validURL),
|
||||||
let queryItems = components.queryItems else { return nil }
|
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 since = queryItems.first { $0.name == "since_id" }?.value
|
||||||
let max = queryItems.first { $0.name == "max_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) }
|
let limit = queryItems.first { $0.name == "limit" }.flatMap { $0.value }.flatMap { Int($0) }
|
||||||
|
|
||||||
|
|
|
@ -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 {}
|
|
@ -168,6 +168,7 @@
|
||||||
D6D4DDE5212518A200E1C4BB /* TuskerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6D4DDE4212518A200E1C4BB /* TuskerTests.swift */; };
|
D6D4DDE5212518A200E1C4BB /* TuskerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6D4DDE4212518A200E1C4BB /* TuskerTests.swift */; };
|
||||||
D6D4DDF0212518A200E1C4BB /* TuskerUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6D4DDEF212518A200E1C4BB /* TuskerUITests.swift */; };
|
D6D4DDF0212518A200E1C4BB /* TuskerUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6D4DDEF212518A200E1C4BB /* TuskerUITests.swift */; };
|
||||||
D6D58DF922074B74009C8DD9 /* LinkLabel.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6D58DF822074B74009C8DD9 /* LinkLabel.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 */; };
|
D6E0DC8E216EDF1E00369478 /* Previewing.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6E0DC8D216EDF1E00369478 /* Previewing.swift */; };
|
||||||
D6E6F26321603F8B006A8599 /* CharacterCounter.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6E6F26221603F8B006A8599 /* CharacterCounter.swift */; };
|
D6E6F26321603F8B006A8599 /* CharacterCounter.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6E6F26221603F8B006A8599 /* CharacterCounter.swift */; };
|
||||||
D6E6F26521604242006A8599 /* CharacterCounterTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6E6F26421604242006A8599 /* CharacterCounterTests.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>"; };
|
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>"; };
|
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>"; };
|
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>"; };
|
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>"; };
|
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>"; };
|
D6E6F26421604242006A8599 /* CharacterCounterTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CharacterCounterTests.swift; sourceTree = "<group>"; };
|
||||||
|
@ -542,6 +544,7 @@
|
||||||
D61099C82144B13C00432DC2 /* Client.swift */,
|
D61099C82144B13C00432DC2 /* Client.swift */,
|
||||||
D6109A0A2145953C00432DC2 /* ClientModel.swift */,
|
D6109A0A2145953C00432DC2 /* ClientModel.swift */,
|
||||||
D6E6F26221603F8B006A8599 /* CharacterCounter.swift */,
|
D6E6F26221603F8B006A8599 /* CharacterCounter.swift */,
|
||||||
|
D6DD353A22F25D2E00A9563A /* TimelineSegment.swift */,
|
||||||
D61099D72144B74500432DC2 /* Extensions */,
|
D61099D72144B74500432DC2 /* Extensions */,
|
||||||
D61099CC2144B2C300432DC2 /* Request */,
|
D61099CC2144B2C300432DC2 /* Request */,
|
||||||
D61099DA2144BDB600432DC2 /* Response */,
|
D61099DA2144BDB600432DC2 /* Response */,
|
||||||
|
@ -927,7 +930,6 @@
|
||||||
D6C7D27B22B6EBE200071952 /* Attachments */,
|
D6C7D27B22B6EBE200071952 /* Attachments */,
|
||||||
D641C78B213DD92F004B4513 /* Profile Header */,
|
D641C78B213DD92F004B4513 /* Profile Header */,
|
||||||
D641C78C213DD937004B4513 /* Notifications */,
|
D641C78C213DD937004B4513 /* Notifications */,
|
||||||
D6C693CB2161256B007D6A6D /* Silent Action Permissions */,
|
|
||||||
);
|
);
|
||||||
path = Views;
|
path = Views;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
|
@ -1365,6 +1367,7 @@
|
||||||
D6109A0921458C4A00432DC2 /* Empty.swift in Sources */,
|
D6109A0921458C4A00432DC2 /* Empty.swift in Sources */,
|
||||||
D6285B4F21EA695800FE4B39 /* StatusContentType.swift in Sources */,
|
D6285B4F21EA695800FE4B39 /* StatusContentType.swift in Sources */,
|
||||||
D61099DF2144C11400432DC2 /* MastodonError.swift in Sources */,
|
D61099DF2144C11400432DC2 /* MastodonError.swift in Sources */,
|
||||||
|
D6DD353B22F25D2E00A9563A /* TimelineSegment.swift in Sources */,
|
||||||
D61099D62144B4B200432DC2 /* FormAttachment.swift in Sources */,
|
D61099D62144B4B200432DC2 /* FormAttachment.swift in Sources */,
|
||||||
D6109A072145756700432DC2 /* LoginSettings.swift in Sources */,
|
D6109A072145756700432DC2 /* LoginSettings.swift in Sources */,
|
||||||
D61099ED2145664800432DC2 /* Filter.swift in Sources */,
|
D61099ED2145664800432DC2 /* Filter.swift in Sources */,
|
||||||
|
|
|
@ -26,7 +26,7 @@ class TimelineTableViewController: EnhancedTableViewController {
|
||||||
|
|
||||||
var timeline: Timeline!
|
var timeline: Timeline!
|
||||||
|
|
||||||
var statusIDs: [String] = [] {
|
var timelineSegments: [TimelineSegment<Status>] = [] {
|
||||||
didSet {
|
didSet {
|
||||||
DispatchQueue.main.async {
|
DispatchQueue.main.async {
|
||||||
self.tableView.reloadData()
|
self.tableView.reloadData()
|
||||||
|
@ -53,6 +53,10 @@ class TimelineTableViewController: EnhancedTableViewController {
|
||||||
fatalError("init(coder:) has not been implemented")
|
fatalError("init(coder:) has not been implemented")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func statusID(for indexPath: IndexPath) -> String {
|
||||||
|
return timelineSegments[indexPath.section][indexPath.row]
|
||||||
|
}
|
||||||
|
|
||||||
override func viewDidLoad() {
|
override func viewDidLoad() {
|
||||||
super.viewDidLoad()
|
super.viewDidLoad()
|
||||||
|
|
||||||
|
@ -67,8 +71,8 @@ class TimelineTableViewController: EnhancedTableViewController {
|
||||||
let request = MastodonController.client.getStatuses(timeline: timeline)
|
let request = MastodonController.client.getStatuses(timeline: timeline)
|
||||||
MastodonController.client.run(request) { response in
|
MastodonController.client.run(request) { response in
|
||||||
guard case let .success(statuses, pagination) = response else { fatalError() }
|
guard case let .success(statuses, pagination) = response else { fatalError() }
|
||||||
self.statusIDs = statuses.map { $0.id }
|
|
||||||
MastodonCache.addAll(statuses: statuses)
|
MastodonCache.addAll(statuses: statuses)
|
||||||
|
self.timelineSegments.insert(TimelineSegment(objects: statuses), at: 0)
|
||||||
self.newer = pagination?.newer
|
self.newer = pagination?.newer
|
||||||
self.older = pagination?.older
|
self.older = pagination?.older
|
||||||
}
|
}
|
||||||
|
@ -87,21 +91,18 @@ class TimelineTableViewController: EnhancedTableViewController {
|
||||||
// MARK: - Table view data source
|
// MARK: - Table view data source
|
||||||
|
|
||||||
override func numberOfSections(in tableView: UITableView) -> Int {
|
override func numberOfSections(in tableView: UITableView) -> Int {
|
||||||
return 1
|
return timelineSegments.count
|
||||||
}
|
}
|
||||||
|
|
||||||
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
|
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 {
|
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
|
||||||
guard let cell = tableView.dequeueReusableCell(withIdentifier: "statusCell", for: indexPath) as? StatusTableViewCell else { fatalError() }
|
guard let cell = tableView.dequeueReusableCell(withIdentifier: "statusCell", for: indexPath) as? StatusTableViewCell else { fatalError() }
|
||||||
|
|
||||||
let statusID = statusIDs[indexPath.row]
|
cell.updateUI(for: statusID(for: indexPath))
|
||||||
|
|
||||||
cell.updateUI(for: statusID)
|
|
||||||
|
|
||||||
cell.delegate = self
|
cell.delegate = self
|
||||||
|
|
||||||
return cell
|
return cell
|
||||||
|
@ -110,7 +111,8 @@ class TimelineTableViewController: EnhancedTableViewController {
|
||||||
// MARK: - Table view delegate
|
// MARK: - Table view delegate
|
||||||
|
|
||||||
override func tableView(_ tableView: UITableView, willDisplay cell: UITableViewCell, forRowAt indexPath: IndexPath) {
|
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 }
|
guard let older = older else { return }
|
||||||
|
|
||||||
let request = MastodonController.client.getStatuses(timeline: timeline, range: older)
|
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() }
|
guard case let .success(newStatuses, pagination) = response else { fatalError() }
|
||||||
self.older = pagination?.older
|
self.older = pagination?.older
|
||||||
MastodonCache.addAll(statuses: newStatuses)
|
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() }
|
guard case let .success(newStatuses, pagination) = response else { fatalError() }
|
||||||
self.newer = pagination?.newer
|
self.newer = pagination?.newer
|
||||||
MastodonCache.addAll(statuses: newStatuses)
|
MastodonCache.addAll(statuses: newStatuses)
|
||||||
self.statusIDs.insert(contentsOf: newStatuses.map { $0.id }, at: 0)
|
self.timelineSegments[0].insertAtBeginning(objects: newStatuses)
|
||||||
DispatchQueue.main.async {
|
DispatchQueue.main.async {
|
||||||
self.refreshControl?.endRefreshing()
|
self.refreshControl?.endRefreshing()
|
||||||
|
|
||||||
|
@ -164,7 +166,7 @@ extension TimelineTableViewController: StatusTableViewCellDelegate {}
|
||||||
extension TimelineTableViewController: UITableViewDataSourcePrefetching {
|
extension TimelineTableViewController: UITableViewDataSourcePrefetching {
|
||||||
func tableView(_ tableView: UITableView, prefetchRowsAt indexPaths: [IndexPath]) {
|
func tableView(_ tableView: UITableView, prefetchRowsAt indexPaths: [IndexPath]) {
|
||||||
for indexPath in indexPaths {
|
for indexPath in indexPaths {
|
||||||
guard let status = 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)
|
ImageCache.avatars.get(status.account.avatar, completion: nil)
|
||||||
for attachment in status.attachments {
|
for attachment in status.attachments {
|
||||||
ImageCache.attachments.get(attachment.url, completion: nil)
|
ImageCache.attachments.get(attachment.url, completion: nil)
|
||||||
|
@ -174,7 +176,7 @@ extension TimelineTableViewController: UITableViewDataSourcePrefetching {
|
||||||
|
|
||||||
func tableView(_ tableView: UITableView, cancelPrefetchingForRowsAt indexPaths: [IndexPath]) {
|
func tableView(_ tableView: UITableView, cancelPrefetchingForRowsAt indexPaths: [IndexPath]) {
|
||||||
for indexPath in indexPaths {
|
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)
|
ImageCache.avatars.cancel(status.account.avatar)
|
||||||
for attachment in status.attachments {
|
for attachment in status.attachments {
|
||||||
ImageCache.attachments.cancel(attachment.url)
|
ImageCache.attachments.cancel(attachment.url)
|
||||||
|
|
Loading…
Reference in New Issue