feat: merge AI / Cloud / HomeLab into Tech pill with sub-sections
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

- Removed AI, Cloud, HomeLab as top-level pills
- Tech pill now matches all their slugs (ai, ml, cloud, homelab, security, dev)
- Tech view shows a sectioned front page: AI · Cloud · HomeLab · Security · Dev · Technology
- Sub-sections defined via NewsSection.techSubSections + StoryPill.subSections
- digest() extracted to makeDigest(sections:) so any pill can have sections
- Tech auto-loads 100 stories to fill its sub-sections (same as All)
- "All" Tech section in the main feed now links to Tech pill

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Robin Kutesa
2026-06-21 02:53:41 +03:00
parent ff304d1fda
commit 7a97118d12
2 changed files with 52 additions and 36 deletions

View File

@@ -60,15 +60,15 @@ struct SignalFeedView: View {
private var isDigest: Bool { true } private var isDigest: Bool { true }
/// Front-page digest: Top Stories + up to 3 per category section, deduped. /// Front-page digest: Top Stories + up to 3 per category section, deduped.
private var digest: (top: [StorySummary], sections: [(NewsSection, [StorySummary])]) { private func makeDigest(sections: [NewsSection]) -> (top: [StorySummary], sections: [(NewsSection, [StorySummary])]) {
let unread = filteredStories.filter { !readStoryIds.contains($0.id) } let unread = filteredStories.filter { !readStoryIds.contains($0.id) }
let top = Array(unread.prefix(5)) let top = Array(unread.prefix(5))
var pool = Array(unread.dropFirst(5)) var pool = Array(unread.dropFirst(5))
var out: [(NewsSection, [StorySummary])] = [] var out: [(NewsSection, [StorySummary])] = []
for section in NewsSection.sections { for section in sections {
var picked: [StorySummary] = [], rest: [StorySummary] = [] var picked: [StorySummary] = [], rest: [StorySummary] = []
for s in pool { for s in pool {
if picked.count < 3 && s.inSection(section) { picked.append(s) } if picked.count < 4 && s.inSection(section) { picked.append(s) }
else { rest.append(s) } else { rest.append(s) }
} }
pool = rest pool = rest
@@ -136,8 +136,8 @@ struct SignalFeedView: View {
.task(id: selectedPill) { .task(id: selectedPill) {
await store.setPill(selectedPill) await store.setPill(selectedPill)
var pages = 0 var pages = 0
if selectedPill == .all { // All and Tech both use multi-section layouts that need enough stories.
// Front page: pull enough stories to fill the sections. if selectedPill == .all || selectedPill == .tech {
while store.stories.count < 100 && store.hasMore && pages < 5 { while store.stories.count < 100 && store.hasMore && pages < 5 {
await store.loadMore(); pages += 1 await store.loadMore(); pages += 1
} }
@@ -262,10 +262,12 @@ struct SignalFeedView: View {
/// Card layout for every pill. "All" gets full multi-section front page; /// Card layout for every pill. "All" gets full multi-section front page;
/// topic pills get a hero card + flat list within that topic. /// topic pills get a hero card + flat list within that topic.
@ViewBuilder private var digestContent: some View { @ViewBuilder private var digestContent: some View {
let d = digest let activeSections = selectedPill == .all
let headerLabel = selectedPill == .all ? NewsSection.sections
? "TOP STORIES" : (selectedPill.subSections ?? [])
: selectedPill.label.uppercased() let d = makeDigest(sections: activeSections)
let headerLabel = selectedPill == .all ? "TOP STORIES" : selectedPill.label.uppercased()
sectionHeader(headerLabel, pill: nil) sectionHeader(headerLabel, pill: nil)
if let lead = d.top.first { if let lead = d.top.first {
mainRow(lead, hero: true) mainRow(lead, hero: true)
@@ -273,12 +275,11 @@ struct SignalFeedView: View {
} else { } else {
emptyState.plainBlackRow() emptyState.plainBlackRow()
} }
if selectedPill == .all { // "All" and pills with sub-sections (Tech) get a sectioned breakdown.
ForEach(d.sections, id: \.0.id) { section, stories in ForEach(d.sections, id: \.0.id) { section, stories in
sectionHeader(section.label.uppercased(), pill: section.pill) sectionHeader(section.label.uppercased(), pill: section.pill)
ForEach(stories) { mainRow($0) } ForEach(stories) { mainRow($0) }
} }
}
readShelf readShelf
} }

View File

@@ -216,10 +216,7 @@ enum StoryPill: String, CaseIterable, Identifiable {
case all = "All" case all = "All"
case f1 = "F1" case f1 = "F1"
case sport = "Sport" case sport = "Sport"
case ai = "AI" case tech = "Tech" // covers AI, Cloud, HomeLab use sub-sections inside
case cloud = "Cloud"
case homelab = "HomeLab"
case tech = "Tech"
case uganda = "Uganda" case uganda = "Uganda"
case southAfrica = "South Africa" case southAfrica = "South Africa"
case canada = "Canada" case canada = "Canada"
@@ -233,18 +230,27 @@ enum StoryPill: String, CaseIterable, Identifiable {
case .all: return [] case .all: return []
case .f1: return ["formula-1"] case .f1: return ["formula-1"]
case .sport: return ["sports", "esports"] case .sport: return ["sports", "esports"]
case .ai: return ["artificial-intelligence", "machine-learning", "robotics"] case .tech: return [
case .cloud: return ["cloud-computing"] "technology", "programming-and-software-development",
case .homelab: return ["homelab"]
case .tech: return ["technology", "programming-and-software-development",
"cybersecurity", "security", "privacy-and-data-protection", "cybersecurity", "security", "privacy-and-data-protection",
"web-design-and-ui-ux", "wordpress-and-web-development"] "web-design-and-ui-ux", "wordpress-and-web-development",
"artificial-intelligence", "machine-learning", "robotics",
"cloud-computing", "homelab",
]
case .uganda: return ["uganda"] case .uganda: return ["uganda"]
case .southAfrica: return ["south-africa"] case .southAfrica: return ["south-africa"]
case .canada: return ["canada"] case .canada: return ["canada"]
case .unitedStates: return ["united-states"] case .unitedStates: return ["united-states"]
} }
} }
/// Sub-sections to show inside the card layout for this pill (nil = flat).
var subSections: [NewsSection]? {
switch self {
case .tech: return NewsSection.techSubSections
default: return nil
}
}
} }
extension StorySummary { extension StorySummary {
@@ -293,8 +299,7 @@ struct NewsSection: Identifiable {
.init(id: "world", label: "World", slugs: ["world-news", "news", "news-and-current-affairs"], pill: nil), .init(id: "world", label: "World", slugs: ["world-news", "news", "news-and-current-affairs"], pill: nil),
.init(id: "politics", label: "Politics", slugs: ["politics", "human-rights", "investigative"], pill: nil), .init(id: "politics", label: "Politics", slugs: ["politics", "human-rights", "investigative"], pill: nil),
.init(id: "business", label: "Business", slugs: ["business", "finance", "fintech", "cryptocurrency", "blockchain", "entrepreneur", "startups", "real-estate"], pill: nil), .init(id: "business", label: "Business", slugs: ["business", "finance", "fintech", "cryptocurrency", "blockchain", "entrepreneur", "startups", "real-estate"], pill: nil),
.init(id: "ai", label: "AI", slugs: ["artificial-intelligence", "machine-learning", "robotics"], pill: .ai), .init(id: "tech", label: "Tech", slugs: ["technology", "programming-and-software-development", "cybersecurity", "security", "web-design-and-ui-ux", "wordpress-and-web-development", "privacy-and-data-protection", "cloud-computing", "artificial-intelligence", "machine-learning", "robotics", "homelab"], pill: .tech),
.init(id: "technology", label: "Technology", slugs: ["technology", "programming-and-software-development", "cybersecurity", "security", "web-design-and-ui-ux", "wordpress-and-web-development", "privacy-and-data-protection", "cloud-computing"], pill: .tech),
.init(id: "africa", label: "Africa", slugs: ["africa", "east-africa", "south-africa", "uganda"], pill: nil), .init(id: "africa", label: "Africa", slugs: ["africa", "east-africa", "south-africa", "uganda"], pill: nil),
.init(id: "sport", label: "Sport", slugs: ["sports", "esports", "formula-1"], pill: .sport), .init(id: "sport", label: "Sport", slugs: ["sports", "esports", "formula-1"], pill: .sport),
.init(id: "science", label: "Science", slugs: ["science", "astronomy", "environment", "green-technology", "sustainability"], pill: nil), .init(id: "science", label: "Science", slugs: ["science", "astronomy", "environment", "green-technology", "sustainability"], pill: nil),
@@ -302,4 +307,14 @@ struct NewsSection: Identifiable {
.init(id: "health", label: "Health", slugs: ["health", "health-and-wellness", "mental-health"], pill: nil), .init(id: "health", label: "Health", slugs: ["health", "health-and-wellness", "mental-health"], pill: nil),
.init(id: "lifestyle", label: "Lifestyle", slugs: ["lifestyle", "food-and-drink", "travel", "gardening", "outdoor-and-hiking", "parenting-and-family", "interior-design-and-home-decor", "photography", "self-improvement-and-personal-development", "education", "e-learning"], pill: nil), .init(id: "lifestyle", label: "Lifestyle", slugs: ["lifestyle", "food-and-drink", "travel", "gardening", "outdoor-and-hiking", "parenting-and-family", "interior-design-and-home-decor", "photography", "self-improvement-and-personal-development", "education", "e-learning"], pill: nil),
] ]
/// Sub-sections shown inside the Tech pill view.
static let techSubSections: [NewsSection] = [
.init(id: "t-ai", label: "AI", slugs: ["artificial-intelligence", "machine-learning", "robotics"], pill: nil),
.init(id: "t-cloud", label: "Cloud", slugs: ["cloud-computing"], pill: nil),
.init(id: "t-homelab", label: "HomeLab", slugs: ["homelab"], pill: nil),
.init(id: "t-security", label: "Security", slugs: ["cybersecurity", "security", "privacy-and-data-protection"], pill: nil),
.init(id: "t-dev", label: "Dev", slugs: ["programming-and-software-development", "web-design-and-ui-ux", "wordpress-and-web-development"], pill: nil),
.init(id: "t-tech", label: "Technology", slugs: ["technology"], pill: nil),
]
} }