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: deleting a feed post leaves it in the persisted cache — reappears after relaunch #108
Found while implementing #105 (see PR #107), which fixed the same class of bug in the other
direction.
The problem
FeedView.deleteMessage removes the message from the view's private @State private var messages
working copy, but never from AppDataStore.feedMessages — and therefore never from the persisted
feed cache that AppDataStore writes.
Consequence: the row disappears from the screen, and then comes back — on the next launch that
reads the cache, or any path that re-seeds the view from the store — until a live fetch happens to
replace the page. The server delete succeeds; only the local cache is wrong.
This is the mirror of the staleness #107 fixed for edits, where an edited post updated only the
view's copy and left the store holding the old text.
#107 was scoped to in-place row updates. Delete is a different operation with its own question
(below), and folding it in would have mixed two behaviours in one PR.
Call it from FeedView.deleteMessage alongside the existing local removal.
Decide the reply case: deleting a parent — does the cache need to drop its replies too, or
does the server cascade and the next fetch settle it? Message has parentId and the backend
cascades on delete (onDelete: Cascade in model Message), so leaving orphans in the cache
until the next fetch may be acceptable. Worth one deliberate decision rather than an accident.
Check the other delete paths for the same gap — MessageDetailView, MessageThreadView and MessageReplyTree all delete messages.
Acceptance criteria
A deleted post stays deleted across a relaunch with no network.
Unit test: delete removes from feedMessages and from the persisted cache; unknown id is a
no-op.
No regression to the optimistic-removal UX (the row still disappears immediately).
Files:Views/FeedView.swift, Services/AppDataStore.swift, and the other delete call sites.
Implemented in #109 (stacked on #107, which must merge first). Closing here — further work tracks in
the PR.
Correcting this issue: my audit list was wrong
I wrote:
Check the other delete paths for the same gap — MessageDetailView, MessageThreadView and MessageReplyTree all delete messages.
None of that is true. Verified:
MessageReplyTreedoes not exist in this repo.
MessageDetailView.swift and MessageThreadView.swift contain zero occurrences of "delete".
APIClient.deleteMessage(id:) has exactly one production call site app-wide: Views/FeedView.swift:536.
A reply cannot be deleted from the iOS app at all today.
The names came from the web codebase — components/messages/MessageReplyTree.tsx and MessageDetailClient.tsx, which I had grepped while investigating #76 — and I wrote them into this
issue as if they were iOS files without re-checking. My error, and worth flagging because an audit
list that looks authoritative is worse than no list.
What the audit did find
FeedView's search-results list passes the same onDelete, and deleteMessage never removed
the row from searchResults — so deleting from a search result left the row on screen. Real, same
bug class, fixed in #109.
DMThreadView / MessagesInboxView deletes are trashDM on DMMessage — a different domain that
never enters feedMessages. Correctly out of scope.
The replies decision
No cascade — remove exactly the named id, and the reasoning is stronger than "orphans are
acceptable": the feed cache cannot hold a reply. GET /api/messages appends parentId: null
("Only top-level messages (no replies in main feed)"), and the only other cache writer, ComposeView,
inserts only when !isReply && !isRepost. A cascade would be unreachable code; the backend's onDelete: Cascade handles the real thing.
Three tests construct the impossible state anyway — including a cache round-trip, since [Message]
decodes all-or-nothing and one malformed row would take the entire feed cache with it.
Two implementation details worth keeping
The optimistic local removal is load-bearing, not cosmetic.FeedMerge (from fix(feed): reconcile in-place row updates in the feed #107)
deliberately keeps rows the store doesn't hold — that is how paginated pages survive a merge — so store.removeFeedMessage alone can never evict a row the view already holds. fix(feed): drop deleted posts from the feed cache #109 adds a test that
fails loudly if a future "simplification" drops the view-side line.
removeAll, not remove(at:) — insertFeedMessage does not deduplicate (there is an existing
test asserting count 2 for a repeated id), so an index-based removal would leave a copy behind.
Unrelated latent issue noted, not fixed
EditMessageView.swift:132 reconstructs its Message with parentId: nil hardcoded. Harmless today
since feed rows are all top-level, but a field-drop waiting to happen if replies ever become
editable — relevant to #76.
Activity
Adron commentedon Sep 16, 2026
Implemented in #109 (stacked on #107, which must merge first). Closing here — further work tracks in
the PR.
Correcting this issue: my audit list was wrong
I wrote:
None of that is true. Verified:
MessageReplyTreedoes not exist in this repo.MessageDetailView.swiftandMessageThreadView.swiftcontain zero occurrences of "delete".APIClient.deleteMessage(id:)has exactly one production call site app-wide:Views/FeedView.swift:536.A reply cannot be deleted from the iOS app at all today.
The names came from the web codebase —
components/messages/MessageReplyTree.tsxandMessageDetailClient.tsx, which I had grepped while investigating #76 — and I wrote them into thisissue as if they were iOS files without re-checking. My error, and worth flagging because an audit
list that looks authoritative is worse than no list.
What the audit did find
FeedView's search-results list passes the sameonDelete, anddeleteMessagenever removedthe row from
searchResults— so deleting from a search result left the row on screen. Real, samebug class, fixed in #109.
DMThreadView/MessagesInboxViewdeletes aretrashDMonDMMessage— a different domain thatnever enters
feedMessages. Correctly out of scope.The replies decision
No cascade — remove exactly the named id, and the reasoning is stronger than "orphans are
acceptable": the feed cache cannot hold a reply.
GET /api/messagesappendsparentId: null("Only top-level messages (no replies in main feed)"), and the only other cache writer,
ComposeView,inserts only when
!isReply && !isRepost. A cascade would be unreachable code; the backend'sonDelete: Cascadehandles the real thing.Three tests construct the impossible state anyway — including a cache round-trip, since
[Message]decodes all-or-nothing and one malformed row would take the entire feed cache with it.
Two implementation details worth keeping
FeedMerge(from fix(feed): reconcile in-place row updates in the feed #107)deliberately keeps rows the store doesn't hold — that is how paginated pages survive a merge — so
store.removeFeedMessagealone can never evict a row the view already holds. fix(feed): drop deleted posts from the feed cache #109 adds a test thatfails loudly if a future "simplification" drops the view-side line.
removeAll, notremove(at:)—insertFeedMessagedoes not deduplicate (there is an existingtest asserting count 2 for a repeated id), so an index-based removal would leave a copy behind.
Unrelated latent issue noted, not fixed
EditMessageView.swift:132reconstructs itsMessagewithparentId: nilhardcoded. Harmless todaysince feed rows are all top-level, but a field-drop waiting to happen if replies ever become
editable — relevant to #76.