mirror of
https://github.com/Dimillian/IceCubesApp.git
synced 2025-04-27 02:14:45 +00:00
Refactor Timeline position management + add thin cache layer + fix crashes
This commit is contained in:
parent
9bf40b262f
commit
cdf45fa58c
7 changed files with 213 additions and 130 deletions
|
@ -2,7 +2,7 @@ import Foundation
|
||||||
import Models
|
import Models
|
||||||
import SwiftUI
|
import SwiftUI
|
||||||
|
|
||||||
public class Client: ObservableObject, Equatable, Identifiable {
|
public class Client: ObservableObject, Equatable, Identifiable, Hashable {
|
||||||
public static func == (lhs: Client, rhs: Client) -> Bool {
|
public static func == (lhs: Client, rhs: Client) -> Bool {
|
||||||
lhs.isAuth == rhs.isAuth &&
|
lhs.isAuth == rhs.isAuth &&
|
||||||
lhs.server == rhs.server &&
|
lhs.server == rhs.server &&
|
||||||
|
@ -21,6 +21,10 @@ public class Client: ObservableObject, Equatable, Identifiable {
|
||||||
public var id: String {
|
public var id: String {
|
||||||
"\(isAuth)\(server)\(oauthToken?.accessToken ?? "")"
|
"\(isAuth)\(server)\(oauthToken?.accessToken ?? "")"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public func hash(into hasher: inout Hasher) {
|
||||||
|
hasher.combine(id)
|
||||||
|
}
|
||||||
|
|
||||||
public var server: String
|
public var server: String
|
||||||
public let version: Version
|
public let version: Version
|
||||||
|
|
|
@ -23,6 +23,7 @@ public struct StatusesListView<Fetcher>: View where Fetcher: StatusesFetcher {
|
||||||
StatusRowView(viewModel: .init(status: status, isCompact: false))
|
StatusRowView(viewModel: .init(status: status, isCompact: false))
|
||||||
.padding(.horizontal, isEmbdedInList ? 0 : .layoutPadding)
|
.padding(.horizontal, isEmbdedInList ? 0 : .layoutPadding)
|
||||||
.redacted(reason: .placeholder)
|
.redacted(reason: .placeholder)
|
||||||
|
.id(UUID())
|
||||||
.listRowBackground(theme.primaryBackgroundColor)
|
.listRowBackground(theme.primaryBackgroundColor)
|
||||||
.listRowInsets(.init(top: 12,
|
.listRowInsets(.init(top: 12,
|
||||||
leading: .layoutPadding,
|
leading: .layoutPadding,
|
||||||
|
|
|
@ -132,7 +132,7 @@ public struct StatusPollView: View {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.frame(height: .pollBarHeight)
|
.frame(height: .pollBarHeight)
|
||||||
.buttonStyle(.borderless)
|
|
||||||
}
|
}
|
||||||
|
.buttonStyle(.borderless)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -28,17 +28,11 @@ class PendingStatusesObserver: ObservableObject {
|
||||||
|
|
||||||
struct PendingStatusesObserverView: View {
|
struct PendingStatusesObserverView: View {
|
||||||
@ObservedObject var observer: PendingStatusesObserver
|
@ObservedObject var observer: PendingStatusesObserver
|
||||||
var proxy: ScrollViewProxy
|
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
if observer.pendingStatusesCount > 0 {
|
if observer.pendingStatusesCount > 0 {
|
||||||
HStack(spacing: 6) {
|
HStack(spacing: 6) {
|
||||||
Spacer()
|
Spacer()
|
||||||
Button {
|
Button { } label: {
|
||||||
withAnimation {
|
|
||||||
proxy.scrollTo(observer.pendingStatuses.last, anchor: .top)
|
|
||||||
}
|
|
||||||
} label: {
|
|
||||||
Text("\(observer.pendingStatusesCount)")
|
Text("\(observer.pendingStatusesCount)")
|
||||||
}
|
}
|
||||||
.buttonStyle(.bordered)
|
.buttonStyle(.bordered)
|
||||||
|
|
19
Packages/Timeline/Sources/Timeline/TimelineCache.swift
Normal file
19
Packages/Timeline/Sources/Timeline/TimelineCache.swift
Normal file
|
@ -0,0 +1,19 @@
|
||||||
|
import SwiftUI
|
||||||
|
import Models
|
||||||
|
import Network
|
||||||
|
|
||||||
|
actor TimelineCache {
|
||||||
|
static let shared: TimelineCache = .init()
|
||||||
|
|
||||||
|
private var memoryCache: [Client: [Status]] = [:]
|
||||||
|
|
||||||
|
private init() {}
|
||||||
|
|
||||||
|
func set(statuses: [Status], client: Client) {
|
||||||
|
memoryCache[client] = statuses.prefix(upTo: min(100, (statuses.count - 1))).map{ $0 }
|
||||||
|
}
|
||||||
|
|
||||||
|
func getStatuses(for client: Client) -> [Status]? {
|
||||||
|
memoryCache[client]
|
||||||
|
}
|
||||||
|
}
|
|
@ -48,18 +48,26 @@ public struct TimelineView: View {
|
||||||
StatusesListView(fetcher: viewModel)
|
StatusesListView(fetcher: viewModel)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.id(account.account?.id ?? client.id)
|
.id(client.id)
|
||||||
.environment(\.defaultMinListRowHeight, 1)
|
.environment(\.defaultMinListRowHeight, 1)
|
||||||
.listStyle(.plain)
|
.listStyle(.plain)
|
||||||
.scrollContentBackground(.hidden)
|
.scrollContentBackground(.hidden)
|
||||||
.background(theme.primaryBackgroundColor)
|
.background(theme.primaryBackgroundColor)
|
||||||
if viewModel.pendingStatusesEnabled {
|
if viewModel.pendingStatusesEnabled {
|
||||||
makePendingNewPostsView(proxy: proxy)
|
PendingStatusesObserverView(observer: viewModel.pendingStatusesObserver)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.onAppear {
|
.onChange(of: viewModel.scrollToStatus) { statusId in
|
||||||
viewModel.scrollProxy = proxy
|
if let statusId {
|
||||||
|
viewModel.scrollToStatus = nil
|
||||||
|
proxy.scrollTo(statusId, anchor: .top)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
.onChange(of: scrollToTopSignal, perform: { _ in
|
||||||
|
withAnimation {
|
||||||
|
proxy.scrollTo(Constants.scrollToTop, anchor: .top)
|
||||||
|
}
|
||||||
|
})
|
||||||
}
|
}
|
||||||
.navigationTitle(timeline.localizedTitle())
|
.navigationTitle(timeline.localizedTitle())
|
||||||
.navigationBarTitleDisplayMode(.inline)
|
.navigationBarTitleDisplayMode(.inline)
|
||||||
|
@ -83,11 +91,6 @@ public struct TimelineView: View {
|
||||||
viewModel.handleEvent(event: latestEvent, currentAccount: account)
|
viewModel.handleEvent(event: latestEvent, currentAccount: account)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.onChange(of: scrollToTopSignal, perform: { _ in
|
|
||||||
withAnimation {
|
|
||||||
viewModel.scrollProxy?.scrollTo(Constants.scrollToTop, anchor: .top)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.onChange(of: timeline) { newTimeline in
|
.onChange(of: timeline) { newTimeline in
|
||||||
switch newTimeline {
|
switch newTimeline {
|
||||||
case let .remoteLocal(server):
|
case let .remoteLocal(server):
|
||||||
|
@ -115,11 +118,6 @@ public struct TimelineView: View {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@ViewBuilder
|
|
||||||
private func makePendingNewPostsView(proxy: ScrollViewProxy) -> some View {
|
|
||||||
PendingStatusesObserverView(observer: viewModel.pendingStatusesObserver, proxy: proxy)
|
|
||||||
}
|
|
||||||
|
|
||||||
@ViewBuilder
|
@ViewBuilder
|
||||||
private var tagHeaderView: some View {
|
private var tagHeaderView: some View {
|
||||||
if let tag = viewModel.tag {
|
if let tag = viewModel.tag {
|
||||||
|
@ -158,11 +156,17 @@ public struct TimelineView: View {
|
||||||
}
|
}
|
||||||
|
|
||||||
private var scrollToTopView: some View {
|
private var scrollToTopView: some View {
|
||||||
HStack{ }
|
HStack{ EmptyView() }
|
||||||
.listRowBackground(theme.primaryBackgroundColor)
|
.listRowBackground(theme.primaryBackgroundColor)
|
||||||
.listRowSeparator(.hidden)
|
.listRowSeparator(.hidden)
|
||||||
.listRowInsets(.init())
|
.listRowInsets(.init())
|
||||||
.frame(height: .layoutPadding)
|
.frame(height: .layoutPadding)
|
||||||
.id(Constants.scrollToTop)
|
.id(Constants.scrollToTop)
|
||||||
|
.onAppear {
|
||||||
|
viewModel.scrollToTopVisible = true
|
||||||
|
}
|
||||||
|
.onDisappear {
|
||||||
|
viewModel.scrollToTopVisible = false
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,9 +3,10 @@ import Models
|
||||||
import Network
|
import Network
|
||||||
import Status
|
import Status
|
||||||
import SwiftUI
|
import SwiftUI
|
||||||
|
import Account
|
||||||
|
|
||||||
@MainActor
|
@MainActor
|
||||||
class TimelineViewModel: ObservableObject, StatusesFetcher {
|
class TimelineViewModel: ObservableObject {
|
||||||
var client: Client? {
|
var client: Client? {
|
||||||
didSet {
|
didSet {
|
||||||
if oldValue != client {
|
if oldValue != client {
|
||||||
|
@ -17,13 +18,15 @@ class TimelineViewModel: ObservableObject, StatusesFetcher {
|
||||||
// Internal source of truth for a timeline.
|
// Internal source of truth for a timeline.
|
||||||
private var statuses: [Status] = []
|
private var statuses: [Status] = []
|
||||||
private var visibileStatusesIds = Set<String>()
|
private var visibileStatusesIds = Set<String>()
|
||||||
|
var scrollToTopVisible: Bool = false
|
||||||
|
|
||||||
private var canStreamEvents: Bool = true
|
private var canStreamEvents: Bool = true
|
||||||
|
|
||||||
var scrollProxy: ScrollViewProxy?
|
let pendingStatusesObserver: PendingStatusesObserver = .init()
|
||||||
|
let cache: TimelineCache = .shared
|
||||||
var pendingStatusesObserver: PendingStatusesObserver = .init()
|
|
||||||
|
|
||||||
|
@Published var scrollToStatus: String?
|
||||||
|
|
||||||
@Published var statusesState: StatusesState = .loading
|
@Published var statusesState: StatusesState = .loading
|
||||||
@Published var timeline: TimelineFilter = .federated {
|
@Published var timeline: TimelineFilter = .federated {
|
||||||
didSet {
|
didSet {
|
||||||
|
@ -55,108 +58,6 @@ class TimelineViewModel: ObservableObject, StatusesFetcher {
|
||||||
client?.server ?? "Error"
|
client?.server ?? "Error"
|
||||||
}
|
}
|
||||||
|
|
||||||
func fetchStatuses() async {
|
|
||||||
guard let client else { return }
|
|
||||||
do {
|
|
||||||
if statuses.isEmpty {
|
|
||||||
pendingStatusesObserver.pendingStatuses = []
|
|
||||||
statusesState = .loading
|
|
||||||
statuses = try await client.get(endpoint: timeline.endpoint(sinceId: nil,
|
|
||||||
maxId: nil,
|
|
||||||
minId: nil,
|
|
||||||
offset: statuses.count))
|
|
||||||
withAnimation {
|
|
||||||
statusesState = .display(statuses: statuses, nextPageState: statuses.count < 20 ? .none : .hasNextPage)
|
|
||||||
}
|
|
||||||
} else if let first = statuses.first {
|
|
||||||
canStreamEvents = false
|
|
||||||
var newStatuses: [Status] = await fetchNewPages(minId: first.id, maxPages: 10)
|
|
||||||
if !pendingStatusesEnabled {
|
|
||||||
statuses.insert(contentsOf: newStatuses, at: 0)
|
|
||||||
pendingStatusesObserver.pendingStatuses = []
|
|
||||||
withAnimation {
|
|
||||||
statusesState = .display(statuses: statuses, nextPageState: statuses.count < 20 ? .none : .hasNextPage)
|
|
||||||
canStreamEvents = true
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
newStatuses = newStatuses.filter { status in
|
|
||||||
!statuses.contains(where: { $0.id == status.id })
|
|
||||||
}
|
|
||||||
|
|
||||||
guard !newStatuses.isEmpty else {
|
|
||||||
canStreamEvents = true
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
pendingStatusesObserver.pendingStatuses.insert(contentsOf: newStatuses.map{ $0.id }, at: 0)
|
|
||||||
pendingStatusesObserver.feedbackGenerator.impactOccurred()
|
|
||||||
|
|
||||||
// High chance the user is scrolled to the top, this is a workaround to keep scroll position when prepending statuses.
|
|
||||||
if let firstStatusId = statuses.first?.id, visibileStatusesIds.contains(firstStatusId) {
|
|
||||||
statuses.insert(contentsOf: newStatuses, at: 0)
|
|
||||||
pendingStatusesObserver.disableUpdate = true
|
|
||||||
withAnimation {
|
|
||||||
statusesState = .display(statuses: statuses, nextPageState: statuses.count < 20 ? .none : .hasNextPage)
|
|
||||||
DispatchQueue.main.asyncAfter(deadline: .now() + 0.01) {
|
|
||||||
self.scrollProxy?.scrollTo(firstStatusId, anchor: .top)
|
|
||||||
self.pendingStatusesObserver.disableUpdate = false
|
|
||||||
self.canStreamEvents = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
statuses.insert(contentsOf: newStatuses, at: 0)
|
|
||||||
withAnimation {
|
|
||||||
statusesState = .display(statuses: statuses, nextPageState: statuses.count < 20 ? .none : .hasNextPage)
|
|
||||||
canStreamEvents = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch {
|
|
||||||
statusesState = .error(error: error)
|
|
||||||
canStreamEvents = true
|
|
||||||
print("timeline parse error: \(error)")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func fetchNewPages(minId: String, maxPages: Int) async -> [Status] {
|
|
||||||
guard let client else { return [] }
|
|
||||||
var pagesLoaded = 0
|
|
||||||
var allStatuses: [Status] = []
|
|
||||||
var latestMinId = minId
|
|
||||||
do {
|
|
||||||
while let newStatuses: [Status] = try await client.get(endpoint: timeline.endpoint(sinceId: nil,
|
|
||||||
maxId: nil,
|
|
||||||
minId: latestMinId,
|
|
||||||
offset: statuses.count)),
|
|
||||||
!newStatuses.isEmpty,
|
|
||||||
pagesLoaded < maxPages
|
|
||||||
{
|
|
||||||
pagesLoaded += 1
|
|
||||||
allStatuses.insert(contentsOf: newStatuses, at: 0)
|
|
||||||
latestMinId = newStatuses.first?.id ?? ""
|
|
||||||
}
|
|
||||||
} catch {
|
|
||||||
return allStatuses
|
|
||||||
}
|
|
||||||
return allStatuses
|
|
||||||
}
|
|
||||||
|
|
||||||
func fetchNextPage() async {
|
|
||||||
guard let client else { return }
|
|
||||||
do {
|
|
||||||
guard let lastId = statuses.last?.id else { return }
|
|
||||||
statusesState = .display(statuses: statuses, nextPageState: .loadingNextPage)
|
|
||||||
let newStatuses: [Status] = try await client.get(endpoint: timeline.endpoint(sinceId: nil,
|
|
||||||
maxId: lastId,
|
|
||||||
minId: nil,
|
|
||||||
offset: statuses.count))
|
|
||||||
statuses.append(contentsOf: newStatuses)
|
|
||||||
statusesState = .display(statuses: statuses, nextPageState: .hasNextPage)
|
|
||||||
} catch {
|
|
||||||
statusesState = .error(error: error)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func fetchTag(id: String) async {
|
func fetchTag(id: String) async {
|
||||||
guard let client else { return }
|
guard let client else { return }
|
||||||
|
@ -188,7 +89,167 @@ class TimelineViewModel: ObservableObject, StatusesFetcher {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Cache
|
||||||
|
extension TimelineViewModel {
|
||||||
|
private func cache(statuses: [Status]) async {
|
||||||
|
if let client {
|
||||||
|
await cache.set(statuses: statuses, client: client)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func getCachedStatuses() async -> [Status]? {
|
||||||
|
if let client {
|
||||||
|
return await cache.getStatuses(for: client)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - StatusesFetcher
|
||||||
|
extension TimelineViewModel: StatusesFetcher {
|
||||||
|
func fetchStatuses() async {
|
||||||
|
guard let client else { return }
|
||||||
|
do {
|
||||||
|
if statuses.isEmpty {
|
||||||
|
try await fetchFirstPage(client: client)
|
||||||
|
} else if let latest = statuses.first {
|
||||||
|
try await fetchNewPagesFrom(latestStatus: latest, client: client)
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
statusesState = .error(error: error)
|
||||||
|
canStreamEvents = true
|
||||||
|
print("timeline parse error: \(error)")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Hydrate statuses in the Timeline when statuses are empty.
|
||||||
|
private func fetchFirstPage(client: Client) async throws {
|
||||||
|
pendingStatusesObserver.pendingStatuses = []
|
||||||
|
statusesState = .loading
|
||||||
|
|
||||||
|
// 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(), timeline == .home {
|
||||||
|
statuses = cachedStatuses
|
||||||
|
withAnimation {
|
||||||
|
statusesState = .display(statuses: statuses, nextPageState: statuses.count < 20 ? .none : .hasNextPage)
|
||||||
|
}
|
||||||
|
// And then we fetch statuses again toget newest statuses from there.
|
||||||
|
await fetchStatuses()
|
||||||
|
} else {
|
||||||
|
statuses = try await client.get(endpoint: timeline.endpoint(sinceId: nil,
|
||||||
|
maxId: nil,
|
||||||
|
minId: nil,
|
||||||
|
offset: statuses.count))
|
||||||
|
if timeline == .home {
|
||||||
|
await cache(statuses: statuses)
|
||||||
|
}
|
||||||
|
withAnimation {
|
||||||
|
statusesState = .display(statuses: statuses, nextPageState: statuses.count < 20 ? .none : .hasNextPage)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fetch pages from the top most status of the tomeline.
|
||||||
|
private func fetchNewPagesFrom(latestStatus: Status, client: Client) async throws {
|
||||||
|
canStreamEvents = false
|
||||||
|
var newStatuses: [Status] = await fetchNewPages(minId: latestStatus.id, maxPages: 10)
|
||||||
|
|
||||||
|
// Dedup statuses, a status with the same id could have been streamed in.
|
||||||
|
newStatuses = newStatuses.filter { status in
|
||||||
|
!statuses.contains(where: { $0.id == status.id })
|
||||||
|
}
|
||||||
|
|
||||||
|
// If no new statuses, resume streaming and exit.
|
||||||
|
guard !newStatuses.isEmpty else {
|
||||||
|
canStreamEvents = true
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Keep track of the top most status, so we can scroll back to it after view update.
|
||||||
|
let topStatusId = statuses.first?.id
|
||||||
|
|
||||||
|
// Insert new statuses in internal datasource.
|
||||||
|
statuses.insert(contentsOf: newStatuses, at: 0)
|
||||||
|
|
||||||
|
// Cache statuses for home timeline.
|
||||||
|
if timeline == .home {
|
||||||
|
await cache(statuses: statuses)
|
||||||
|
}
|
||||||
|
|
||||||
|
// If pending statuses are not enabled, we simply load status on the top regardless of the current position.
|
||||||
|
if !pendingStatusesEnabled {
|
||||||
|
pendingStatusesObserver.pendingStatuses = []
|
||||||
|
withAnimation {
|
||||||
|
statusesState = .display(statuses: statuses, nextPageState: statuses.count < 20 ? .none : .hasNextPage)
|
||||||
|
canStreamEvents = true
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Append new statuses in the timeline indicator.
|
||||||
|
pendingStatusesObserver.pendingStatuses.insert(contentsOf: newStatuses.map{ $0.id }, at: 0)
|
||||||
|
pendingStatusesObserver.feedbackGenerator.impactOccurred()
|
||||||
|
|
||||||
|
// 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 {
|
||||||
|
pendingStatusesObserver.disableUpdate = true
|
||||||
|
statusesState = .display(statuses: statuses, nextPageState: statuses.count < 20 ? .none : .hasNextPage)
|
||||||
|
scrollToStatus = topStatusId
|
||||||
|
DispatchQueue.main.async {
|
||||||
|
self.pendingStatusesObserver.disableUpdate = false
|
||||||
|
self.canStreamEvents = true
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// This will keep the scroll position (if the list is scrolled) and prepend statuses on the top.
|
||||||
|
withAnimation {
|
||||||
|
statusesState = .display(statuses: statuses, nextPageState: statuses.count < 20 ? .none : .hasNextPage)
|
||||||
|
canStreamEvents = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func fetchNewPages(minId: String, maxPages: Int) async -> [Status] {
|
||||||
|
guard let client else { return [] }
|
||||||
|
var pagesLoaded = 0
|
||||||
|
var allStatuses: [Status] = []
|
||||||
|
var latestMinId = minId
|
||||||
|
do {
|
||||||
|
while let newStatuses: [Status] = try await client.get(endpoint: timeline.endpoint(sinceId: nil,
|
||||||
|
maxId: nil,
|
||||||
|
minId: latestMinId,
|
||||||
|
offset: statuses.count)),
|
||||||
|
!newStatuses.isEmpty,
|
||||||
|
pagesLoaded < maxPages
|
||||||
|
{
|
||||||
|
pagesLoaded += 1
|
||||||
|
allStatuses.insert(contentsOf: newStatuses, at: 0)
|
||||||
|
latestMinId = newStatuses.first?.id ?? ""
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
return allStatuses
|
||||||
|
}
|
||||||
|
return allStatuses
|
||||||
|
}
|
||||||
|
|
||||||
|
func fetchNextPage() async {
|
||||||
|
guard let client else { return }
|
||||||
|
do {
|
||||||
|
guard let lastId = statuses.last?.id else { return }
|
||||||
|
statusesState = .display(statuses: statuses, nextPageState: .loadingNextPage)
|
||||||
|
let newStatuses: [Status] = try await client.get(endpoint: timeline.endpoint(sinceId: nil,
|
||||||
|
maxId: lastId,
|
||||||
|
minId: nil,
|
||||||
|
offset: statuses.count))
|
||||||
|
statuses.append(contentsOf: newStatuses)
|
||||||
|
statusesState = .display(statuses: statuses, nextPageState: .hasNextPage)
|
||||||
|
} catch {
|
||||||
|
statusesState = .error(error: error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func statusDidAppear(status: Status) {
|
func statusDidAppear(status: Status) {
|
||||||
pendingStatusesObserver.removeStatus(status: status)
|
pendingStatusesObserver.removeStatus(status: status)
|
||||||
visibileStatusesIds.insert(status.id)
|
visibileStatusesIds.insert(status.id)
|
||||||
|
|
Loading…
Reference in a new issue