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 {
|
||||||
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 }) {
|
// 2. API sync + counter refresh — background, fire-and-forget
|
||||||
currentEntry = updated
|
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 {
|
do {
|
||||||
let fullEntry = try await ReaderAPI().getEntry(id: entry.id)
|
let fullEntry = try await ReaderAPI().getEntry(id: entryId)
|
||||||
let fullHTML = fullEntry.articleHTML
|
let fullHTML = fullEntry.articleHTML
|
||||||
|
|
||||||
if !fullHTML.isEmpty && fullHTML.count > articleContent.count {
|
if !fullHTML.isEmpty && fullHTML.count > articleContent.count {
|
||||||
articleContent = fullHTML
|
articleContent = fullHTML
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Preserve local status/starred (may differ from server due to race)
|
||||||
var merged = fullEntry
|
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.status = local.status
|
||||||
merged.starred = local.starred
|
merged.starred = local.starred
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user