feat: offline article caching + smart pull-to-refresh reshuffle
Offline caching: - BackgroundRefreshManager.preCacheArticles() fetches top article per story (score ≥60, up to 20 stories) and stores as CachedArticle in SwiftData - Runs after every BG fetch AND after every foreground sync via StoryStore - Articles are available in ArticleReaderView offline without ever opening them Pull-to-refresh reshuffle when nothing new: - loadStories(refresh:) now returns newCount (genuinely new story IDs) - If server returns 0 new stories, feed merges in any BG-cached stories not already in the live feed, then re-sorts by signal score (reshuffle) - seenSnapshot is always cleared so everything re-ranks fresh Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -65,6 +65,9 @@ enum BackgroundRefreshManager {
|
||||
|
||||
try? context.save()
|
||||
|
||||
// Pre-cache articles for offline reading
|
||||
await preCacheArticles(page.data, container: container)
|
||||
|
||||
if !newStories.isEmpty {
|
||||
notifyNewStories(newStories)
|
||||
}
|
||||
@@ -73,6 +76,30 @@ enum BackgroundRefreshManager {
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Article pre-caching
|
||||
|
||||
/// Fetch and store the top article for each high-signal uncached story so
|
||||
/// the user can read them offline without having opened them first.
|
||||
/// Safe to call in background or foreground — creates its own ModelContext.
|
||||
static func preCacheArticles(_ stories: [StorySummary], container: ModelContainer) async {
|
||||
let context = ModelContext(container)
|
||||
let top = stories
|
||||
.filter { $0.signalScore >= 60 && !$0.sources.isEmpty }
|
||||
.sorted { $0.signalScore > $1.signalScore }
|
||||
.prefix(20)
|
||||
|
||||
for story in top {
|
||||
let sid = story.id
|
||||
let alreadyCached = (try? context.fetch(
|
||||
FetchDescriptor<CachedArticle>(predicate: #Predicate { $0.storyId == sid })
|
||||
))?.isEmpty == false
|
||||
guard !alreadyCached, let src = story.sources.first else { continue }
|
||||
guard let art = try? await APIClient.shared.fetchArticle(id: src.id) else { continue }
|
||||
context.insert(CachedArticle(from: art))
|
||||
try? context.save()
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - New-story notifications
|
||||
|
||||
// Slugs for the featured categories the user cares about.
|
||||
|
||||
Reference in New Issue
Block a user