2023-01-17 10:36:01 +00:00
|
|
|
import Env
|
2022-11-29 08:28:17 +00:00
|
|
|
import Models
|
2023-01-17 10:36:01 +00:00
|
|
|
import Network
|
2023-09-18 05:01:23 +00:00
|
|
|
import Observation
|
2024-01-06 18:27:26 +00:00
|
|
|
import StatusKit
|
2023-01-17 10:36:01 +00:00
|
|
|
import SwiftUI
|
2022-11-25 11:00:01 +00:00
|
|
|
|
|
|
|
@MainActor
|
2023-09-18 05:01:23 +00:00
|
|
|
@Observable class TimelineViewModel {
|
|
|
|
var scrollToIndex: Int?
|
|
|
|
var statusesState: StatusesState = .loading
|
|
|
|
var timeline: TimelineFilter = .federated {
|
2023-12-27 12:26:30 +00:00
|
|
|
willSet {
|
|
|
|
if timeline == .home && newValue != .resume {
|
|
|
|
saveMarker()
|
|
|
|
}
|
|
|
|
}
|
2022-12-01 08:05:26 +00:00
|
|
|
didSet {
|
2023-02-08 17:46:09 +00:00
|
|
|
timelineTask?.cancel()
|
|
|
|
timelineTask = Task {
|
2023-12-30 08:51:34 +00:00
|
|
|
await handleLatestOrResume(oldValue)
|
2023-12-27 12:26:30 +00:00
|
|
|
|
2022-12-25 11:46:42 +00:00
|
|
|
if oldValue != timeline {
|
2023-02-25 18:47:15 +00:00
|
|
|
await reset()
|
2023-01-31 08:01:26 +00:00
|
|
|
pendingStatusesObserver.pendingStatuses = []
|
2023-01-04 17:37:58 +00:00
|
|
|
tag = nil
|
2022-12-25 11:46:42 +00:00
|
|
|
}
|
2023-12-27 12:26:30 +00:00
|
|
|
|
2023-02-08 17:46:09 +00:00
|
|
|
guard !Task.isCancelled else {
|
|
|
|
return
|
|
|
|
}
|
2023-12-27 12:26:30 +00:00
|
|
|
|
2024-01-04 11:56:46 +00:00
|
|
|
await fetchNewestStatuses(pullToRefresh: false)
|
2022-12-25 07:17:16 +00:00
|
|
|
switch timeline {
|
|
|
|
case let .hashtag(tag, _):
|
|
|
|
await fetchTag(id: tag)
|
|
|
|
default:
|
|
|
|
break
|
2022-12-01 08:05:26 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
2023-02-12 15:29:41 +00:00
|
|
|
|
2023-12-30 14:40:04 +00:00
|
|
|
private(set) var timelineTask: Task<Void, Never>?
|
2023-01-17 10:36:01 +00:00
|
|
|
|
2023-09-18 05:01:23 +00:00
|
|
|
var tag: Tag?
|
2023-07-19 05:46:25 +00:00
|
|
|
|
2023-02-04 16:17:38 +00:00
|
|
|
// Internal source of truth for a timeline.
|
2023-12-30 08:08:19 +00:00
|
|
|
private(set) var datasource = TimelineDatasource()
|
2023-09-22 06:32:13 +00:00
|
|
|
private let cache = TimelineCache()
|
2024-01-03 13:59:28 +00:00
|
|
|
private var isCacheEnabled: Bool {
|
|
|
|
canFilterTimeline && timeline.supportNewestPagination
|
|
|
|
}
|
2024-01-03 10:34:50 +00:00
|
|
|
|
|
|
|
@ObservationIgnored
|
|
|
|
private var visibileStatuses: [Status] = []
|
|
|
|
|
2024-01-04 11:56:46 +00:00
|
|
|
private var canStreamEvents: Bool = true {
|
|
|
|
didSet {
|
|
|
|
if canStreamEvents {
|
|
|
|
pendingStatusesObserver.isLoadingNewStatuses = false
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
2024-01-03 13:59:28 +00:00
|
|
|
|
|
|
|
@ObservationIgnored
|
|
|
|
var canFilterTimeline: Bool = true
|
2023-02-04 16:17:38 +00:00
|
|
|
|
|
|
|
var client: Client? {
|
|
|
|
didSet {
|
|
|
|
if oldValue != client {
|
2023-02-18 17:04:46 +00:00
|
|
|
Task {
|
2023-02-25 18:47:15 +00:00
|
|
|
await reset()
|
2023-02-18 17:04:46 +00:00
|
|
|
}
|
2023-02-04 16:17:38 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
var scrollToTopVisible: Bool = false {
|
|
|
|
didSet {
|
|
|
|
if scrollToTopVisible {
|
|
|
|
pendingStatusesObserver.pendingStatuses = []
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-11-25 11:00:01 +00:00
|
|
|
var serverName: String {
|
2022-12-19 11:28:55 +00:00
|
|
|
client?.server ?? "Error"
|
2022-11-25 11:00:01 +00:00
|
|
|
}
|
2023-02-04 16:17:38 +00:00
|
|
|
|
|
|
|
var isTimelineVisible: Bool = false
|
2023-12-31 10:18:42 +00:00
|
|
|
let pendingStatusesObserver: TimelineUnreadStatusesObserver = .init()
|
2023-02-04 16:17:38 +00:00
|
|
|
var scrollToIndexAnimated: Bool = false
|
2023-12-27 12:26:30 +00:00
|
|
|
var marker: Marker.Content?
|
|
|
|
|
2023-02-04 13:05:30 +00:00
|
|
|
init() {
|
|
|
|
pendingStatusesObserver.scrollToIndex = { [weak self] index in
|
|
|
|
self?.scrollToIndexAnimated = true
|
|
|
|
self?.scrollToIndex = index
|
|
|
|
}
|
|
|
|
}
|
2023-01-17 10:36:01 +00:00
|
|
|
|
2023-02-02 05:39:03 +00:00
|
|
|
private func fetchTag(id: String) async {
|
2023-02-01 11:43:11 +00:00
|
|
|
guard let client else { return }
|
|
|
|
do {
|
2024-01-03 11:33:06 +00:00
|
|
|
let tag: Tag = try await client.get(endpoint: Tags.tag(id: id))
|
|
|
|
withAnimation {
|
|
|
|
self.tag = tag
|
|
|
|
}
|
2023-02-01 11:43:11 +00:00
|
|
|
} catch {}
|
|
|
|
}
|
2023-02-21 06:23:42 +00:00
|
|
|
|
2023-02-19 20:37:22 +00:00
|
|
|
func reset() async {
|
|
|
|
await datasource.reset()
|
|
|
|
}
|
2023-12-30 08:51:34 +00:00
|
|
|
|
|
|
|
private func handleLatestOrResume(_ oldValue: TimelineFilter) async {
|
|
|
|
if timeline == .latest || timeline == .resume {
|
2024-01-03 10:34:50 +00:00
|
|
|
await clearCache(filter: oldValue)
|
2023-12-30 08:51:34 +00:00
|
|
|
if timeline == .resume, let marker = await fetchMarker() {
|
|
|
|
self.marker = marker
|
|
|
|
}
|
|
|
|
timeline = oldValue
|
|
|
|
}
|
|
|
|
}
|
2023-02-01 11:43:11 +00:00
|
|
|
|
2023-12-30 08:08:19 +00:00
|
|
|
func handleEvent(event: any StreamEvent) async {
|
|
|
|
if let event = event as? StreamEventUpdate,
|
2024-01-01 15:46:34 +00:00
|
|
|
let client,
|
2023-12-30 08:08:19 +00:00
|
|
|
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)
|
2024-01-03 10:34:50 +00:00
|
|
|
await cache()
|
2024-01-01 15:46:34 +00:00
|
|
|
StatusDataControllerProvider.shared.updateDataControllers(for: [event.status], client: client)
|
2024-01-04 12:19:36 +00:00
|
|
|
let statuses = await datasource.getFiltered()
|
2023-12-30 08:08:19 +00:00
|
|
|
withAnimation {
|
|
|
|
statusesState = .display(statuses: statuses, nextPageState: .hasNextPage)
|
|
|
|
}
|
|
|
|
} else if let event = event as? StreamEventDelete {
|
|
|
|
await datasource.remove(event.status)
|
2024-01-03 10:34:50 +00:00
|
|
|
await cache()
|
2024-01-04 12:19:36 +00:00
|
|
|
let statuses = await datasource.getFiltered()
|
2023-12-30 08:08:19 +00:00
|
|
|
withAnimation {
|
|
|
|
statusesState = .display(statuses: statuses, nextPageState: .hasNextPage)
|
|
|
|
}
|
2024-01-01 15:46:34 +00:00
|
|
|
} else if let event = event as? StreamEventStatusUpdate, let client {
|
2023-12-30 08:08:19 +00:00
|
|
|
if let originalIndex = await datasource.indexOf(statusId: event.status.id) {
|
2024-01-01 15:46:34 +00:00
|
|
|
StatusDataControllerProvider.shared.updateDataControllers(for: [event.status], client: client)
|
2023-12-30 08:08:19 +00:00
|
|
|
await datasource.replace(event.status, at: originalIndex)
|
2024-01-03 10:34:50 +00:00
|
|
|
await cache()
|
2024-01-04 12:19:36 +00:00
|
|
|
let statuses = await datasource.getFiltered()
|
2023-12-30 14:40:04 +00:00
|
|
|
statusesState = .display(statuses: statuses, nextPageState: .hasNextPage)
|
2023-02-01 11:43:11 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// MARK: - Cache
|
2023-02-01 11:49:59 +00:00
|
|
|
|
2023-02-01 11:43:11 +00:00
|
|
|
extension TimelineViewModel {
|
2024-01-03 10:34:50 +00:00
|
|
|
private func cache() async {
|
2024-01-03 13:59:28 +00:00
|
|
|
if let client, isCacheEnabled {
|
2024-01-03 10:34:50 +00:00
|
|
|
await cache.set(statuses: datasource.get(), client: client.id, filter: timeline.id)
|
2023-02-01 11:43:11 +00:00
|
|
|
}
|
|
|
|
}
|
2023-02-01 11:49:59 +00:00
|
|
|
|
2023-02-01 11:43:11 +00:00
|
|
|
private func getCachedStatuses() async -> [Status]? {
|
2024-01-03 13:59:28 +00:00
|
|
|
if let client, isCacheEnabled {
|
2024-01-03 10:34:50 +00:00
|
|
|
return await cache.getStatuses(for: client.id, filter: timeline.id)
|
2023-02-01 11:43:11 +00:00
|
|
|
}
|
|
|
|
return nil
|
|
|
|
}
|
2023-02-26 05:45:57 +00:00
|
|
|
|
2024-01-03 10:34:50 +00:00
|
|
|
private func clearCache(filter: TimelineFilter) async {
|
2024-01-03 13:59:28 +00:00
|
|
|
if let client, isCacheEnabled {
|
2024-01-03 10:34:50 +00:00
|
|
|
await cache.clearCache(for: client.id, filter: filter.id)
|
|
|
|
await cache.setLatestSeenStatuses([], for: client, filter: filter.id)
|
2023-02-25 18:32:47 +00:00
|
|
|
}
|
|
|
|
}
|
2023-02-01 11:43:11 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// MARK: - StatusesFetcher
|
2023-02-01 11:49:59 +00:00
|
|
|
|
2023-02-01 11:43:11 +00:00
|
|
|
extension TimelineViewModel: StatusesFetcher {
|
2023-02-27 17:41:51 +00:00
|
|
|
func pullToRefresh() async {
|
2023-04-09 13:11:02 +00:00
|
|
|
timelineTask?.cancel()
|
2023-12-01 07:51:19 +00:00
|
|
|
if !timeline.supportNewestPagination || UserPreferences.shared.fastRefreshEnabled {
|
2023-02-27 17:41:51 +00:00
|
|
|
await reset()
|
|
|
|
}
|
2024-01-04 11:56:46 +00:00
|
|
|
await fetchNewestStatuses(pullToRefresh: true)
|
2023-02-27 17:41:51 +00:00
|
|
|
}
|
2023-07-19 05:46:25 +00:00
|
|
|
|
2023-04-09 13:11:02 +00:00
|
|
|
func refreshTimeline() {
|
|
|
|
timelineTask?.cancel()
|
|
|
|
timelineTask = Task {
|
2023-12-01 07:51:19 +00:00
|
|
|
if UserPreferences.shared.fastRefreshEnabled {
|
|
|
|
await reset()
|
|
|
|
}
|
2024-01-04 11:56:46 +00:00
|
|
|
await fetchNewestStatuses(pullToRefresh: false)
|
2023-04-09 13:11:02 +00:00
|
|
|
}
|
|
|
|
}
|
2023-12-27 12:26:30 +00:00
|
|
|
|
|
|
|
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))
|
|
|
|
|
|
|
|
ReblogCache.shared.removeDuplicateReblogs(&statuses)
|
|
|
|
StatusDataControllerProvider.shared.updateDataControllers(for: statuses, client: client)
|
|
|
|
|
|
|
|
await datasource.set(statuses)
|
2024-01-03 10:34:50 +00:00
|
|
|
await cache()
|
2024-01-07 10:59:15 +00:00
|
|
|
statuses = await datasource.getFiltered()
|
2023-12-27 12:26:30 +00:00
|
|
|
marker = nil
|
|
|
|
|
|
|
|
withAnimation {
|
|
|
|
statusesState = .display(statuses: statuses, nextPageState: .hasNextPage)
|
|
|
|
}
|
|
|
|
|
2024-01-04 11:56:46 +00:00
|
|
|
await fetchNewestStatuses(pullToRefresh: false)
|
2023-12-27 12:26:30 +00:00
|
|
|
}
|
2023-03-13 12:38:28 +00:00
|
|
|
|
2024-01-04 11:56:46 +00:00
|
|
|
func fetchNewestStatuses(pullToRefresh: Bool) async {
|
2023-01-31 12:43:27 +00:00
|
|
|
guard let client else { return }
|
2022-11-25 11:00:01 +00:00
|
|
|
do {
|
2023-12-27 12:26:30 +00:00
|
|
|
if let marker {
|
|
|
|
try await fetchStatuses(from: marker)
|
|
|
|
} else if await datasource.isEmpty {
|
2023-02-01 11:43:11 +00:00
|
|
|
try await fetchFirstPage(client: client)
|
2023-02-27 17:41:51 +00:00
|
|
|
} else if let latest = await datasource.get().first, timeline.supportNewestPagination {
|
2024-01-04 11:56:46 +00:00
|
|
|
pendingStatusesObserver.isLoadingNewStatuses = !pullToRefresh
|
2023-12-27 12:26:30 +00:00
|
|
|
try await fetchNewPagesFrom(latestStatus: latest.id, client: client)
|
2022-12-24 11:20:42 +00:00
|
|
|
}
|
2022-11-25 11:00:01 +00:00
|
|
|
} catch {
|
2022-12-19 06:17:01 +00:00
|
|
|
statusesState = .error(error: error)
|
2023-01-31 16:43:06 +00:00
|
|
|
canStreamEvents = true
|
2022-11-25 11:00:01 +00:00
|
|
|
}
|
|
|
|
}
|
2023-02-01 11:49:59 +00:00
|
|
|
|
2023-02-01 11:43:11 +00:00
|
|
|
// Hydrate statuses in the Timeline when statuses are empty.
|
|
|
|
private func fetchFirstPage(client: Client) async throws {
|
|
|
|
pendingStatusesObserver.pendingStatuses = []
|
2023-02-12 15:29:41 +00:00
|
|
|
|
2023-02-18 17:04:46 +00:00
|
|
|
if await datasource.isEmpty {
|
2023-02-08 17:46:09 +00:00
|
|
|
statusesState = .loading
|
|
|
|
}
|
2023-02-01 11:49:59 +00:00
|
|
|
|
2023-02-01 11:43:11 +00:00
|
|
|
// If we get statuses from the cache for the home timeline, we displays those.
|
|
|
|
// Else we fetch top most page from the API.
|
2024-01-03 10:34:50 +00:00
|
|
|
if timeline.supportNewestPagination,
|
|
|
|
let cachedStatuses = await getCachedStatuses(),
|
2023-02-04 16:17:38 +00:00
|
|
|
!cachedStatuses.isEmpty,
|
2024-01-03 10:34:50 +00:00
|
|
|
!UserPreferences.shared.fastRefreshEnabled
|
2023-02-04 16:17:38 +00:00
|
|
|
{
|
2023-02-18 17:04:46 +00:00
|
|
|
await datasource.set(cachedStatuses)
|
2024-01-04 12:19:36 +00:00
|
|
|
let statuses = await datasource.getFiltered()
|
2024-01-03 10:34:50 +00:00
|
|
|
if let latestSeenId = await cache.getLatestSeenStatus(for: client, filter: timeline.id)?.first,
|
2023-02-18 17:04:46 +00:00
|
|
|
let index = await datasource.indexOf(statusId: latestSeenId),
|
2023-02-04 16:17:38 +00:00
|
|
|
index > 0
|
|
|
|
{
|
2023-02-04 13:42:10 +00:00
|
|
|
// Restore cache and scroll to latest seen status.
|
2024-01-03 10:34:50 +00:00
|
|
|
statusesState = .display(statuses: statuses, nextPageState: .hasNextPage)
|
2023-02-04 13:42:10 +00:00
|
|
|
scrollToIndexAnimated = false
|
|
|
|
scrollToIndex = index + 1
|
|
|
|
} else {
|
|
|
|
// Restore cache and scroll to top.
|
|
|
|
withAnimation {
|
2023-02-04 19:37:22 +00:00
|
|
|
statusesState = .display(statuses: statuses, nextPageState: .hasNextPage)
|
2023-02-04 13:42:10 +00:00
|
|
|
}
|
2023-02-01 11:43:11 +00:00
|
|
|
}
|
|
|
|
// And then we fetch statuses again toget newest statuses from there.
|
2024-01-04 11:56:46 +00:00
|
|
|
await fetchNewestStatuses(pullToRefresh: false)
|
2023-02-01 11:43:11 +00:00
|
|
|
} else {
|
2023-02-18 17:04:46 +00:00
|
|
|
var statuses: [Status] = try await client.get(endpoint: timeline.endpoint(sinceId: nil,
|
|
|
|
maxId: nil,
|
|
|
|
minId: nil,
|
|
|
|
offset: 0))
|
2023-02-01 17:56:06 +00:00
|
|
|
|
|
|
|
ReblogCache.shared.removeDuplicateReblogs(&statuses)
|
2023-03-02 05:42:58 +00:00
|
|
|
StatusDataControllerProvider.shared.updateDataControllers(for: statuses, client: client)
|
2023-02-21 06:23:42 +00:00
|
|
|
|
2023-02-18 17:04:46 +00:00
|
|
|
await datasource.set(statuses)
|
2024-01-03 10:34:50 +00:00
|
|
|
await cache()
|
2024-01-04 12:19:36 +00:00
|
|
|
statuses = await datasource.getFiltered()
|
2023-02-12 15:29:41 +00:00
|
|
|
|
2023-02-01 11:43:11 +00:00
|
|
|
withAnimation {
|
|
|
|
statusesState = .display(statuses: statuses, nextPageState: statuses.count < 20 ? .none : .hasNextPage)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
2023-02-01 11:49:59 +00:00
|
|
|
|
2023-02-01 11:43:11 +00:00
|
|
|
// Fetch pages from the top most status of the tomeline.
|
2023-12-27 12:26:30 +00:00
|
|
|
private func fetchNewPagesFrom(latestStatus: String, client: Client) async throws {
|
2023-02-01 11:43:11 +00:00
|
|
|
canStreamEvents = false
|
2023-02-16 06:28:52 +00:00
|
|
|
let initialTimeline = timeline
|
2023-12-31 07:11:53 +00:00
|
|
|
var newStatuses: [Status] = await fetchNewPages(minId: latestStatus, maxPages: 5)
|
2023-02-01 11:49:59 +00:00
|
|
|
|
2023-02-01 11:43:11 +00:00
|
|
|
// Dedup statuses, a status with the same id could have been streamed in.
|
2023-09-16 12:15:03 +00:00
|
|
|
let ids = await datasource.get().map(\.id)
|
2023-02-01 11:43:11 +00:00
|
|
|
newStatuses = newStatuses.filter { status in
|
2023-02-18 17:04:46 +00:00
|
|
|
!ids.contains(where: { $0 == status.id })
|
2023-02-01 11:43:11 +00:00
|
|
|
}
|
2023-02-04 16:17:38 +00:00
|
|
|
|
2023-02-01 17:56:06 +00:00
|
|
|
ReblogCache.shared.removeDuplicateReblogs(&newStatuses)
|
2023-03-02 05:42:58 +00:00
|
|
|
StatusDataControllerProvider.shared.updateDataControllers(for: newStatuses, client: client)
|
2023-02-01 17:56:06 +00:00
|
|
|
|
2023-02-01 11:43:11 +00:00
|
|
|
// If no new statuses, resume streaming and exit.
|
|
|
|
guard !newStatuses.isEmpty else {
|
|
|
|
canStreamEvents = true
|
|
|
|
return
|
|
|
|
}
|
2023-02-04 16:17:38 +00:00
|
|
|
|
2023-02-04 15:54:03 +00:00
|
|
|
// 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
|
|
|
|
}
|
2023-02-12 15:29:41 +00:00
|
|
|
|
2023-02-08 17:46:09 +00:00
|
|
|
// Return if task has been cancelled.
|
|
|
|
guard !Task.isCancelled else {
|
|
|
|
canStreamEvents = true
|
|
|
|
return
|
|
|
|
}
|
2023-02-18 06:26:48 +00:00
|
|
|
|
2023-02-16 06:28:52 +00:00
|
|
|
// 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
|
|
|
|
}
|
2023-02-01 11:49:59 +00:00
|
|
|
|
2023-02-01 11:43:11 +00:00
|
|
|
// Keep track of the top most status, so we can scroll back to it after view update.
|
2024-01-04 12:19:36 +00:00
|
|
|
let topStatus = await datasource.getFiltered().first
|
2023-02-01 11:49:59 +00:00
|
|
|
|
2023-02-01 11:43:11 +00:00
|
|
|
// Insert new statuses in internal datasource.
|
2023-02-18 17:04:46 +00:00
|
|
|
await datasource.insert(contentOf: newStatuses, at: 0)
|
2023-02-01 11:49:59 +00:00
|
|
|
|
2024-01-03 10:34:50 +00:00
|
|
|
// Cache statuses for timeline.
|
|
|
|
await cache()
|
2023-02-01 11:49:59 +00:00
|
|
|
|
2023-02-25 18:47:15 +00:00
|
|
|
// Append new statuses in the timeline indicator.
|
2023-09-16 12:15:03 +00:00
|
|
|
pendingStatusesObserver.pendingStatuses.insert(contentsOf: newStatuses.map(\.id), at: 0)
|
2023-02-25 18:47:15 +00:00
|
|
|
|
|
|
|
// 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.
|
2024-01-03 10:34:50 +00:00
|
|
|
if let topStatus, visibileStatuses.contains(where: { $0.id == topStatus.id}), scrollToTopVisible {
|
2023-02-25 18:47:15 +00:00
|
|
|
pendingStatusesObserver.disableUpdate = true
|
2024-01-04 12:19:36 +00:00
|
|
|
let statuses = await datasource.getFiltered()
|
2023-02-25 18:47:15 +00:00
|
|
|
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
|
2023-02-01 11:43:11 +00:00
|
|
|
}
|
|
|
|
} else {
|
2023-02-25 18:47:15 +00:00
|
|
|
// This will keep the scroll position (if the list is scrolled) and prepend statuses on the top.
|
2024-01-04 12:19:36 +00:00
|
|
|
let statuses = await datasource.getFiltered()
|
2023-02-25 18:47:15 +00:00
|
|
|
withAnimation {
|
2023-02-18 17:04:46 +00:00
|
|
|
statusesState = .display(statuses: statuses,
|
|
|
|
nextPageState: statuses.count < 20 ? .none : .hasNextPage)
|
2023-02-25 18:47:15 +00:00
|
|
|
canStreamEvents = true
|
2023-02-01 11:43:11 +00:00
|
|
|
}
|
2023-02-25 18:47:15 +00:00
|
|
|
}
|
2023-02-12 15:29:41 +00:00
|
|
|
|
2023-02-25 18:47:15 +00:00
|
|
|
// We trigger a new fetch so we can get the next new statuses if any.
|
|
|
|
// If none, it'll stop there.
|
2023-04-09 13:11:02 +00:00
|
|
|
// Only do that in the context of the home timeline as other don't worth catching up that much.
|
|
|
|
if timeline == .home,
|
2023-07-19 05:46:25 +00:00
|
|
|
!Task.isCancelled,
|
|
|
|
let latest = await datasource.get().first
|
|
|
|
{
|
2024-01-04 11:56:46 +00:00
|
|
|
pendingStatusesObserver.isLoadingNewStatuses = true
|
2023-12-27 12:26:30 +00:00
|
|
|
try await fetchNewPagesFrom(latestStatus: latest.id, client: client)
|
2023-02-01 11:43:11 +00:00
|
|
|
}
|
|
|
|
}
|
2023-02-01 11:49:59 +00:00
|
|
|
|
2023-02-01 11:43:11 +00:00
|
|
|
private func fetchNewPages(minId: String, maxPages: Int) async -> [Status] {
|
2022-12-28 18:10:13 +00:00
|
|
|
guard let client else { return [] }
|
|
|
|
var pagesLoaded = 0
|
|
|
|
var allStatuses: [Status] = []
|
|
|
|
var latestMinId = minId
|
|
|
|
do {
|
2023-02-26 18:09:21 +00:00
|
|
|
while
|
|
|
|
!Task.isCancelled,
|
|
|
|
var newStatuses: [Status] =
|
2023-02-21 06:23:42 +00:00
|
|
|
try await client.get(endpoint: timeline.endpoint(sinceId: nil,
|
|
|
|
maxId: nil,
|
|
|
|
minId: latestMinId,
|
|
|
|
offset: datasource.get().count)),
|
2023-01-17 10:36:01 +00:00
|
|
|
!newStatuses.isEmpty,
|
|
|
|
pagesLoaded < maxPages
|
|
|
|
{
|
2022-12-28 18:10:13 +00:00
|
|
|
pagesLoaded += 1
|
2023-02-01 17:56:06 +00:00
|
|
|
|
|
|
|
ReblogCache.shared.removeDuplicateReblogs(&newStatuses)
|
2023-03-02 05:42:58 +00:00
|
|
|
StatusDataControllerProvider.shared.updateDataControllers(for: newStatuses, client: client)
|
2023-02-04 16:17:38 +00:00
|
|
|
|
2022-12-28 18:10:13 +00:00
|
|
|
allStatuses.insert(contentsOf: newStatuses, at: 0)
|
|
|
|
latestMinId = newStatuses.first?.id ?? ""
|
|
|
|
}
|
|
|
|
} catch {
|
2023-01-05 05:39:23 +00:00
|
|
|
return allStatuses
|
2022-12-28 18:10:13 +00:00
|
|
|
}
|
|
|
|
return allStatuses
|
|
|
|
}
|
2023-02-01 11:49:59 +00:00
|
|
|
|
2022-12-19 06:17:01 +00:00
|
|
|
func fetchNextPage() async {
|
2022-12-19 11:28:55 +00:00
|
|
|
guard let client else { return }
|
2022-11-25 11:00:01 +00:00
|
|
|
do {
|
2024-01-04 12:19:36 +00:00
|
|
|
let statuses = await datasource.get()
|
|
|
|
guard let lastId = statuses.last?.id else { return }
|
|
|
|
statusesState = await .display(statuses: datasource.getFiltered(), nextPageState: .loadingNextPage)
|
2023-02-01 17:56:06 +00:00
|
|
|
var newStatuses: [Status] = try await client.get(endpoint: timeline.endpoint(sinceId: nil,
|
2022-12-28 18:10:13 +00:00
|
|
|
maxId: lastId,
|
2023-01-01 13:28:15 +00:00
|
|
|
minId: nil,
|
2024-01-04 12:19:36 +00:00
|
|
|
offset: statuses.count))
|
2023-02-01 17:56:06 +00:00
|
|
|
|
2023-02-02 05:39:03 +00:00
|
|
|
ReblogCache.shared.removeDuplicateReblogs(&newStatuses)
|
2023-02-01 17:56:06 +00:00
|
|
|
|
2023-02-18 17:04:46 +00:00
|
|
|
await datasource.append(contentOf: newStatuses)
|
2023-03-02 05:42:58 +00:00
|
|
|
StatusDataControllerProvider.shared.updateDataControllers(for: newStatuses, client: client)
|
2023-02-02 05:39:03 +00:00
|
|
|
|
2024-01-04 12:19:36 +00:00
|
|
|
statusesState = await .display(statuses: datasource.getFiltered(),
|
2023-02-18 17:04:46 +00:00
|
|
|
nextPageState: newStatuses.count < 20 ? .none : .hasNextPage)
|
2022-11-25 11:00:01 +00:00
|
|
|
} catch {
|
2022-12-19 06:17:01 +00:00
|
|
|
statusesState = .error(error: error)
|
2022-11-25 11:00:01 +00:00
|
|
|
}
|
|
|
|
}
|
2023-02-01 11:49:59 +00:00
|
|
|
|
2023-01-31 07:04:35 +00:00
|
|
|
func statusDidAppear(status: Status) {
|
2023-01-31 08:01:26 +00:00
|
|
|
pendingStatusesObserver.removeStatus(status: status)
|
2024-01-03 10:34:50 +00:00
|
|
|
visibileStatuses.insert(status, at: 0)
|
|
|
|
|
|
|
|
if let client, timeline.supportNewestPagination {
|
2023-02-04 13:42:10 +00:00
|
|
|
Task {
|
2024-01-03 10:34:50 +00:00
|
|
|
await cache.setLatestSeenStatuses(visibileStatuses, for: client, filter: timeline.id)
|
2023-02-04 13:42:10 +00:00
|
|
|
}
|
|
|
|
}
|
2023-01-31 11:17:35 +00:00
|
|
|
}
|
2023-02-01 11:49:59 +00:00
|
|
|
|
2023-01-31 11:17:35 +00:00
|
|
|
func statusDidDisappear(status: Status) {
|
2024-01-03 10:34:50 +00:00
|
|
|
visibileStatuses.removeAll(where: { $0.id == status.id })
|
2023-01-05 06:07:28 +00:00
|
|
|
}
|
2022-11-25 11:00:01 +00:00
|
|
|
}
|
2023-12-27 12:26:30 +00:00
|
|
|
|
|
|
|
// MARK: - MARKER
|
|
|
|
extension TimelineViewModel {
|
|
|
|
func fetchMarker() async -> Marker.Content? {
|
|
|
|
guard let client else {
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
do {
|
|
|
|
let data: Marker = try await client.get(endpoint: Markers.markers)
|
|
|
|
return data.home
|
|
|
|
} catch {
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
func saveMarker() {
|
|
|
|
guard timeline == .home, let client else { return }
|
|
|
|
Task {
|
2024-01-03 10:34:50 +00:00
|
|
|
guard let id = await cache.getLatestSeenStatus(for: client, filter: timeline.id)?.first else { return }
|
2023-12-27 12:26:30 +00:00
|
|
|
do {
|
|
|
|
let _: Marker = try await client.post(endpoint: Markers.markHome(lastReadId: id))
|
|
|
|
} catch { }
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|