Found while implementing #96 (see PR #103).
The problem
FeedView mirrors store.feedMessages into a private @State private var messages and reconciles
in .onChange(of: store.feedMessages.count) (Views/FeedView.swift:385). The merge adds only ids it
does not already hold:
let existingIds = Set(messages.map { $0.id })
let newMessages = store.feedMessages.filter { !existingIds.contains($0.id) }
So any change to an existing row is invisible to the feed:
- the trigger is
.count, which does not move when a row is edited in place, and
- even if it fired, the merge skips ids already present.
The store and the persisted cache are correct; only the view's private copy goes stale, until a full
reload replaces it.
How it surfaced
#103 adds AppDataStore.applyLinkMetadata(_:toMessageId:) to backfill a just-posted row's link
preview. The store updates correctly and the test proves it — and the feed still renders the old row.
Why it is worth fixing properly
This is not specific to link metadata. Any future in-place mutation — a dig count corrected from the
server, an edited body, a moderation state change — hits the same wall, and each one will look like
its own mysterious bug. The dig path already works around it with a separate digStates dictionary
and a locallyToggled set, which is the same problem solved once, locally.
Build this
Acceptance criteria
Blocks
#103 / #96 are inert until this and #104 land.
Files: Views/FeedView.swift, possibly Services/AppDataStore.swift.
Found while implementing #96 (see PR #103).
The problem
FeedViewmirrorsstore.feedMessagesinto a private@State private var messagesand reconcilesin
.onChange(of: store.feedMessages.count)(Views/FeedView.swift:385). The merge adds only ids itdoes not already hold:
So any change to an existing row is invisible to the feed:
.count, which does not move when a row is edited in place, andThe store and the persisted cache are correct; only the view's private copy goes stale, until a full
reload replaces it.
How it surfaced
#103 adds
AppDataStore.applyLinkMetadata(_:toMessageId:)to backfill a just-posted row's linkpreview. The store updates correctly and the test proves it — and the feed still renders the old row.
Why it is worth fixing properly
This is not specific to link metadata. Any future in-place mutation — a dig count corrected from the
server, an edited body, a moderation state change — hits the same wall, and each one will look like
its own mysterious bug. The dig path already works around it with a separate
digStatesdictionaryand a
locallyToggledset, which is the same problem solved once, locally.Build this
.count. Options, roughly inincreasing order of cost: observe the array itself rather than its count (
MessageisHashable); or replace matched ids in place during the merge rather than skipping them; ordrop the private
@Statemirror and render from the store directly.syncedFromStorefirst-load behaviour must not regress, and a change must not re-trigger a load.
digStates/locallyToggledcan then collapse into the same path. Do not doit in the same PR unless it falls out cleanly.
Acceptance criteria
AppDataStoreupdates that row in the rendered feed without a reload.Blocks
#103 / #96 are inert until this and #104 land.
Files:
Views/FeedView.swift, possiblyServices/AppDataStore.swift.