You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
Bug: FeedView cannot see in-place row updates — merges only unseen ids, keyed on count #105
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:
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
Make the reconciliation respond to content changes, not just .count. Options, roughly in
increasing order of cost: observe the array itself rather than its count (Message is Hashable); or replace matched ids in place during the merge rather than skipping them; or
drop the private @State mirror and render from the store directly.
Preserve what the mirror is currently buying — scroll position and the syncedFromStore
first-load behaviour must not regress, and a change must not re-trigger a load.
Consider whether digStates / locallyToggled can then collapse into the same path. Do not do
it in the same PR unless it falls out cleanly.
Acceptance criteria
Mutating one message in AppDataStore updates that row in the rendered feed without a reload.
No scroll-position regression, no extra fetches, no duplicate rows.
A test that mutates a store message and asserts the view's data source reflects it.
Implemented in #107. Closing here — further work tracks in the PR.
This issue's cheapest suggested option does not work, and the reason is a repo-wide trap
I suggested "observe the array itself rather than its count (Message is Hashable)". That would silently never fire. Message's conformance is deliberately identity-based:
So [Message] compares equal after a row's content is replaced, and .onChange(of: store.feedMessages) observes nothing. Worth knowing generally: any onChange(of:) over a Message collection anywhere in the app has this problem.
Making == content-based would fix the trigger but changes a type used app-wide and needs Equatable on four nested payload types — and would impose a deep array diff on every body
evaluation of the busiest screen. #107 uses an O(1) feedRevision counter instead.
What landed
Two halves, because the trigger and the merge were both broken:
AppDataStore.feedRevision, a @Published counter bumped from didSet on feedMessages; FeedView observes that instead of .count.
FeedMerge.merge(existing:incoming:) extracted to Services/ — matched ids replaced in place
with position preserved, unseen ids still prepended, pagination rows the store never saw kept.
"Render from the store" was considered and rejected: the private @State is a working copy, not a
mirror — loadMore() appends pages the store never holds, and loadMessages() replaces it wholesale
for the filtered views.
A latent bug fixed on the way
AppDataStore.updateFeedMessage(_:) was added because nothing on main could mutate an existing
feed row. Its production call site in FeedView's edit callback also fixes real staleness: an edited
post previously updated only the view's copy, leaving the store and its persisted cache holding
the old text.
Follow-up filed
FeedView.deleteMessage has the same bug in the other direction — it removes from the working copy
only, so the persisted cache keeps a deleted post. Filed as #108. Deliberately not fixed here to keep
this PR to one behaviour.
Collapsing digStates / locallyToggled into the merge was checked and does not fall out free — locallyToggled encodes a policy the merge doesn't know about — so it was left alone per scope.
Activity
Adron commentedon Sep 16, 2026
Implemented in #107. Closing here — further work tracks in the PR.
This issue's cheapest suggested option does not work, and the reason is a repo-wide trap
I suggested "observe the array itself rather than its count (
MessageisHashable)". That wouldsilently never fire.
Message's conformance is deliberately identity-based:So
[Message]compares equal after a row's content is replaced, and.onChange(of: store.feedMessages)observes nothing. Worth knowing generally: anyonChange(of:)over aMessagecollection anywhere in the app has this problem.Making
==content-based would fix the trigger but changes a type used app-wide and needsEquatableon four nested payload types — and would impose a deep array diff on every bodyevaluation of the busiest screen. #107 uses an O(1)
feedRevisioncounter instead.What landed
Two halves, because the trigger and the merge were both broken:
AppDataStore.feedRevision, a@Publishedcounter bumped fromdidSetonfeedMessages;FeedViewobserves that instead of.count.FeedMerge.merge(existing:incoming:)extracted toServices/— matched ids replaced in placewith position preserved, unseen ids still prepended, pagination rows the store never saw kept.
"Render from the store" was considered and rejected: the private
@Stateis a working copy, not amirror —
loadMore()appends pages the store never holds, andloadMessages()replaces it wholesalefor the filtered views.
A latent bug fixed on the way
AppDataStore.updateFeedMessage(_:)was added because nothing onmaincould mutate an existingfeed row. Its production call site in
FeedView's edit callback also fixes real staleness: an editedpost previously updated only the view's copy, leaving the store and its persisted cache holding
the old text.
Follow-up filed
FeedView.deleteMessagehas the same bug in the other direction — it removes from the working copyonly, so the persisted cache keeps a deleted post. Filed as #108. Deliberately not fixed here to keep
this PR to one behaviour.
Collapsing
digStates/locallyToggledinto the merge was checked and does not fall out free —locallyToggledencodes a policy the merge doesn't know about — so it was left alone per scope.Merge origin/main into fix/feedview-inplace-row-updates