Feed resilience, clearer empty states, and read/unread
- Don't blank the feed on refresh: keep current stories until new data arrives, and ignore benign URLError.cancelled (-999) so a network blip no longer wipes the list. Coalesce WebSocket story.created bursts into a single debounced refresh. - Empty state now distinguishes polling, couldn't-reach-server (with the error + host + Retry), offline, and genuinely-empty. - Track read stories in SwiftData (ReadStory); mark a story read when opened and drop/dim its signal stripe across the feed and Latest/Saved/Search. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -34,12 +34,15 @@ private struct TabHeader: View {
|
||||
private struct StoryList: View {
|
||||
let stories: [StorySummary]
|
||||
let cachedIds: Set<String>
|
||||
var readIds: Set<String> = []
|
||||
var body: some View {
|
||||
ScrollView {
|
||||
LazyVStack(spacing: 0) {
|
||||
ForEach(stories) { story in
|
||||
NavigationLink(value: story) {
|
||||
StoryRowView(story: story, isCached: cachedIds.contains(story.id))
|
||||
StoryRowView(story: story,
|
||||
isCached: cachedIds.contains(story.id),
|
||||
isRead: readIds.contains(story.id))
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
}
|
||||
@@ -53,6 +56,7 @@ private struct StoryList: View {
|
||||
struct LatestView: View {
|
||||
@EnvironmentObject var store: StoryStore
|
||||
@Query private var cachedArticles: [CachedArticle]
|
||||
@Query private var readStories: [ReadStory]
|
||||
|
||||
private var latest: [StorySummary] {
|
||||
store.stories.sorted { $0.createdAt > $1.createdAt }
|
||||
@@ -65,7 +69,8 @@ struct LatestView: View {
|
||||
VStack(spacing: 0) {
|
||||
TabHeader(title: "Latest")
|
||||
Divider().overlay(Palette.hairline)
|
||||
StoryList(stories: latest, cachedIds: Set(cachedArticles.map(\.storyId)))
|
||||
StoryList(stories: latest, cachedIds: Set(cachedArticles.map(\.storyId)),
|
||||
readIds: Set(readStories.map(\.id)))
|
||||
}
|
||||
}
|
||||
.navigationBarHidden(true)
|
||||
@@ -80,6 +85,7 @@ struct LatestView: View {
|
||||
struct SavedView: View {
|
||||
@Query private var cachedStories: [CachedStory]
|
||||
@Query private var cachedArticles: [CachedArticle]
|
||||
@Query private var readStories: [ReadStory]
|
||||
|
||||
private var stories: [StorySummary] {
|
||||
cachedStories.map(StorySummary.init(cached:)).sorted { $0.signalScore > $1.signalScore }
|
||||
@@ -102,7 +108,8 @@ struct SavedView: View {
|
||||
}
|
||||
.frame(maxWidth: .infinity, maxHeight: .infinity)
|
||||
} else {
|
||||
StoryList(stories: stories, cachedIds: Set(cachedArticles.map(\.storyId)))
|
||||
StoryList(stories: stories, cachedIds: Set(cachedArticles.map(\.storyId)),
|
||||
readIds: Set(readStories.map(\.id)))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -118,6 +125,7 @@ struct SavedView: View {
|
||||
struct SearchView: View {
|
||||
@EnvironmentObject var store: StoryStore
|
||||
@Query private var cachedArticles: [CachedArticle]
|
||||
@Query private var readStories: [ReadStory]
|
||||
@State private var query = ""
|
||||
|
||||
private var results: [StorySummary] {
|
||||
@@ -141,7 +149,8 @@ struct SearchView: View {
|
||||
} else if results.isEmpty {
|
||||
placeholder("No matches for “\(query)”")
|
||||
} else {
|
||||
StoryList(stories: results, cachedIds: Set(cachedArticles.map(\.storyId)))
|
||||
StoryList(stories: results, cachedIds: Set(cachedArticles.map(\.storyId)),
|
||||
readIds: Set(readStories.map(\.id)))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user