mirror of
https://github.com/Dimillian/IceCubesApp.git
synced 2024-12-24 08:00:38 +00:00
Merge branch 'main' into iOS-18
This commit is contained in:
commit
cf38a77105
2 changed files with 131 additions and 80 deletions
|
@ -47,6 +47,7 @@ import SwiftUI
|
|||
|
||||
// Internal source of truth for a timeline.
|
||||
private(set) var datasource = TimelineDatasource()
|
||||
private let statusFetcher: TimelineStatusFetching
|
||||
private let cache = TimelineCache()
|
||||
private var isCacheEnabled: Bool {
|
||||
canFilterTimeline && timeline.supportNewestPagination && client?.isAuth == true
|
||||
|
@ -93,7 +94,8 @@ import SwiftUI
|
|||
var scrollToIndexAnimated: Bool = false
|
||||
var marker: Marker.Content?
|
||||
|
||||
init() {
|
||||
init(statusFetcher: TimelineStatusFetching = TimelineStatusFetcher()) {
|
||||
self.statusFetcher = statusFetcher
|
||||
pendingStatusesObserver.scrollToIndex = { [weak self] index in
|
||||
self?.scrollToIndexAnimated = true
|
||||
self?.scrollToIndex = index
|
||||
|
@ -123,41 +125,6 @@ import SwiftUI
|
|||
timeline = oldValue
|
||||
}
|
||||
}
|
||||
|
||||
func handleEvent(event: any StreamEvent) async {
|
||||
if let event = event as? StreamEventUpdate,
|
||||
let client,
|
||||
timeline == .home,
|
||||
canStreamEvents,
|
||||
isTimelineVisible,
|
||||
await !datasource.contains(statusId: event.status.id)
|
||||
{
|
||||
pendingStatusesObserver.pendingStatuses.insert(event.status.id, at: 0)
|
||||
let newStatus = event.status
|
||||
await datasource.insert(newStatus, at: 0)
|
||||
await cache()
|
||||
StatusDataControllerProvider.shared.updateDataControllers(for: [event.status], client: client)
|
||||
let statuses = await datasource.getFiltered()
|
||||
withAnimation {
|
||||
statusesState = .display(statuses: statuses, nextPageState: .hasNextPage)
|
||||
}
|
||||
} else if let event = event as? StreamEventDelete {
|
||||
await datasource.remove(event.status)
|
||||
await cache()
|
||||
let statuses = await datasource.getFiltered()
|
||||
withAnimation {
|
||||
statusesState = .display(statuses: statuses, nextPageState: .hasNextPage)
|
||||
}
|
||||
} else if let event = event as? StreamEventStatusUpdate, let client {
|
||||
if let originalIndex = await datasource.indexOf(statusId: event.status.id) {
|
||||
StatusDataControllerProvider.shared.updateDataControllers(for: [event.status], client: client)
|
||||
await datasource.replace(event.status, at: originalIndex)
|
||||
await cache()
|
||||
let statuses = await datasource.getFiltered()
|
||||
statusesState = .display(statuses: statuses, nextPageState: .hasNextPage)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Cache
|
||||
|
@ -216,10 +183,8 @@ extension TimelineViewModel: StatusesFetcher {
|
|||
func fetchStatuses(from: Marker.Content) async throws {
|
||||
guard let client else { return }
|
||||
statusesState = .loading
|
||||
var statuses: [Status] = try await client.get(endpoint: timeline.endpoint(sinceId: nil,
|
||||
maxId: from.lastReadId,
|
||||
minId: nil,
|
||||
offset: 0))
|
||||
var statuses: [Status] = try await statusFetcher.fetchFirstPage(client: client,
|
||||
timeline: timeline)
|
||||
|
||||
StatusDataControllerProvider.shared.updateDataControllers(for: statuses, client: client)
|
||||
|
||||
|
@ -286,10 +251,8 @@ extension TimelineViewModel: StatusesFetcher {
|
|||
// And then we fetch statuses again toget newest statuses from there.
|
||||
await fetchNewestStatuses(pullToRefresh: false)
|
||||
} else {
|
||||
var statuses: [Status] = try await client.get(endpoint: timeline.endpoint(sinceId: nil,
|
||||
maxId: nil,
|
||||
minId: nil,
|
||||
offset: 0))
|
||||
var statuses: [Status] = try await statusFetcher.fetchFirstPage(client: client,
|
||||
timeline: timeline)
|
||||
|
||||
StatusDataControllerProvider.shared.updateDataControllers(for: statuses, client: client)
|
||||
|
||||
|
@ -308,7 +271,8 @@ extension TimelineViewModel: StatusesFetcher {
|
|||
canStreamEvents = false
|
||||
let initialTimeline = timeline
|
||||
|
||||
let newStatuses = await fetchAndDedupNewStatuses(latestStatus: latestStatus, client: client)
|
||||
let newStatuses = try await fetchAndDedupNewStatuses(latestStatus: latestStatus,
|
||||
client: client)
|
||||
|
||||
guard !newStatuses.isEmpty,
|
||||
isTimelineVisible,
|
||||
|
@ -327,8 +291,11 @@ extension TimelineViewModel: StatusesFetcher {
|
|||
}
|
||||
}
|
||||
|
||||
private func fetchAndDedupNewStatuses(latestStatus: String, client: Client) async -> [Status] {
|
||||
var newStatuses = await fetchNewPages(minId: latestStatus, maxPages: 5)
|
||||
private func fetchAndDedupNewStatuses(latestStatus: String, client: Client) async throws -> [Status] {
|
||||
var newStatuses = try await statusFetcher.fetchNewPages(client: client,
|
||||
timeline: timeline,
|
||||
minId: latestStatus,
|
||||
maxPages: 5)
|
||||
let ids = await datasource.get().map(\.id)
|
||||
newStatuses = newStatuses.filter { status in
|
||||
!ids.contains(where: { $0 == status.id })
|
||||
|
@ -378,34 +345,6 @@ extension TimelineViewModel: StatusesFetcher {
|
|||
}
|
||||
}
|
||||
|
||||
private func fetchNewPages(minId: String, maxPages: Int) async -> [Status] {
|
||||
guard let client else { return [] }
|
||||
var allStatuses: [Status] = []
|
||||
var latestMinId = minId
|
||||
do {
|
||||
for _ in 1 ... maxPages {
|
||||
if Task.isCancelled { break }
|
||||
|
||||
let newStatuses: [Status] = try await client.get(endpoint: timeline.endpoint(
|
||||
sinceId: nil,
|
||||
maxId: nil,
|
||||
minId: latestMinId,
|
||||
offset: nil
|
||||
))
|
||||
|
||||
if newStatuses.isEmpty { break }
|
||||
|
||||
StatusDataControllerProvider.shared.updateDataControllers(for: newStatuses, client: client)
|
||||
allStatuses.insert(contentsOf: newStatuses, at: 0)
|
||||
latestMinId = newStatuses.first?.id ?? latestMinId
|
||||
}
|
||||
} catch {
|
||||
return allStatuses
|
||||
}
|
||||
|
||||
return allStatuses
|
||||
}
|
||||
|
||||
enum NextPageError: Error {
|
||||
case internalError
|
||||
}
|
||||
|
@ -413,10 +352,10 @@ extension TimelineViewModel: StatusesFetcher {
|
|||
func fetchNextPage() async throws {
|
||||
let statuses = await datasource.get()
|
||||
guard let client, let lastId = statuses.last?.id else { throw NextPageError.internalError }
|
||||
let newStatuses: [Status] = try await client.get(endpoint: timeline.endpoint(sinceId: nil,
|
||||
maxId: lastId,
|
||||
minId: nil,
|
||||
offset: statuses.count))
|
||||
let newStatuses: [Status] = try await statusFetcher.fetchNextPage(client: client,
|
||||
timeline: timeline,
|
||||
lastId: lastId,
|
||||
offset: statuses.count)
|
||||
|
||||
await datasource.append(contentOf: newStatuses)
|
||||
StatusDataControllerProvider.shared.updateDataControllers(for: newStatuses, client: client)
|
||||
|
@ -441,7 +380,7 @@ extension TimelineViewModel: StatusesFetcher {
|
|||
}
|
||||
}
|
||||
|
||||
// MARK: - MARKER
|
||||
// MARK: - Marker handling
|
||||
|
||||
extension TimelineViewModel {
|
||||
func fetchMarker() async -> Marker.Content? {
|
||||
|
@ -466,3 +405,55 @@ extension TimelineViewModel {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Event handling
|
||||
|
||||
extension TimelineViewModel {
|
||||
func handleEvent(event: any StreamEvent) async {
|
||||
guard let client = client, canStreamEvents, isTimelineVisible else { return }
|
||||
|
||||
switch event {
|
||||
case let updateEvent as StreamEventUpdate:
|
||||
await handleUpdateEvent(updateEvent, client: client)
|
||||
case let deleteEvent as StreamEventDelete:
|
||||
await handleDeleteEvent(deleteEvent)
|
||||
case let statusUpdateEvent as StreamEventStatusUpdate:
|
||||
await handleStatusUpdateEvent(statusUpdateEvent, client: client)
|
||||
default:
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
private func handleUpdateEvent(_ event: StreamEventUpdate, client: Client) async {
|
||||
guard timeline == .home,
|
||||
await !datasource.contains(statusId: event.status.id) else { return }
|
||||
|
||||
pendingStatusesObserver.pendingStatuses.insert(event.status.id, at: 0)
|
||||
await datasource.insert(event.status, at: 0)
|
||||
await cache()
|
||||
StatusDataControllerProvider.shared.updateDataControllers(for: [event.status], client: client)
|
||||
await updateStatusesState()
|
||||
}
|
||||
|
||||
private func handleDeleteEvent(_ event: StreamEventDelete) async {
|
||||
await datasource.remove(event.status)
|
||||
await cache()
|
||||
await updateStatusesState()
|
||||
}
|
||||
|
||||
private func handleStatusUpdateEvent(_ event: StreamEventStatusUpdate, client: Client) async {
|
||||
guard let originalIndex = await datasource.indexOf(statusId: event.status.id) else { return }
|
||||
|
||||
StatusDataControllerProvider.shared.updateDataControllers(for: [event.status], client: client)
|
||||
await datasource.replace(event.status, at: originalIndex)
|
||||
await cache()
|
||||
await updateStatusesState()
|
||||
}
|
||||
|
||||
private func updateStatusesState() async {
|
||||
let statuses = await datasource.getFiltered()
|
||||
withAnimation {
|
||||
statusesState = .display(statuses: statuses, nextPageState: .hasNextPage)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,60 @@
|
|||
import Foundation
|
||||
import Models
|
||||
import Network
|
||||
|
||||
protocol TimelineStatusFetching: Sendable {
|
||||
func fetchFirstPage(client: Client?,
|
||||
timeline: TimelineFilter) async throws -> [Status]
|
||||
func fetchNewPages(client: Client?,
|
||||
timeline: TimelineFilter,
|
||||
minId: String,
|
||||
maxPages: Int) async throws -> [Status]
|
||||
func fetchNextPage(client: Client?,
|
||||
timeline: TimelineFilter,
|
||||
lastId: String,
|
||||
offset: Int) async throws -> [Status]
|
||||
}
|
||||
|
||||
enum StatusFetcherError: Error {
|
||||
case noClientAvailable
|
||||
}
|
||||
|
||||
struct TimelineStatusFetcher: TimelineStatusFetching {
|
||||
func fetchFirstPage(client: Client?, timeline: TimelineFilter) async throws -> [Status] {
|
||||
guard let client = client else { throw StatusFetcherError.noClientAvailable }
|
||||
return try await client.get(endpoint: timeline.endpoint(sinceId: nil,
|
||||
maxId: nil,
|
||||
minId: nil,
|
||||
offset: 0))
|
||||
}
|
||||
|
||||
func fetchNewPages(client: Client?, timeline: TimelineFilter, minId: String, maxPages: Int) async throws -> [Status] {
|
||||
guard let client = client else { throw StatusFetcherError.noClientAvailable }
|
||||
var allStatuses: [Status] = []
|
||||
var latestMinId = minId
|
||||
for _ in 1...maxPages {
|
||||
if Task.isCancelled { break }
|
||||
|
||||
let newStatuses: [Status] = try await client.get(endpoint: timeline.endpoint(
|
||||
sinceId: nil,
|
||||
maxId: nil,
|
||||
minId: latestMinId,
|
||||
offset: nil
|
||||
))
|
||||
|
||||
if newStatuses.isEmpty { break }
|
||||
|
||||
allStatuses.insert(contentsOf: newStatuses, at: 0)
|
||||
latestMinId = newStatuses.first?.id ?? latestMinId
|
||||
}
|
||||
return allStatuses
|
||||
}
|
||||
|
||||
func fetchNextPage(client: Client?, timeline: TimelineFilter, lastId: String, offset: Int) async throws -> [Status] {
|
||||
guard let client = client else { throw StatusFetcherError.noClientAvailable }
|
||||
return try await client.get(endpoint: timeline.endpoint(sinceId: nil,
|
||||
maxId: lastId,
|
||||
minId: nil,
|
||||
offset: offset))
|
||||
}
|
||||
}
|
Loading…
Reference in a new issue