1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
import Foundation
@Observable
@MainActor
final class FeedViewModel {
var galleries: [GrainGallery] = []
var isLoading = false
var error: Error?
/// Set to `true` after the first network fetch completes (success or failure).
/// Used by FeedTabContent to always run a fresh fetch on first appear, even when
/// galleries are pre-populated from the disk cache.
var hasFetchedInitial = false
private var cursor: String?
private var hasMore = true
private var loadTask: Task<Void, Never>?
private let client: XRPCClient
private let feedName: String
private let actor: String?
private let camera: String?
private let location: String?
/// The place's display name, sent with `location` so the feed covers every
/// cell the index folded into it.
private let locationName: String?
private let tag: String?
private let cacheKey: String?
init(
client: XRPCClient,
feedName: String,
actor: String? = nil,
camera: String? = nil,
location: String? = nil,
locationName: String? = nil,
tag: String? = nil,
cacheKey: String? = nil
) {
self.client = client
self.feedName = feedName
self.actor = actor
self.camera = camera
self.location = location
self.locationName = locationName
self.tag = tag
self.cacheKey = cacheKey
if let cacheKey {
galleries = FeedCache.shared.load(key: cacheKey)
}
}
convenience init(client: XRPCClient, pinnedFeed: PinnedFeed, userDID: String? = nil) {
self.init(
client: client,
feedName: pinnedFeed.feedName,
actor: (pinnedFeed.id == "following" || pinnedFeed.id == "foryou") ? userDID : nil,
camera: pinnedFeed.type == "camera" ? pinnedFeed.feedValue : nil,
location: pinnedFeed.type == "location" ? pinnedFeed.feedValue : nil,
locationName: pinnedFeed.type == "location" ? pinnedFeed.label : nil,
tag: pinnedFeed.type == "hashtag" ? pinnedFeed.feedValue : nil,
cacheKey: pinnedFeed.id
)
}
func loadInitial(auth: AuthContext? = nil) async {
loadTask?.cancel()
isLoading = true
error = nil
cursor = nil
hasMore = true
let task = Task {
do {
let response = try await client.getFeed(
feed: feedName,
actor: actor,
camera: camera,
location: location,
name: locationName,
tag: tag,
auth: auth
)
guard !Task.isCancelled else { return }
galleries = response.items ?? []
cursor = response.cursor
hasMore = response.cursor != nil
if let key = cacheKey {
let toCache = galleries
// Captured now, not inside the detached task: this feed
// belongs to whoever was signed in when it was fetched.
let owner = AccountScopedStorage.activeAccountID
Task.detached(priority: .utility) {
FeedCache.shared.save(toCache, key: key, did: owner)
}
}
} catch {
guard !Task.isCancelled else { return }
self.error = error
}
isLoading = false
hasFetchedInitial = true
}
loadTask = task
await task.value
}
func loadMore(auth: AuthContext? = nil) async {
guard !isLoading, hasMore, let cursor else { return }
isLoading = true
do {
let response = try await client.getFeed(
feed: feedName,
cursor: cursor,
actor: actor,
camera: camera,
location: location,
name: locationName,
auth: auth
)
galleries.append(contentsOf: response.items ?? [])
self.cursor = response.cursor
hasMore = response.cursor != nil
} catch {
self.error = error
}
isLoading = false
}
}