fix: synchronous optimistic read-state on article open
The local entries mutation now runs synchronously at the top of
.task, before any Task {} or await:
1. vm.entries[idx].status = "read" ← synchronous, immediate
2. currentEntry syncs from mutated array ← immediate
3. Task { api.markEntries + getCounters } ← background
4. await getEntry ← content fetch, merge preserves local status
Previously markAsRead was wrapped in Task {} (fire-and-forget),
which SCHEDULED the mutation but didn't execute it until the main
actor yielded. Line 2 read stale state because the mutation hadn't
run yet.
Now the @Observable array mutation happens before any async work,
so the ForEach row re-renders on the same run loop — the list
shows "read" even during the push animation.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -59,22 +59,35 @@ struct ArticleView: View {
|
||||
}
|
||||
}
|
||||
.task {
|
||||
Task { await vm.markAsRead(entry) }
|
||||
// 1. Synchronous local mutation — runs before any async work.
|
||||
// The @Observable array mutation triggers ForEach row re-render
|
||||
// immediately, so the list shows "read" even during push animation.
|
||||
let entryId = entry.id
|
||||
if let idx = vm.entries.firstIndex(where: { $0.id == entryId }),
|
||||
!vm.entries[idx].isRead {
|
||||
vm.entries[idx].status = "read"
|
||||
}
|
||||
currentEntry = vm.entries.first(where: { $0.id == entryId }) ?? currentEntry
|
||||
|
||||
if let updated = vm.entries.first(where: { $0.id == entry.id }) {
|
||||
currentEntry = updated
|
||||
// 2. API sync + counter refresh — background, fire-and-forget
|
||||
Task {
|
||||
let api = ReaderAPI()
|
||||
try? await api.markEntries(ids: [entryId], status: "read")
|
||||
vm.counters = try? await api.getCounters()
|
||||
}
|
||||
|
||||
// 3. Fetch full content — update when ready
|
||||
do {
|
||||
let fullEntry = try await ReaderAPI().getEntry(id: entry.id)
|
||||
let fullEntry = try await ReaderAPI().getEntry(id: entryId)
|
||||
let fullHTML = fullEntry.articleHTML
|
||||
|
||||
if !fullHTML.isEmpty && fullHTML.count > articleContent.count {
|
||||
articleContent = fullHTML
|
||||
}
|
||||
|
||||
// Preserve local status/starred (may differ from server due to race)
|
||||
var merged = fullEntry
|
||||
if let local = vm.entries.first(where: { $0.id == entry.id }) {
|
||||
if let local = vm.entries.first(where: { $0.id == entryId }) {
|
||||
merged.status = local.status
|
||||
merged.starred = local.starred
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user