IceCubesApp/Packages/Timeline/Sources/Timeline/TimelineViewModel.swift

188 lines
6 KiB
Swift
Raw Normal View History

2022-11-25 11:00:01 +00:00
import SwiftUI
import Network
2022-11-29 08:28:17 +00:00
import Models
2022-12-19 06:17:01 +00:00
import Status
2022-12-25 18:18:19 +00:00
import Env
2022-11-25 11:00:01 +00:00
@MainActor
2022-12-19 06:17:01 +00:00
class TimelineViewModel: ObservableObject, StatusesFetcher {
2022-12-30 07:36:22 +00:00
var client: Client? {
didSet {
if oldValue != client {
statuses = []
}
}
}
2022-12-01 08:05:26 +00:00
// Internal source of truth for a timeline.
2022-11-25 11:00:01 +00:00
private var statuses: [Status] = []
2022-12-19 06:17:01 +00:00
@Published var statusesState: StatusesState = .loading
2022-12-01 08:05:26 +00:00
@Published var timeline: TimelineFilter = .pub {
didSet {
2022-12-25 07:17:16 +00:00
Task {
if oldValue != timeline {
statuses = []
2022-12-26 07:24:55 +00:00
pendingStatuses = []
}
await fetchStatuses(userIntent: 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
}
}
}
}
2022-12-21 11:39:29 +00:00
@Published var tag: Tag?
enum PendingStatusesState {
case refresh, stream
}
2022-12-25 17:43:15 +00:00
@Published var pendingStatuses: [Status] = []
@Published var pendingStatusesState: PendingStatusesState = .stream
var pendingStatusesButtonTitle: String {
switch pendingStatusesState {
case .stream:
return "\(pendingStatuses.count) new posts"
case .refresh:
return "See new posts"
}
}
var pendingStatusesEnabled: Bool {
timeline == .home
}
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
}
2022-11-29 11:18:06 +00:00
2022-12-19 06:17:01 +00:00
func fetchStatuses() async {
await fetchStatuses(userIntent: false)
}
func fetchStatuses(userIntent: Bool) async {
2022-12-19 11:28:55 +00:00
guard let client else { return }
2022-11-25 11:00:01 +00:00
do {
if statuses.isEmpty {
2022-12-26 07:24:55 +00:00
pendingStatuses = []
statusesState = .loading
statuses = try await client.get(endpoint: timeline.endpoint(sinceId: nil, maxId: nil, minId: nil))
2022-12-30 07:36:22 +00:00
statusesState = .display(statuses: statuses, nextPageState: statuses.count < 20 ? .none : .hasNextPage)
} else if let first = statuses.first {
var newStatuses: [Status] = await fetchNewPages(minId: first.id, maxPages: 10)
if userIntent || !pendingStatusesEnabled {
2022-12-26 07:24:55 +00:00
pendingStatuses = []
statuses.insert(contentsOf: newStatuses, at: 0)
withAnimation {
2022-12-30 07:36:22 +00:00
statusesState = .display(statuses: statuses, nextPageState: statuses.count < 20 ? .none : .hasNextPage)
}
} else {
2022-12-26 07:24:55 +00:00
newStatuses = newStatuses.filter { status in
!pendingStatuses.contains(where: { $0.id == status.id })
}
pendingStatuses.insert(contentsOf: newStatuses, at: 0)
pendingStatusesState = .refresh
}
}
2022-11-25 11:00:01 +00:00
} catch {
2022-12-19 06:17:01 +00:00
statusesState = .error(error: error)
2022-12-21 11:39:29 +00:00
print("timeline parse error: \(error)")
2022-11-25 11:00:01 +00:00
}
}
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)),
!newStatuses.isEmpty,
pagesLoaded < maxPages {
pagesLoaded += 1
allStatuses.insert(contentsOf: newStatuses, at: 0)
latestMinId = newStatuses.first?.id ?? ""
}
} catch {
return []
}
return allStatuses
}
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 {
guard let lastId = statuses.last?.id else { return }
2022-12-19 06:17:01 +00:00
statusesState = .display(statuses: statuses, nextPageState: .loadingNextPage)
let newStatuses: [Status] = try await client.get(endpoint: timeline.endpoint(sinceId: nil,
maxId: lastId,
minId: nil))
2022-11-25 11:00:01 +00:00
statuses.append(contentsOf: newStatuses)
2022-12-19 06:17:01 +00:00
statusesState = .display(statuses: statuses, nextPageState: .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
}
}
2022-12-21 11:39:29 +00:00
func fetchTag(id: String) async {
guard let client else { return }
do {
tag = try await client.get(endpoint: Tags.tag(id: id))
} catch {}
}
func followTag(id: String) async {
guard let client else { return }
do {
tag = try await client.post(endpoint: Tags.follow(id: id))
} catch {}
}
func unfollowTag(id: String) async {
guard let client else { return }
do {
tag = try await client.post(endpoint: Tags.unfollow(id: id))
} catch {}
}
2022-12-25 18:18:19 +00:00
func handleEvent(event: any StreamEvent, currentAccount: CurrentAccount) {
if let event = event as? StreamEventUpdate {
if event.status.account.id == currentAccount.account?.id,
timeline == .home {
2022-12-25 18:18:19 +00:00
statuses.insert(event.status, at: 0)
statusesState = .display(statuses: statuses, nextPageState: .hasNextPage)
} else if pendingStatusesEnabled,
2022-12-27 12:49:54 +00:00
!statuses.contains(where: { $0.id == event.status.id }),
!pendingStatuses.contains(where: { $0.id == event.status.id }){
2022-12-25 18:18:19 +00:00
pendingStatuses.insert(event.status, at: 0)
pendingStatusesState = .stream
2022-12-25 18:18:19 +00:00
}
} else if let event = event as? StreamEventDelete {
statuses.removeAll(where: { $0.id == event.status })
statusesState = .display(statuses: statuses, nextPageState: .hasNextPage)
} else if let event = event as? StreamEventStatusUpdate {
if let originalIndex = statuses.firstIndex(where: { $0.id == event.status.id }) {
statuses[originalIndex] = event.status
statusesState = .display(statuses: statuses, nextPageState: .hasNextPage)
}
}
}
2022-12-25 17:43:15 +00:00
func displayPendingStatuses() {
2022-12-25 17:48:50 +00:00
guard timeline == .home else { return }
pendingStatuses = pendingStatuses.filter { status in
!statuses.contains(where: { $0.id == status.id })
}
2022-12-25 17:43:15 +00:00
statuses.insert(contentsOf: pendingStatuses, at: 0)
pendingStatuses = []
statusesState = .display(statuses: statuses, nextPageState: .hasNextPage)
}
2022-11-25 11:00:01 +00:00
}