Sink seen/read stories, fix F1 matching, drop East Africa pill
- Sink already-seen and read stories to the bottom of the feed (new SeenStory model + per-load snapshot so order is stable while scrolling and refreshes sink what you've seen) so fresh content surfaces. - F1 matching: clear F1 headline OR a majority of F1-feed sources, replacing the broad keyword list that leaked crypto in (fia -> "deFIAnt"). Dropped unused matchSources path. - Remove the East Africa pill. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -25,10 +25,14 @@ struct SignalFeedView: View {
|
||||
@Query private var cachedArticles: [CachedArticle]
|
||||
@Query private var readStories: [ReadStory]
|
||||
@Query private var savedStories: [SavedStory]
|
||||
@Query private var seenStories: [SeenStory]
|
||||
@State private var showFeeds = false
|
||||
@State private var showSettings = false
|
||||
@State private var selectedInterest: Interest? = nil
|
||||
@State private var shareStory: StorySummary?
|
||||
/// Snapshot of already-seen story ids, captured per load so the order stays
|
||||
/// stable while scrolling (and refreshes to sink newly-seen ones).
|
||||
@State private var seenSnapshot: Set<String> = []
|
||||
|
||||
/// Stories that have full article content cached → eligible for the green dot.
|
||||
private var cachedStoryIds: Set<String> { Set(cachedArticles.map(\.storyId)) }
|
||||
@@ -45,16 +49,36 @@ struct SignalFeedView: View {
|
||||
} else {
|
||||
base = []
|
||||
}
|
||||
guard let interest = selectedInterest else {
|
||||
|
||||
let filtered: [StorySummary]
|
||||
if let interest = selectedInterest {
|
||||
if interest.id == "f1" {
|
||||
filtered = base.filter(interest.matches)
|
||||
} else {
|
||||
// Other pills (incl. Tech/Finance) exclude F1 (backend mis-tags it).
|
||||
filtered = base.filter { interest.matches($0) && !Interest.f1.matches($0) }
|
||||
}
|
||||
} else {
|
||||
// "All" is news-only: F1 lives solely under its own pill.
|
||||
return base.filter { !Interest.f1.matches($0) }
|
||||
filtered = base.filter { !Interest.f1.matches($0) }
|
||||
}
|
||||
if interest.id == "f1" {
|
||||
return base.filter(interest.matches)
|
||||
}
|
||||
// Every other pill (incl. Tech/Finance) excludes F1, since the backend
|
||||
// mis-tags F1 stories into finance/tech/politics.
|
||||
return base.filter { interest.matches($0) && !Interest.f1.matches($0) }
|
||||
|
||||
// Sink stories you've already read or seen (in a prior load) so fresh
|
||||
// content surfaces and you stop seeing the same articles. Uses a per-load
|
||||
// snapshot so the order is stable while you scroll.
|
||||
let demoted = readStoryIds.union(seenSnapshot)
|
||||
let fresh = filtered.filter { !demoted.contains($0.id) }
|
||||
let old = filtered.filter { demoted.contains($0.id) }
|
||||
return fresh + old
|
||||
}
|
||||
|
||||
private func captureSeenSnapshot() { seenSnapshot = Set(seenStories.map(\.id)) }
|
||||
|
||||
private func markSeen(_ s: StorySummary) {
|
||||
let id = s.id
|
||||
guard !seenStories.contains(where: { $0.id == id }) else { return }
|
||||
modelContext.insert(SeenStory(id: id))
|
||||
try? modelContext.save()
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
@@ -218,6 +242,7 @@ struct SignalFeedView: View {
|
||||
.listRowBackground(Color.black)
|
||||
.listRowSeparator(.hidden)
|
||||
.onAppear {
|
||||
markSeen(story)
|
||||
if story.id == displayStories.last?.id {
|
||||
Task { await store.loadMore() }
|
||||
}
|
||||
@@ -255,7 +280,9 @@ struct SignalFeedView: View {
|
||||
.listStyle(.plain)
|
||||
.scrollContentBackground(.hidden)
|
||||
.background(Color.black)
|
||||
.onAppear { captureSeenSnapshot() }
|
||||
.refreshable {
|
||||
captureSeenSnapshot() // sink what you've already seen
|
||||
await store.loadStories(refresh: true)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user