feat: sync read/unread state to backend with persistent device ID
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

- APIClient: generate and persist a stable device UUID (jarvis.device.id
  in UserDefaults); attach as X-Jarvis-Device-Id header on every request
- Add markStoryRead / markStoryUnread — POST/DELETE /stories/:id/read with
  device_id, read_at, and source fields
- Wire both calls into all three read-toggle sites: SignalFeedView,
  ArticleReaderView, StoryDetailView
- Add postVoid helper for fire-and-forget POST calls (no response body needed)
- Pass include_read param on stories fetch for future server-side filtering
- CacheMaintenance: recentHours 84→72 (mirrors backend 72h story window);
  markerHours 96→192 so read suppression survives a full weekly news cycle

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Robin Kutesa
2026-06-22 19:35:45 +03:00
parent a476e426f3
commit 044f67cf63
5 changed files with 55 additions and 4 deletions

View File

@@ -114,6 +114,7 @@ struct SignalFeedView: View {
FetchDescriptor<ReadStory>(predicate: #Predicate { $0.id == id })))?.first {
modelContext.delete(row)
try? modelContext.save()
Task { try? await APIClient.shared.markStoryUnread(id: id) }
}
}
@@ -632,6 +633,7 @@ struct SignalFeedView: View {
if existing?.first == nil {
modelContext.insert(ReadStory(id: id))
try? modelContext.save()
Task { try? await APIClient.shared.markStoryRead(id: id) }
withAnimation(.snappy) { showRead = true }
}
}

View File

@@ -431,8 +431,10 @@ struct ArticleReaderView: View {
if let row = (try? modelContext.fetch(
FetchDescriptor<ReadStory>(predicate: #Predicate { $0.id == id })))?.first {
modelContext.delete(row)
Task { try? await APIClient.shared.markStoryUnread(id: id) }
} else {
modelContext.insert(ReadStory(id: id))
Task { try? await APIClient.shared.markStoryRead(id: id) }
}
try? modelContext.save()
}

View File

@@ -142,6 +142,7 @@ struct StoryDetailView: View {
else { return }
modelContext.insert(ReadStory(id: id))
try? modelContext.save()
Task { try? await APIClient.shared.markStoryRead(id: id) }
}
private func toggleRead() {
@@ -149,8 +150,10 @@ struct StoryDetailView: View {
if let row = (try? modelContext.fetch(
FetchDescriptor<ReadStory>(predicate: #Predicate { $0.id == id })))?.first {
modelContext.delete(row)
Task { try? await APIClient.shared.markStoryUnread(id: id) }
} else {
modelContext.insert(ReadStory(id: id))
Task { try? await APIClient.shared.markStoryRead(id: id) }
}
try? modelContext.save()
}