mirror of
https://github.com/Dimillian/IceCubesApp.git
synced 2024-11-26 02:01:02 +00:00
Cache and restore position on all timelines
This commit is contained in:
parent
6854df4b89
commit
2e23b08b88
4 changed files with 83 additions and 52 deletions
|
@ -4,7 +4,7 @@ public enum Timelines: Endpoint {
|
|||
case pub(sinceId: String?, maxId: String?, minId: String?, local: Bool)
|
||||
case home(sinceId: String?, maxId: String?, minId: String?)
|
||||
case list(listId: String, sinceId: String?, maxId: String?, minId: String?)
|
||||
case hashtag(tag: String, additional: [String]?, maxId: String?)
|
||||
case hashtag(tag: String, additional: [String]?, maxId: String?, minId: String?)
|
||||
|
||||
public func path() -> String {
|
||||
switch self {
|
||||
|
@ -14,7 +14,7 @@ public enum Timelines: Endpoint {
|
|||
"timelines/home"
|
||||
case let .list(listId, _, _, _):
|
||||
"timelines/list/\(listId)"
|
||||
case let .hashtag(tag, _, _):
|
||||
case let .hashtag(tag, _, _, _):
|
||||
"timelines/tag/\(tag)"
|
||||
}
|
||||
}
|
||||
|
@ -29,8 +29,8 @@ public enum Timelines: Endpoint {
|
|||
return makePaginationParam(sinceId: sinceId, maxId: maxId, mindId: mindId)
|
||||
case let .list(_, sinceId, maxId, mindId):
|
||||
return makePaginationParam(sinceId: sinceId, maxId: maxId, mindId: mindId)
|
||||
case let .hashtag(_, additional, maxId):
|
||||
var params = makePaginationParam(sinceId: nil, maxId: maxId, mindId: nil) ?? []
|
||||
case let .hashtag(_, additional, maxId, minId):
|
||||
var params = makePaginationParam(sinceId: nil, maxId: maxId, mindId: minId) ?? []
|
||||
params.append(contentsOf: (additional ?? [])
|
||||
.map { URLQueryItem(name: "any[]", value: $0) })
|
||||
return params
|
||||
|
|
|
@ -162,15 +162,15 @@ public enum TimelineFilter: Hashable, Equatable, Identifiable {
|
|||
if let accountId {
|
||||
return Accounts.statuses(id: accountId, sinceId: nil, tag: tag, onlyMedia: nil, excludeReplies: nil, pinned: nil)
|
||||
} else {
|
||||
return Timelines.hashtag(tag: tag, additional: nil, maxId: maxId)
|
||||
return Timelines.hashtag(tag: tag, additional: nil, maxId: maxId, minId: minId)
|
||||
}
|
||||
case let .tagGroup(_, tags):
|
||||
var tags = tags
|
||||
if !tags.isEmpty {
|
||||
let tag = tags.removeFirst()
|
||||
return Timelines.hashtag(tag: tag, additional: tags, maxId: maxId)
|
||||
return Timelines.hashtag(tag: tag, additional: tags, maxId: maxId, minId: minId)
|
||||
} else {
|
||||
return Timelines.hashtag(tag: "", additional: tags, maxId: maxId)
|
||||
return Timelines.hashtag(tag: "", additional: tags, maxId: maxId, minId: minId)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -48,7 +48,10 @@ import SwiftUI
|
|||
// Internal source of truth for a timeline.
|
||||
private(set) var datasource = TimelineDatasource()
|
||||
private let cache = TimelineCache()
|
||||
private var visibileStatusesIds = Set<String>()
|
||||
|
||||
@ObservationIgnored
|
||||
private var visibileStatuses: [Status] = []
|
||||
|
||||
private var canStreamEvents: Bool = true
|
||||
|
||||
var client: Client? {
|
||||
|
@ -98,9 +101,7 @@ import SwiftUI
|
|||
|
||||
private func handleLatestOrResume(_ oldValue: TimelineFilter) async {
|
||||
if timeline == .latest || timeline == .resume {
|
||||
if oldValue == .home {
|
||||
await clearHomeCache()
|
||||
}
|
||||
await clearCache(filter: oldValue)
|
||||
if timeline == .resume, let marker = await fetchMarker() {
|
||||
self.marker = marker
|
||||
}
|
||||
|
@ -119,7 +120,7 @@ import SwiftUI
|
|||
pendingStatusesObserver.pendingStatuses.insert(event.status.id, at: 0)
|
||||
let newStatus = event.status
|
||||
await datasource.insert(newStatus, at: 0)
|
||||
await cacheHome()
|
||||
await cache()
|
||||
StatusDataControllerProvider.shared.updateDataControllers(for: [event.status], client: client)
|
||||
let statuses = await datasource.get()
|
||||
withAnimation {
|
||||
|
@ -127,7 +128,7 @@ import SwiftUI
|
|||
}
|
||||
} else if let event = event as? StreamEventDelete {
|
||||
await datasource.remove(event.status)
|
||||
await cacheHome()
|
||||
await cache()
|
||||
let statuses = await datasource.get()
|
||||
withAnimation {
|
||||
statusesState = .display(statuses: statuses, nextPageState: .hasNextPage)
|
||||
|
@ -137,7 +138,7 @@ import SwiftUI
|
|||
StatusDataControllerProvider.shared.updateDataControllers(for: [event.status], client: client)
|
||||
await datasource.replace(event.status, at: originalIndex)
|
||||
let statuses = await datasource.get()
|
||||
await cacheHome()
|
||||
await cache()
|
||||
statusesState = .display(statuses: statuses, nextPageState: .hasNextPage)
|
||||
}
|
||||
}
|
||||
|
@ -147,23 +148,23 @@ import SwiftUI
|
|||
// MARK: - Cache
|
||||
|
||||
extension TimelineViewModel {
|
||||
private func cacheHome() async {
|
||||
if let client, timeline == .home {
|
||||
await cache.set(statuses: datasource.get(), client: client.id)
|
||||
private func cache() async {
|
||||
if let client, timeline.supportNewestPagination {
|
||||
await cache.set(statuses: datasource.get(), client: client.id, filter: timeline.id)
|
||||
}
|
||||
}
|
||||
|
||||
private func getCachedStatuses() async -> [Status]? {
|
||||
if let client {
|
||||
return await cache.getStatuses(for: client.id)
|
||||
return await cache.getStatuses(for: client.id, filter: timeline.id)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
private func clearHomeCache() async {
|
||||
private func clearCache(filter: TimelineFilter) async {
|
||||
if let client {
|
||||
await cache.clearCache(for: client.id)
|
||||
await cache.setLatestSeenStatuses(ids: [], for: client)
|
||||
await cache.clearCache(for: client.id, filter: filter.id)
|
||||
await cache.setLatestSeenStatuses([], for: client, filter: filter.id)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -201,7 +202,7 @@ extension TimelineViewModel: StatusesFetcher {
|
|||
StatusDataControllerProvider.shared.updateDataControllers(for: statuses, client: client)
|
||||
|
||||
await datasource.set(statuses)
|
||||
await cacheHome()
|
||||
await cache()
|
||||
marker = nil
|
||||
|
||||
withAnimation {
|
||||
|
@ -237,22 +238,23 @@ extension TimelineViewModel: StatusesFetcher {
|
|||
|
||||
// If we get statuses from the cache for the home timeline, we displays those.
|
||||
// Else we fetch top most page from the API.
|
||||
if let cachedStatuses = await getCachedStatuses(),
|
||||
if timeline.supportNewestPagination,
|
||||
let cachedStatuses = await getCachedStatuses(),
|
||||
!cachedStatuses.isEmpty,
|
||||
timeline == .home, !UserPreferences.shared.fastRefreshEnabled
|
||||
!UserPreferences.shared.fastRefreshEnabled
|
||||
{
|
||||
await datasource.set(cachedStatuses)
|
||||
if let latestSeenId = await cache.getLatestSeenStatus(for: client)?.last,
|
||||
let statuses = await datasource.get()
|
||||
if let latestSeenId = await cache.getLatestSeenStatus(for: client, filter: timeline.id)?.first,
|
||||
let index = await datasource.indexOf(statusId: latestSeenId),
|
||||
index > 0
|
||||
{
|
||||
// Restore cache and scroll to latest seen status.
|
||||
statusesState = await .display(statuses: datasource.get(), nextPageState: .hasNextPage)
|
||||
statusesState = .display(statuses: statuses, nextPageState: .hasNextPage)
|
||||
scrollToIndexAnimated = false
|
||||
scrollToIndex = index + 1
|
||||
} else {
|
||||
// Restore cache and scroll to top.
|
||||
let statuses = await datasource.get()
|
||||
withAnimation {
|
||||
statusesState = .display(statuses: statuses, nextPageState: .hasNextPage)
|
||||
}
|
||||
|
@ -270,7 +272,7 @@ extension TimelineViewModel: StatusesFetcher {
|
|||
|
||||
await datasource.set(statuses)
|
||||
statuses = await datasource.get()
|
||||
await cacheHome()
|
||||
await cache()
|
||||
|
||||
withAnimation {
|
||||
statusesState = .display(statuses: statuses, nextPageState: statuses.count < 20 ? .none : .hasNextPage)
|
||||
|
@ -318,20 +320,20 @@ extension TimelineViewModel: StatusesFetcher {
|
|||
}
|
||||
|
||||
// Keep track of the top most status, so we can scroll back to it after view update.
|
||||
let topStatusId = await datasource.get().first?.id
|
||||
let topStatus = await datasource.get().first
|
||||
|
||||
// Insert new statuses in internal datasource.
|
||||
await datasource.insert(contentOf: newStatuses, at: 0)
|
||||
|
||||
// Cache statuses for home timeline.
|
||||
await cacheHome()
|
||||
// Cache statuses for timeline.
|
||||
await cache()
|
||||
|
||||
// Append new statuses in the timeline indicator.
|
||||
pendingStatusesObserver.pendingStatuses.insert(contentsOf: newStatuses.map(\.id), at: 0)
|
||||
|
||||
// High chance the user is scrolled to the top.
|
||||
// We need to update the statuses state, and then scroll to the previous top most status.
|
||||
if let topStatusId, visibileStatusesIds.contains(topStatusId), scrollToTopVisible {
|
||||
if let topStatus, visibileStatuses.contains(where: { $0.id == topStatus.id}), scrollToTopVisible {
|
||||
pendingStatusesObserver.disableUpdate = true
|
||||
let statuses = await datasource.get()
|
||||
statusesState = .display(statuses: statuses,
|
||||
|
@ -417,17 +419,17 @@ extension TimelineViewModel: StatusesFetcher {
|
|||
|
||||
func statusDidAppear(status: Status) {
|
||||
pendingStatusesObserver.removeStatus(status: status)
|
||||
visibileStatusesIds.insert(status.id)
|
||||
visibileStatuses.insert(status, at: 0)
|
||||
|
||||
if let client, timeline == .home {
|
||||
if let client, timeline.supportNewestPagination {
|
||||
Task {
|
||||
await cache.setLatestSeenStatuses(ids: visibileStatusesIds.map { $0 }, for: client)
|
||||
await cache.setLatestSeenStatuses(visibileStatuses, for: client, filter: timeline.id)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func statusDidDisappear(status: Status) {
|
||||
visibileStatusesIds.remove(status.id)
|
||||
visibileStatuses.removeAll(where: { $0.id == status.id })
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -448,7 +450,7 @@ extension TimelineViewModel {
|
|||
func saveMarker() {
|
||||
guard timeline == .home, let client else { return }
|
||||
Task {
|
||||
guard let id = await cache.getLatestSeenStatus(for: client)?.first else { return }
|
||||
guard let id = await cache.getLatestSeenStatus(for: client, filter: timeline.id)?.first else { return }
|
||||
do {
|
||||
let _: Marker = try await client.post(endpoint: Markers.markHome(lastReadId: id))
|
||||
} catch { }
|
||||
|
|
|
@ -4,8 +4,12 @@ import Network
|
|||
import SwiftUI
|
||||
|
||||
public actor TimelineCache {
|
||||
private func storageFor(_ client: String) -> SQLiteStorageEngine {
|
||||
SQLiteStorageEngine.default(appendingPath: client)
|
||||
private func storageFor(_ client: String, _ filter: String) -> SQLiteStorageEngine {
|
||||
if filter == "Home" {
|
||||
SQLiteStorageEngine.default(appendingPath: "\(client)")
|
||||
} else {
|
||||
SQLiteStorageEngine.default(appendingPath: "\(client)/\(filter)")
|
||||
}
|
||||
}
|
||||
|
||||
private let decoder = JSONDecoder()
|
||||
|
@ -14,21 +18,38 @@ public actor TimelineCache {
|
|||
public init() {}
|
||||
|
||||
public func cachedPostsCount(for client: String) async -> Int {
|
||||
await storageFor(client).allKeys().count
|
||||
do {
|
||||
let directory = FileManager.Directory.defaultStorageDirectory(appendingPath: client).url
|
||||
let content = try FileManager.default.contentsOfDirectory(at: directory, includingPropertiesForKeys: nil)
|
||||
var total: Int = await storageFor(client, "Home").allKeys().count
|
||||
for storage in content {
|
||||
if !storage.lastPathComponent.hasSuffix("sqlite3") {
|
||||
total += await storageFor(client, storage.lastPathComponent).allKeys().count
|
||||
}
|
||||
}
|
||||
return total
|
||||
} catch {
|
||||
return 0
|
||||
}
|
||||
}
|
||||
|
||||
public func clearCache(for client: String) async {
|
||||
let engine = storageFor(client)
|
||||
let directory = FileManager.Directory.defaultStorageDirectory(appendingPath: client)
|
||||
try? FileManager.default.removeItem(at: directory.url)
|
||||
}
|
||||
|
||||
public func clearCache(for client: String, filter: String) async {
|
||||
let engine = storageFor(client, filter)
|
||||
do {
|
||||
try await engine.removeAllData()
|
||||
} catch {}
|
||||
}
|
||||
|
||||
func set(statuses: [Status], client: String) async {
|
||||
func set(statuses: [Status], client: String, filter: String) async {
|
||||
guard !statuses.isEmpty else { return }
|
||||
let statuses = statuses.prefix(upTo: min(600, statuses.count - 1)).map { $0 }
|
||||
do {
|
||||
let engine = storageFor(client)
|
||||
let engine = storageFor(client, filter)
|
||||
try await engine.removeAllData()
|
||||
let itemKeys = statuses.map { CacheKey($0[keyPath: \.id]) }
|
||||
let dataAndKeys = try zip(itemKeys, statuses)
|
||||
|
@ -37,8 +58,8 @@ public actor TimelineCache {
|
|||
} catch {}
|
||||
}
|
||||
|
||||
func getStatuses(for client: String) async -> [Status]? {
|
||||
let engine = storageFor(client)
|
||||
func getStatuses(for client: String, filter: String) async -> [Status]? {
|
||||
let engine = storageFor(client, filter)
|
||||
do {
|
||||
return try await engine
|
||||
.readAllData()
|
||||
|
@ -49,12 +70,20 @@ public actor TimelineCache {
|
|||
}
|
||||
}
|
||||
|
||||
func setLatestSeenStatuses(ids: [String], for client: Client) {
|
||||
UserDefaults.standard.set(ids, forKey: "timeline-last-seen-\(client.id)")
|
||||
func setLatestSeenStatuses(_ statuses: [Status], for client: Client, filter: String) {
|
||||
if filter == "Home" {
|
||||
UserDefaults.standard.set(statuses.map{ $0.id }, forKey: "timeline-last-seen-\(client.id)")
|
||||
} else {
|
||||
UserDefaults.standard.set(statuses.map{ $0.id }, forKey: "timeline-last-seen-\(client.id)-\(filter)")
|
||||
}
|
||||
}
|
||||
|
||||
func getLatestSeenStatus(for client: Client) -> [String]? {
|
||||
func getLatestSeenStatus(for client: Client, filter: String) -> [String]? {
|
||||
if filter == "Home" {
|
||||
UserDefaults.standard.array(forKey: "timeline-last-seen-\(client.id)") as? [String]
|
||||
} else {
|
||||
UserDefaults.standard.array(forKey: "timeline-last-seen-\(client.id)-\(filter)") as? [String]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
Loading…
Reference in a new issue