Sink seen/read stories, fix F1 matching, drop East Africa pill
Some checks failed
CI / Build · macos-14 · Debug (push) Has been cancelled
CI / Build · macos-15 · Debug (push) Has been cancelled
CI / Build · macos-14 · Release (push) Has been cancelled
CI / Build · macos-15 · Release (push) Has been cancelled

- 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:
Robin Kutesa
2026-06-19 14:17:32 +03:00
parent 20dd49a53c
commit feee58748c
4 changed files with 71 additions and 32 deletions

View File

@@ -188,25 +188,35 @@ struct Interest: Identifiable, Hashable {
/// When set, matches the backend topic classification instead of keywords
/// (e.g. finance, tech).
var topicSlug: String? = nil
/// Whether to consider source/outlet names when matching. True only for F1
/// (its sources are the signal). Regions must NOT an outlet's location is
/// not the story's location (e.g. a Congo story carried by an SA outlet).
var matchSources: Bool = false
func matches(_ s: StorySummary) -> Bool {
if id == "f1" { return Interest.isFormula1(s) }
if let topicSlug {
return s.topic.caseInsensitiveCompare(topicSlug) == .orderedSame
}
// Headline only the backend's summaries are polluted by over-clustering
// (a Congo headline can carry a South-Africa summary), so they're unsafe
// to match on. F1 additionally trusts its (clean) source names.
var hay = s.headline
if matchSources { hay += " " + s.sources.map(\.name).joined(separator: " ") }
hay = hay.lowercased()
// Headline only the backend's summaries/outlet names are polluted by
// over-clustering (a Congo headline can carry a South-Africa summary or an
// SA outlet), so they're unsafe to match on.
let hay = s.headline.lowercased()
if excludeKeywords.contains(where: { hay.contains($0) }) { return false }
return keywords.contains { hay.contains($0) }
}
/// F1 = a clear F1 headline, or a majority of F1-feed sources. A single F1
/// source isn't enough the over-clustered backend sometimes attaches one to
/// an unrelated story. Avoids substring traps like "fia" matching "deFIAnt".
static func isFormula1(_ s: StorySummary) -> Bool {
let h = s.headline.lowercased()
if h.contains("formula 1") || h.contains("formula one") || h.contains("grand prix")
|| h.hasPrefix("f1 ") || h.hasSuffix(" f1") || h.contains(" f1 ") || h.contains("f1:") {
return true
}
let feeds = ["f1", "racefans", "motorsport", "autosport", "planetf1", "the race", "formula 1"]
let srcs = s.sources
guard !srcs.isEmpty else { return false }
let n = srcs.filter { src in feeds.contains { src.name.lowercased().contains($0) } }.count
return Double(n) / Double(srcs.count) >= 0.5
}
// Region keywords are place/person names found in the story text never
// outlet names (which are unreliable due to backend over-clustering).
private static let ugandaKeywords = ["uganda", "ugandan", "kampala", "museveni",
@@ -216,22 +226,12 @@ struct Interest: Identifiable, Hashable {
"eskom", "gauteng", "western cape", "soweto",
"drakensberg", "mbalula", "zuma"]
static let f1 = Interest(id: "f1", label: "F1",
keywords: ["formula 1", "formula one", "grand prix", "f1", "verstappen", "hamilton",
"mclaren", "ferrari", "red bull", "racefans", "motorsport", "autosport",
"pole position", "pirelli", "qualifying", "fia", "planetf1"],
matchSources: true)
static let f1 = Interest(id: "f1", label: "F1") // matching handled by isFormula1
/// Pinned filter pills, in order. Regions are mutually exclusive.
/// Pinned filter pills, in order.
static let pinned: [Interest] = [
f1,
Interest(id: "uganda", label: "Uganda", keywords: ugandaKeywords),
Interest(id: "east-africa", label: "East Africa",
keywords: ["east africa", "eastafrican", "kenya", "kenyan", "nairobi",
"tanzania", "tanzanian", "dar es salaam", "rwanda", "rwandan",
"kigali", "ethiopia", "ethiopian", "addis ababa", "burundi",
"south sudan", "juba"],
excludeKeywords: ugandaKeywords + southAfricaKeywords),
Interest(id: "south-africa", label: "South Africa", keywords: southAfricaKeywords),
Interest(id: "finance", label: "Finance", topicSlug: "finance"),
Interest(id: "tech", label: "Tech", topicSlug: "tech"),