Small Refactor TimelineViewModel

This commit is contained in:
Thomas Ricouard 2024-07-21 11:20:36 +02:00
parent 7a9b6cc0e0
commit 82338f815a

View File

@ -307,109 +307,100 @@ extension TimelineViewModel: StatusesFetcher {
private func fetchNewPagesFrom(latestStatus: String, client: Client) async throws { private func fetchNewPagesFrom(latestStatus: String, client: Client) async throws {
canStreamEvents = false canStreamEvents = false
let initialTimeline = timeline let initialTimeline = timeline
var newStatuses: [Status] = await fetchNewPages(minId: latestStatus, maxPages: 5)
let newStatuses = await fetchAndDedupNewStatuses(latestStatus: latestStatus, client: client)
// Dedup statuses, a status with the same id could have been streamed in.
guard !newStatuses.isEmpty,
isTimelineVisible,
!Task.isCancelled,
initialTimeline == timeline else {
canStreamEvents = true
return
}
await updateTimelineWithNewStatuses(newStatuses)
if !Task.isCancelled, let latest = await datasource.get().first {
pendingStatusesObserver.isLoadingNewStatuses = true
try await fetchNewPagesFrom(latestStatus: latest.id, client: client)
}
}
private func fetchAndDedupNewStatuses(latestStatus: String, client: Client) async -> [Status] {
var newStatuses = await fetchNewPages(minId: latestStatus, maxPages: 5)
let ids = await datasource.get().map(\.id) let ids = await datasource.get().map(\.id)
newStatuses = newStatuses.filter { status in newStatuses = newStatuses.filter { status in
!ids.contains(where: { $0 == status.id }) !ids.contains(where: { $0 == status.id })
} }
StatusDataControllerProvider.shared.updateDataControllers(for: newStatuses, client: client) StatusDataControllerProvider.shared.updateDataControllers(for: newStatuses, client: client)
return newStatuses
// If no new statuses, resume streaming and exit. }
guard !newStatuses.isEmpty else {
canStreamEvents = true private func updateTimelineWithNewStatuses(_ newStatuses: [Status]) async {
return
}
// If the timeline is not visible, we don't update it as it would mess up the user position.
guard isTimelineVisible else {
canStreamEvents = true
return
}
// Return if task has been cancelled.
guard !Task.isCancelled else {
canStreamEvents = true
return
}
// As this is a long runnign task we need to ensure that the user didn't changed the timeline filter.
guard initialTimeline == timeline else {
canStreamEvents = true
return
}
// Keep track of the top most status, so we can scroll back to it after view update.
let topStatus = await datasource.getFiltered().first let topStatus = await datasource.getFiltered().first
// Insert new statuses in internal datasource.
await datasource.insert(contentOf: newStatuses, at: 0) await datasource.insert(contentOf: newStatuses, at: 0)
// Cache statuses for timeline.
await cache() await cache()
// Append new statuses in the timeline indicator.
pendingStatusesObserver.pendingStatuses.insert(contentsOf: newStatuses.map(\.id), at: 0) pendingStatusesObserver.pendingStatuses.insert(contentsOf: newStatuses.map(\.id), at: 0)
// High chance the user is scrolled to the top. let statuses = await datasource.getFiltered()
// We need to update the statuses state, and then scroll to the previous top most status. let nextPageState: StatusesState.PagingState = statuses.count < 20 ? .none : .hasNextPage
if let topStatus, visibileStatuses.contains(where: { $0.id == topStatus.id }), scrollToTopVisible {
pendingStatusesObserver.disableUpdate = true if let topStatus = topStatus,
let statuses = await datasource.getFiltered() visibileStatuses.contains(where: { $0.id == topStatus.id }),
statusesState = .display(statuses: statuses, scrollToTopVisible {
nextPageState: statuses.count < 20 ? .none : .hasNextPage) updateTimelineWithScrollToTop(newStatuses: newStatuses, statuses: statuses, nextPageState: nextPageState)
scrollToIndexAnimated = false
scrollToIndex = newStatuses.count + 1
DispatchQueue.main.async {
self.pendingStatusesObserver.disableUpdate = false
self.canStreamEvents = true
}
} else { } else {
// This will keep the scroll position (if the list is scrolled) and prepend statuses on the top. updateTimelineWithAnimation(statuses: statuses, nextPageState: nextPageState)
let statuses = await datasource.getFiltered()
withAnimation {
statusesState = .display(statuses: statuses,
nextPageState: statuses.count < 20 ? .none : .hasNextPage)
canStreamEvents = true
}
} }
}
if !Task.isCancelled,
let latest = await datasource.get().first // Refresh the timeline while keeping the scroll position to the top status.
{ private func updateTimelineWithScrollToTop(newStatuses: [Status], statuses: [Status], nextPageState: StatusesState.PagingState) {
pendingStatusesObserver.isLoadingNewStatuses = true pendingStatusesObserver.disableUpdate = true
try await fetchNewPagesFrom(latestStatus: latest.id, client: client) statusesState = .display(statuses: statuses, nextPageState: nextPageState)
scrollToIndexAnimated = false
scrollToIndex = newStatuses.count + 1
DispatchQueue.main.async { [weak self] in
self?.pendingStatusesObserver.disableUpdate = false
self?.canStreamEvents = true
}
}
// Refresh the timeline while keeping the user current position.
// It works because a side effect of withAnimation is that it keep scroll position IF the List is not scrolled to the top.
private func updateTimelineWithAnimation(statuses: [Status], nextPageState: StatusesState.PagingState) {
withAnimation {
statusesState = .display(statuses: statuses, nextPageState: nextPageState)
canStreamEvents = true
} }
} }
private func fetchNewPages(minId: String, maxPages: Int) async -> [Status] { private func fetchNewPages(minId: String, maxPages: Int) async -> [Status] {
guard let client else { return [] } guard let client else { return [] }
var pagesLoaded = 0
var allStatuses: [Status] = [] var allStatuses: [Status] = []
var latestMinId = minId var latestMinId = minId
do { do {
while for _ in 1...maxPages {
!Task.isCancelled, if Task.isCancelled { break }
let newStatuses: [Status] =
try await client.get(endpoint: timeline.endpoint(sinceId: nil, let newStatuses: [Status] = try await client.get(endpoint: timeline.endpoint(
maxId: nil, sinceId: nil,
minId: latestMinId, maxId: nil,
offset: datasource.get().count)), minId: latestMinId,
!newStatuses.isEmpty, offset: nil
pagesLoaded < maxPages ))
{
pagesLoaded += 1 if newStatuses.isEmpty { break }
StatusDataControllerProvider.shared.updateDataControllers(for: newStatuses, client: client) StatusDataControllerProvider.shared.updateDataControllers(for: newStatuses, client: client)
allStatuses.insert(contentsOf: newStatuses, at: 0) allStatuses.insert(contentsOf: newStatuses, at: 0)
latestMinId = newStatuses.first?.id ?? "" latestMinId = newStatuses.first?.id ?? latestMinId
} }
} catch { } catch {
return allStatuses return allStatuses
} }
return allStatuses return allStatuses
} }