iOS client for Grain grain.social
ios photography atproto
7
fork

Configure Feed

Select the types of activity you want to include in your feed.

fix: stale feed data on refresh and viewer data loss after token expiry

Disable URL response caching on XRPC GET requests so pull-to-refresh
always hits the server. Make authContext() async to await token refresh
before returning, preventing requests with expired tokens that return
200 with null viewer data instead of triggering 401 retry.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

+71 -69
+4 -3
Grain/API/AuthManager.swift
··· 217 217 218 218 /// Build an AuthContext for making authenticated requests. 219 219 /// Proactively refreshes if the token expires within 60 seconds. 220 - func authContext() -> AuthContext? { 221 - guard let dpop, let token = TokenStorage.accessToken else { return nil } 220 + func authContext() async -> AuthContext? { 221 + guard let dpop else { return nil } 222 222 if let expiresAt = TokenStorage.tokenExpiresAt, expiresAt.timeIntervalSinceNow < 60 { 223 - Task { try? await refresh() } 223 + try? await refresh() 224 224 } 225 + guard let token = TokenStorage.accessToken else { return nil } 225 226 return AuthContext(accessToken: token, dpop: dpop) 226 227 } 227 228
+3 -3
Grain/API/PushManager.swift
··· 59 59 /// Must be called while auth context is still valid (before token storage is cleared). 60 60 func unregisterToken() { 61 61 guard let token = currentToken, 62 - let authManager, 63 - let auth = authManager.authContext() else { return } 62 + let authManager else { return } 64 63 let client = authManager.makeClient() 65 64 currentToken = nil 66 65 Task { 66 + guard let auth = await authManager.authContext() else { return } 67 67 do { 68 68 try await client.procedure( 69 69 "dev.hatk.push.unregisterToken", ··· 87 87 private func sendTokenToServer(token: String) async { 88 88 currentToken = token 89 89 90 - guard let authManager, let auth = authManager.authContext() else { 90 + guard let authManager, let auth = await authManager.authContext() else { 91 91 logger.warning("No auth context, skipping token registration") 92 92 return 93 93 }
+1
Grain/API/XRPCClient.swift
··· 52 52 53 53 var request = URLRequest(url: url) 54 54 request.httpMethod = "GET" 55 + request.cachePolicy = .reloadIgnoringLocalCacheData 55 56 56 57 return try await executeWithRetry(request, auth: auth, as: type) 57 58 }
+1 -1
Grain/Views/Components/GalleryCardView.swift
··· 440 440 } 441 441 442 442 private func toggleFavorite() async { 443 - guard let authContext = auth.authContext() else { 443 + guard let authContext = await auth.authContext() else { 444 444 logger.error("No auth context") 445 445 return 446 446 }
+2 -2
Grain/Views/Components/ReportView.swift
··· 80 80 81 81 private func loadLabels() async { 82 82 do { 83 - labelDefs = try await client.describeLabels(auth: auth.authContext()) 83 + labelDefs = try await client.describeLabels(auth: await auth.authContext()) 84 84 if let first = labelDefs.first { 85 85 selectedLabel = first.identifier 86 86 } ··· 99 99 subjectCid: subjectCid, 100 100 label: selectedLabel, 101 101 reason: reason.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty ? nil : reason.trimmingCharacters(in: .whitespacesAndNewlines), 102 - auth: auth.authContext() 102 + auth: await auth.authContext() 103 103 ) 104 104 isSubmitted = true 105 105 } catch {
+1 -1
Grain/Views/Components/SuggestedFollowsView.swift
··· 80 80 } 81 81 82 82 private func followUser(_ item: SuggestedItem) { 83 - guard let authContext = auth.authContext() else { return } 84 83 followingInProgress.insert(item.did) 85 84 Task { 85 + guard let authContext = await auth.authContext() else { return } 86 86 let record = AnyCodable([ 87 87 "subject": item.did, 88 88 "createdAt": DateFormatting.nowISO()
+2 -2
Grain/Views/Create/CreateGalleryView.swift
··· 182 182 .ignoresSafeArea() 183 183 } 184 184 .task { 185 - if let authContext = auth.authContext() { 185 + if let authContext = await auth.authContext() { 186 186 if let prefs = try? await client.getPreferences(auth: authContext).preferences { 187 187 if let exif = prefs.includeExif { 188 188 includeExif = exif ··· 273 273 // MARK: - Create Gallery 274 274 275 275 private func createGallery() async { 276 - guard let authContext = auth.authContext(), let repo = auth.userDID else { return } 276 + guard let authContext = await auth.authContext(), let repo = auth.userDID else { return } 277 277 isUploading = true 278 278 errorMessage = nil 279 279
+12 -12
Grain/Views/Feed/FeedView.swift
··· 41 41 }, 42 42 onStoryCreateTap: { showStoryCreate = true }, 43 43 onRefresh: { [storyStatusCache] in 44 - await storyViewModel.load(auth: auth.authContext(), storyStatusCache: storyStatusCache) 44 + await storyViewModel.load(auth: await auth.authContext(), storyStatusCache: storyStatusCache) 45 45 }, 46 46 prefsViewModel: prefsViewModel 47 47 ) ··· 59 59 .sharedBackgroundVisibility(.hidden) 60 60 } 61 61 .task { 62 - await prefsViewModel.loadIfNeeded(auth: auth.authContext()) 63 - await storyViewModel.load(auth: auth.authContext(), storyStatusCache: storyStatusCache) 62 + await prefsViewModel.loadIfNeeded(auth: await auth.authContext()) 63 + await storyViewModel.load(auth: await auth.authContext(), storyStatusCache: storyStatusCache) 64 64 } 65 65 .onAppear { 66 - Task { await prefsViewModel.refresh(auth: auth.authContext()) } 66 + Task { await prefsViewModel.refresh(auth: await auth.authContext()) } 67 67 } 68 68 .onChange(of: storyViewerDid) { 69 69 if storyViewerDid == nil { ··· 93 93 } 94 94 .sheet(isPresented: $showStoryCreate) { 95 95 StoryCreateView(client: client) { 96 - Task { await storyViewModel.load(auth: auth.authContext(), storyStatusCache: storyStatusCache) } 96 + Task { await storyViewModel.load(auth: await auth.authContext(), storyStatusCache: storyStatusCache) } 97 97 } 98 98 } 99 99 .navigationDestination(item: $deepLinkProfileDid) { did in ··· 142 142 Divider() 143 143 Button(role: .destructive) { 144 144 Task { 145 - await prefsViewModel.unpinFeed(prefsViewModel.selectedFeedId, auth: auth.authContext()) 145 + await prefsViewModel.unpinFeed(prefsViewModel.selectedFeedId, auth: await auth.authContext()) 146 146 } 147 147 } label: { 148 148 Label("Unpin", systemImage: "pin.slash") ··· 195 195 196 196 private func openStoryDeepLink(did: String) async { 197 197 do { 198 - let response = try await client.getStories(actor: did, auth: auth.authContext()) 198 + let response = try await client.getStories(actor: did, auth: await auth.authContext()) 199 199 let count = response.stories.count 200 200 if count > 0, let creator = response.stories.first?.creator { 201 201 deepLinkStoryAuthor = GrainStoryAuthor( ··· 273 273 }) 274 274 .onAppear { 275 275 if gallery.id == viewModel.galleries.last?.id { 276 - Task { await viewModel.loadMore(auth: auth.authContext()) } 276 + Task { await viewModel.loadMore(auth: await auth.authContext()) } 277 277 } 278 278 } 279 279 ··· 293 293 .environment(zoomState) 294 294 .modifier(ImageZoomOverlay(zoomState: zoomState)) 295 295 .refreshable { 296 - let auth = auth.authContext() 296 + let auth = await auth.authContext() 297 297 async let feed: () = viewModel.loadInitial(auth: auth) 298 298 async let stories: ()? = onRefresh?() 299 299 async let prefs: () = prefsViewModel.refresh(auth: auth) ··· 326 326 } 327 327 .task { 328 328 if viewModel.galleries.isEmpty { 329 - await viewModel.loadInitial(auth: auth.authContext()) 329 + await viewModel.loadInitial(auth: await auth.authContext()) 330 330 lastLoadTime = .now 331 331 } 332 332 if !suggestedLoaded, let did = auth.userDID { 333 333 do { 334 - let response = try await client.getSuggestedFollows(actor: did, auth: auth.authContext()) 334 + let response = try await client.getSuggestedFollows(actor: did, auth: await auth.authContext()) 335 335 suggestedFollows = response.items ?? [] 336 336 } catch {} 337 337 suggestedLoaded = true ··· 340 340 .onChange(of: scenePhase) { 341 341 if scenePhase == .active, Date.now.timeIntervalSince(lastLoadTime) > 300 { 342 342 Task { 343 - await viewModel.loadInitial(auth: auth.authContext()) 343 + await viewModel.loadInitial(auth: await auth.authContext()) 344 344 lastLoadTime = .now 345 345 } 346 346 }
+5 -5
Grain/Views/Feed/HashtagFeedView.swift
··· 103 103 private func loadInitial() async { 104 104 isLoading = true 105 105 do { 106 - let response = try await client.getFeed(feed: "hashtag", tag: tag, auth: auth.authContext()) 106 + let response = try await client.getFeed(feed: "hashtag", tag: tag, auth: await auth.authContext()) 107 107 galleries = response.items ?? [] 108 108 cursor = response.cursor 109 109 } catch {} ··· 114 114 guard !isLoading, let cursor else { return } 115 115 isLoading = true 116 116 do { 117 - let response = try await client.getFeed(feed: "hashtag", cursor: cursor, tag: tag, auth: auth.authContext()) 117 + let response = try await client.getFeed(feed: "hashtag", cursor: cursor, tag: tag, auth: await auth.authContext()) 118 118 galleries.append(contentsOf: response.items ?? []) 119 119 self.cursor = response.cursor 120 120 } catch {} ··· 123 123 124 124 private func checkPinned() async { 125 125 do { 126 - let response = try await client.getPreferences(auth: auth.authContext()) 126 + let response = try await client.getPreferences(auth: await auth.authContext()) 127 127 isPinned = response.preferences.pinnedFeeds?.contains(where: { $0.id == feedId }) ?? false 128 128 } catch {} 129 129 } 130 130 131 131 private func togglePin() async { 132 132 do { 133 - let response = try await client.getPreferences(auth: auth.authContext()) 133 + let response = try await client.getPreferences(auth: await auth.authContext()) 134 134 var feeds = response.preferences.pinnedFeeds ?? PinnedFeed.defaults 135 135 if isPinned { 136 136 feeds.removeAll { $0.id == feedId } 137 137 } else { 138 138 feeds.append(PinnedFeed(id: feedId, label: tag, type: "hashtag", path: "/hashtags/\(tag)")) 139 139 } 140 - try await client.putPinnedFeeds(feeds, auth: auth.authContext()) 140 + try await client.putPinnedFeeds(feeds, auth: await auth.authContext()) 141 141 isPinned.toggle() 142 142 } catch {} 143 143 }
+5 -5
Grain/Views/Feed/LocationFeedView.swift
··· 131 131 private func loadInitial() async { 132 132 isLoading = true 133 133 do { 134 - let response = try await client.getFeed(feed: "location", location: h3Index, auth: auth.authContext()) 134 + let response = try await client.getFeed(feed: "location", location: h3Index, auth: await auth.authContext()) 135 135 galleries = response.items ?? [] 136 136 cursor = response.cursor 137 137 } catch {} ··· 142 142 guard !isLoading, let cursor else { return } 143 143 isLoading = true 144 144 do { 145 - let response = try await client.getFeed(feed: "location", cursor: cursor, location: h3Index, auth: auth.authContext()) 145 + let response = try await client.getFeed(feed: "location", cursor: cursor, location: h3Index, auth: await auth.authContext()) 146 146 galleries.append(contentsOf: response.items ?? []) 147 147 self.cursor = response.cursor 148 148 } catch {} ··· 151 151 152 152 private func checkPinned() async { 153 153 do { 154 - let response = try await client.getPreferences(auth: auth.authContext()) 154 + let response = try await client.getPreferences(auth: await auth.authContext()) 155 155 isPinned = response.preferences.pinnedFeeds?.contains(where: { $0.id == feedId }) ?? false 156 156 } catch {} 157 157 } 158 158 159 159 private func togglePin() async { 160 160 do { 161 - let response = try await client.getPreferences(auth: auth.authContext()) 161 + let response = try await client.getPreferences(auth: await auth.authContext()) 162 162 var feeds = response.preferences.pinnedFeeds ?? PinnedFeed.defaults 163 163 if isPinned { 164 164 feeds.removeAll { $0.id == feedId } 165 165 } else { 166 166 feeds.append(PinnedFeed(id: feedId, label: locationName, type: "location", path: "/location/\(h3Index)")) 167 167 } 168 - try await client.putPinnedFeeds(feeds, auth: auth.authContext()) 168 + try await client.putPinnedFeeds(feeds, auth: await auth.authContext()) 169 169 isPinned.toggle() 170 170 } catch {} 171 171 }
+4 -4
Grain/Views/Gallery/GalleryDetailView.swift
··· 244 244 } 245 245 } 246 246 .task { 247 - await viewModel.load(uri: galleryUri, auth: auth.authContext()) 247 + await viewModel.load(uri: galleryUri, auth: await auth.authContext()) 248 248 } 249 249 } 250 250 ··· 262 262 263 263 private func postComment() async { 264 264 let text = commentText.trimmingCharacters(in: .whitespacesAndNewlines) 265 - guard !text.isEmpty, let authContext = auth.authContext() else { return } 265 + guard !text.isEmpty, let authContext = await auth.authContext() else { return } 266 266 267 267 isPostingComment = true 268 268 var recordDict: [String: String] = [ ··· 288 288 } 289 289 290 290 private func deleteGallery() async { 291 - guard let authContext = auth.authContext() else { return } 291 + guard let authContext = await auth.authContext() else { return } 292 292 let rkey = galleryUri.split(separator: "/").last.map(String.init) ?? "" 293 293 do { 294 294 try await client.deleteGallery(rkey: rkey, auth: authContext) ··· 300 300 } 301 301 302 302 private func deleteComment(_ comment: GrainComment) async { 303 - guard let authContext = auth.authContext() else { return } 303 + guard let authContext = await auth.authContext() else { return } 304 304 let rkey = comment.uri.split(separator: "/").last.map(String.init) ?? "" 305 305 do { 306 306 try await client.deleteRecord(collection: "social.grain.comment", rkey: rkey, auth: authContext)
+4 -4
Grain/Views/MainTabView.swift
··· 78 78 if let uiImage = auth.avatarImage { 79 79 avatarTabImage = circularAvatar(uiImage, size: 26) 80 80 } 81 - await notificationsVM.fetchUnseenCount(auth: auth.authContext()) 82 - await labelDefsCache.loadIfNeeded(client: c, auth: auth.authContext()) 81 + await notificationsVM.fetchUnseenCount(auth: await auth.authContext()) 82 + await labelDefsCache.loadIfNeeded(client: c, auth: await auth.authContext()) 83 83 } 84 84 .onChange(of: auth.avatarImage) { 85 85 if let uiImage = auth.avatarImage { ··· 95 95 if scenePhase == .active { 96 96 Task { 97 97 try? await auth.refreshIfNeeded() 98 - await notificationsVM.fetchUnseenCount(auth: auth.authContext()) 99 - await labelDefsCache.loadIfNeeded(client: client, auth: auth.authContext()) 98 + await notificationsVM.fetchUnseenCount(auth: await auth.authContext()) 99 + await labelDefsCache.loadIfNeeded(client: client, auth: await auth.authContext()) 100 100 } 101 101 } 102 102 }
+4 -4
Grain/Views/Notifications/NotificationsView.swift
··· 40 40 } 41 41 .onAppear { 42 42 if notification.id == viewModel.notifications.last?.id { 43 - Task { await viewModel.loadMore(auth: auth.authContext()) } 43 + Task { await viewModel.loadMore(auth: await auth.authContext()) } 44 44 } 45 45 } 46 46 } ··· 55 55 } 56 56 .listStyle(.plain) 57 57 .refreshable { 58 - await viewModel.loadInitial(auth: auth.authContext()) 58 + await viewModel.loadInitial(auth: await auth.authContext()) 59 59 } 60 60 .navigationTitle("Notifications") 61 61 .navigationDestination(item: $selectedGalleryUri) { uri in ··· 78 78 } 79 79 .task(id: viewModel.unseenCount) { 80 80 if viewModel.notifications.isEmpty || viewModel.unseenCount > 0 { 81 - await viewModel.loadInitial(auth: auth.authContext()) 81 + await viewModel.loadInitial(auth: await auth.authContext()) 82 82 } 83 - await viewModel.markAsSeen(auth: auth.authContext()) 83 + await viewModel.markAsSeen(auth: await auth.authContext()) 84 84 } 85 85 } 86 86 }
+6 -6
Grain/Views/Profile/FollowListView.swift
··· 147 147 do { 148 148 switch mode { 149 149 case .followers: 150 - let response = try await client.getFollowers(actor: did, viewer: auth.userDID, cursor: nil, auth: auth.authContext()) 150 + let response = try await client.getFollowers(actor: did, viewer: auth.userDID, cursor: nil, auth: await auth.authContext()) 151 151 items = (response.items ?? []).map { FollowListItem(from: $0) } 152 152 cursor = response.cursor 153 153 totalCount = response.totalCount 154 154 case .following: 155 - let response = try await client.getFollowing(actor: did, viewer: auth.userDID, cursor: nil, auth: auth.authContext()) 155 + let response = try await client.getFollowing(actor: did, viewer: auth.userDID, cursor: nil, auth: await auth.authContext()) 156 156 items = (response.items ?? []).map { FollowListItem(from: $0) } 157 157 cursor = response.cursor 158 158 totalCount = response.totalCount 159 159 case .knownFollowers: 160 160 if let viewer = auth.userDID { 161 - let response = try await client.getKnownFollowers(actor: did, viewer: viewer, auth: auth.authContext()) 161 + let response = try await client.getKnownFollowers(actor: did, viewer: viewer, auth: await auth.authContext()) 162 162 items = (response.items ?? []).map { FollowListItem(from: $0) } 163 163 totalCount = items.count 164 164 } ··· 179 179 let existingIDs = Set(items.map(\.id)) 180 180 switch mode { 181 181 case .followers: 182 - let response = try await client.getFollowers(actor: did, viewer: auth.userDID, cursor: cursor, auth: auth.authContext()) 182 + let response = try await client.getFollowers(actor: did, viewer: auth.userDID, cursor: cursor, auth: await auth.authContext()) 183 183 if let newItems = response.items { 184 184 let filtered = newItems.filter { !existingIDs.contains($0.did) } 185 185 items.append(contentsOf: filtered.map { FollowListItem(from: $0) }) 186 186 } 187 187 cursor = response.cursor 188 188 case .following: 189 - let response = try await client.getFollowing(actor: did, viewer: auth.userDID, cursor: cursor, auth: auth.authContext()) 189 + let response = try await client.getFollowing(actor: did, viewer: auth.userDID, cursor: cursor, auth: await auth.authContext()) 190 190 if let newItems = response.items { 191 191 let filtered = newItems.filter { !existingIDs.contains($0.did) } 192 192 items.append(contentsOf: filtered.map { FollowListItem(from: $0) }) ··· 201 201 } 202 202 203 203 private func toggleFollow(for targetDid: String) async { 204 - guard let authContext = auth.authContext(), let repo = auth.userDID else { return } 204 + guard let authContext = await auth.authContext(), let repo = auth.userDID else { return } 205 205 guard let index = items.firstIndex(where: { $0.did == targetDid }) else { return } 206 206 let item = items[index] 207 207
+6 -6
Grain/Views/Profile/ProfileView.swift
··· 221 221 .buttonStyle(.plain) 222 222 .onAppear { 223 223 if gallery.id == viewModel.galleries.last?.id { 224 - Task { await viewModel.loadMoreGalleries(did: did, auth: auth.authContext()) } 224 + Task { await viewModel.loadMoreGalleries(did: did, auth: await auth.authContext()) } 225 225 } 226 226 } 227 227 } ··· 260 260 ToolbarItem(placement: .topBarTrailing) { 261 261 NavigationLink { 262 262 SettingsView(client: client, onProfileEdited: { 263 - Task { await viewModel.load(did: actor, viewer: auth.userDID, auth: auth.authContext()) } 263 + Task { await viewModel.load(did: actor, viewer: auth.userDID, auth: await auth.authContext()) } 264 264 }) 265 265 } label: { 266 266 Image(systemName: "gearshape") ··· 318 318 } 319 319 .background(Color(.systemBackground)) 320 320 .refreshable { 321 - await viewModel.load(did: actor, viewer: auth.userDID, auth: auth.authContext()) 321 + await viewModel.load(did: actor, viewer: auth.userDID, auth: await auth.authContext()) 322 322 } 323 323 .task { 324 - await viewModel.load(did: actor, viewer: auth.userDID, auth: auth.authContext()) 324 + await viewModel.load(did: actor, viewer: auth.userDID, auth: await auth.authContext()) 325 325 } 326 326 .onChange(of: deletedGalleryUri) { _, uri in 327 327 if let uri { ··· 379 379 private func followButton(profile: GrainProfileDetailed) -> some View { 380 380 if profile.viewer?.following != nil { 381 381 Button { 382 - Task { await viewModel.toggleFollow(auth: auth.authContext()) } 382 + Task { await viewModel.toggleFollow(auth: await auth.authContext()) } 383 383 } label: { 384 384 Text("Following") 385 385 .font(.subheadline.weight(.semibold)) ··· 389 389 .tint(.primary) 390 390 } else { 391 391 Button { 392 - Task { await viewModel.toggleFollow(auth: auth.authContext()) } 392 + Task { await viewModel.toggleFollow(auth: await auth.authContext()) } 393 393 } label: { 394 394 Text("Follow") 395 395 .font(.subheadline.weight(.semibold))
+3 -3
Grain/Views/Search/SearchView.swift
··· 90 90 } 91 91 .onSubmit(of: .search) { 92 92 recentSearches.addTextSearch(searchText) 93 - Task { await viewModel.search(auth: auth.authContext()) } 93 + Task { await viewModel.search(auth: await auth.authContext()) } 94 94 } 95 95 .onChange(of: searchText) { 96 96 viewModel.searchText = searchText 97 97 } 98 98 .onChange(of: viewModel.selectedTab) { 99 99 if !viewModel.searchText.isEmpty { 100 - Task { await viewModel.search(auth: auth.authContext()) } 100 + Task { await viewModel.search(auth: await auth.authContext()) } 101 101 } 102 102 } 103 103 .navigationDestination(item: $searchNavigationUri) { uri in ··· 189 189 searchText = recent.query 190 190 viewModel.searchText = recent.query 191 191 searchIsPresented = true 192 - Task { await viewModel.search(auth: auth.authContext()) } 192 + Task { await viewModel.search(auth: await auth.authContext()) } 193 193 } 194 194 } 195 195 }
+2 -2
Grain/Views/Settings/EditProfileView.swift
··· 146 146 } 147 147 148 148 private func loadProfile() async { 149 - guard let did = auth.userDID, let authContext = auth.authContext() else { return } 149 + guard let did = auth.userDID, let authContext = await auth.authContext() else { return } 150 150 do { 151 151 let profile = try await client.getActorProfile(actor: did, auth: authContext) 152 152 displayName = profile.displayName ?? "" ··· 181 181 } 182 182 183 183 private func save() async { 184 - guard let authContext = auth.authContext() else { return } 184 + guard let authContext = await auth.authContext() else { return } 185 185 isSaving = true 186 186 errorMessage = nil 187 187
+2 -2
Grain/Views/Settings/SettingsView.swift
··· 31 31 .onChange(of: includeExif) { 32 32 guard hasLoadedExifPref else { return } 33 33 Task { 34 - guard let authContext = auth.authContext() else { return } 34 + guard let authContext = await auth.authContext() else { return } 35 35 try? await client.putIncludeExif(includeExif, auth: authContext) 36 36 } 37 37 } ··· 56 56 } 57 57 .navigationTitle("Settings") 58 58 .task { 59 - if let authContext = auth.authContext(), 59 + if let authContext = await auth.authContext(), 60 60 let prefs = try? await client.getPreferences(auth: authContext).preferences, 61 61 let exif = prefs.includeExif { 62 62 includeExif = exif
+1 -1
Grain/Views/Stories/StoryCreateView.swift
··· 184 184 // MARK: - Create 185 185 186 186 private func createStory() async { 187 - guard let authContext = auth.authContext(), 187 + guard let authContext = await auth.authContext(), 188 188 let repo = auth.userDID, 189 189 let previewImage else { return } 190 190
+3 -3
Grain/Views/Stories/StoryViewer.swift
··· 353 353 if let cached = prefetchedStories.removeValue(forKey: did) { 354 354 fetched = cached 355 355 } else { 356 - fetched = try await client.getStories(actor: did, auth: auth.authContext()).stories 356 + fetched = try await client.getStories(actor: did, auth: await auth.authContext()).stories 357 357 } 358 358 stories = fetched 359 359 currentStoryIndex = viewedStories.firstUnviewedIndex(in: fetched) ··· 374 374 let did = authors[nextIndex].profile.did 375 375 guard prefetchedStories[did] == nil else { return } 376 376 Task { 377 - if let response = try? await client.getStories(actor: did, auth: auth.authContext()) { 377 + if let response = try? await client.getStories(actor: did, auth: await auth.authContext()) { 378 378 prefetchedStories[did] = response.stories 379 379 } 380 380 } ··· 386 386 } 387 387 388 388 private func deleteStory(_ story: GrainStory) async { 389 - guard let authContext = auth.authContext() else { return } 389 + guard let authContext = await auth.authContext() else { return } 390 390 let rkey = story.uri.split(separator: "/").last.map(String.init) ?? "" 391 391 do { 392 392 try await client.deleteRecord(collection: "social.grain.story", rkey: rkey, auth: authContext)