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.

feat: add StoryStatusCache for shared story status across views

Populated from getStoryAuthors API, injected via environment so all
views can check if a user has active stories.

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

+27 -1
+2
Grain/GrainApp.swift
··· 3 3 @main 4 4 struct GrainApp: App { 5 5 @State private var authManager = AuthManager() 6 + @State private var storyStatusCache = StoryStatusCache() 6 7 @State private var pendingDeepLink: DeepLink? 7 8 8 9 var body: some Scene { ··· 11 12 if authManager.isAuthenticated { 12 13 MainTabView(pendingDeepLink: $pendingDeepLink) 13 14 .environment(authManager) 15 + .environment(storyStatusCache) 14 16 .tint(Color("AccentColor")) 15 17 } else { 16 18 LoginView()
+23
Grain/ViewModels/StoryStatusCache.swift
··· 1 + import Foundation 2 + 3 + @Observable 4 + @MainActor 5 + final class StoryStatusCache { 6 + private(set) var authorsByDid: [String: GrainStoryAuthor] = [:] 7 + 8 + var didsWithStories: Set<String> { 9 + Set(authorsByDid.keys) 10 + } 11 + 12 + func hasStory(for did: String) -> Bool { 13 + authorsByDid[did] != nil 14 + } 15 + 16 + func author(for did: String) -> GrainStoryAuthor? { 17 + authorsByDid[did] 18 + } 19 + 20 + func update(from authors: [GrainStoryAuthor]) { 21 + authorsByDid = Dictionary(uniqueKeysWithValues: authors.map { ($0.profile.did, $0) }) 22 + } 23 + }
+2 -1
Grain/ViewModels/StoryStripViewModel.swift
··· 12 12 self.client = client 13 13 } 14 14 15 - func load(auth: AuthContext? = nil) async { 15 + func load(auth: AuthContext? = nil, storyStatusCache: StoryStatusCache? = nil) async { 16 16 isLoading = true 17 17 do { 18 18 let response = try await client.getStoryAuthors(auth: auth) 19 19 authors = response.authors 20 + storyStatusCache?.update(from: response.authors) 20 21 } catch { 21 22 // Silently fail — strip just won't show 22 23 }