mirror of
https://github.com/Dimillian/IceCubesApp.git
synced 2024-11-29 03:31:02 +00:00
Small Refactor TimelineViewModel
This commit is contained in:
parent
7a9b6cc0e0
commit
82338f815a
1 changed files with 69 additions and 78 deletions
|
@ -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)
|
|
||||||
|
|
||||||
// Dedup statuses, a status with the same id could have been streamed in.
|
let newStatuses = await fetchAndDedupNewStatuses(latestStatus: latestStatus, client: client)
|
||||||
let ids = await datasource.get().map(\.id)
|
|
||||||
newStatuses = newStatuses.filter { status in
|
|
||||||
!ids.contains(where: { $0 == status.id })
|
|
||||||
}
|
|
||||||
|
|
||||||
StatusDataControllerProvider.shared.updateDataControllers(for: newStatuses, client: client)
|
guard !newStatuses.isEmpty,
|
||||||
|
isTimelineVisible,
|
||||||
// If no new statuses, resume streaming and exit.
|
!Task.isCancelled,
|
||||||
guard !newStatuses.isEmpty else {
|
initialTimeline == timeline else {
|
||||||
canStreamEvents = true
|
canStreamEvents = true
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// If the timeline is not visible, we don't update it as it would mess up the user position.
|
await updateTimelineWithNewStatuses(newStatuses)
|
||||||
guard isTimelineVisible else {
|
|
||||||
canStreamEvents = true
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Return if task has been cancelled.
|
if !Task.isCancelled, let latest = await datasource.get().first {
|
||||||
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
|
|
||||||
|
|
||||||
// Insert new statuses in internal datasource.
|
|
||||||
await datasource.insert(contentOf: newStatuses, at: 0)
|
|
||||||
|
|
||||||
// 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 topStatus, visibileStatuses.contains(where: { $0.id == topStatus.id }), scrollToTopVisible {
|
|
||||||
pendingStatusesObserver.disableUpdate = true
|
|
||||||
let statuses = await datasource.getFiltered()
|
|
||||||
statusesState = .display(statuses: statuses,
|
|
||||||
nextPageState: statuses.count < 20 ? .none : .hasNextPage)
|
|
||||||
scrollToIndexAnimated = false
|
|
||||||
scrollToIndex = newStatuses.count + 1
|
|
||||||
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.
|
|
||||||
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
|
|
||||||
{
|
|
||||||
pendingStatusesObserver.isLoadingNewStatuses = true
|
pendingStatusesObserver.isLoadingNewStatuses = true
|
||||||
try await fetchNewPagesFrom(latestStatus: latest.id, client: client)
|
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)
|
||||||
|
newStatuses = newStatuses.filter { status in
|
||||||
|
!ids.contains(where: { $0 == status.id })
|
||||||
|
}
|
||||||
|
StatusDataControllerProvider.shared.updateDataControllers(for: newStatuses, client: client)
|
||||||
|
return newStatuses
|
||||||
|
}
|
||||||
|
|
||||||
|
private func updateTimelineWithNewStatuses(_ newStatuses: [Status]) async {
|
||||||
|
let topStatus = await datasource.getFiltered().first
|
||||||
|
await datasource.insert(contentOf: newStatuses, at: 0)
|
||||||
|
await cache()
|
||||||
|
pendingStatusesObserver.pendingStatuses.insert(contentsOf: newStatuses.map(\.id), at: 0)
|
||||||
|
|
||||||
|
let statuses = await datasource.getFiltered()
|
||||||
|
let nextPageState: StatusesState.PagingState = statuses.count < 20 ? .none : .hasNextPage
|
||||||
|
|
||||||
|
if let topStatus = topStatus,
|
||||||
|
visibileStatuses.contains(where: { $0.id == topStatus.id }),
|
||||||
|
scrollToTopVisible {
|
||||||
|
updateTimelineWithScrollToTop(newStatuses: newStatuses, statuses: statuses, nextPageState: nextPageState)
|
||||||
|
} else {
|
||||||
|
updateTimelineWithAnimation(statuses: statuses, nextPageState: nextPageState)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Refresh the timeline while keeping the scroll position to the top status.
|
||||||
|
private func updateTimelineWithScrollToTop(newStatuses: [Status], statuses: [Status], nextPageState: StatusesState.PagingState) {
|
||||||
|
pendingStatusesObserver.disableUpdate = true
|
||||||
|
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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue