fix(delete): make agent-deleted messages disappear from desktop UI immediately#918
Conversation
Sprout's channel subscription filter is { kinds: [...], "#h": [channelId] }
— Nostr filters AND across fields, so a kind:5 NIP-09 deletion that lacks
an h tag never matches the subscription and clients only see the deletion
once a non-channel-scoped fetch backfills it. The desktop UI then keeps
rendering the deleted message until manual refresh.
Add the channel id as an h tag on kind:5 deletions in both the SDK builder
(build_delete_compat) and the desktop Tauri builder (events::build_delete_compat).
Thread channel_id through the delete_message Tauri command and all four
desktop callers (forum hooks, messages hook, HomeView, tauri.ts shim).
Extra tags are safe per NIP-09; the relay derives the deletion target
from the e tag and validates author permissions independently of the h
tag. Mirrors the pattern already used by build_message (kind:9).
Signed-off-by: Taylor Ho <taylorkmho@gmail.com>
Agent-deleted messages were not disappearing from the desktop UI until manual refresh. Agents delete via the CLI (sprout messages delete → build_delete_message), which emits kind:9005 (NIP-29 / Sprout-native). Desktop only subscribed to and walked kind:5 NIP-09 deletions, so 9005 events never landed in cache and never registered as deletion markers in formatTimelineMessages.deletedEventIds. Two narrow desktop changes: 1. Add KIND_NIP29_DELETE_EVENT (9005) to CHANNEL_EVENT_KINDS so the channel subscription and history fetch both pick it up. 2. Treat kind:9005 the same as kind:5 in formatTimelineMessages — walk both kinds, extract target ids from e tags. Tag shape is identical between the two builders so getDeletionTargets works unmodified. Why not collapse agents back to kind:5: the relay handles 9005 specifically — it soft-deletes the target AND emits a kind:40099 system message used as an audit trail. kind:5's relay handler emits no 40099. Switching agents to kind:5 would silently drop the audit trail. Two kinds, two jobs. Tests: new formatTimelineMessages.test.mjs covers kind:5 and kind:9005 deletion-marker behavior plus negative cases (non-deletion kinds with e tags, malformed targets). Loader fix: test-loader-hooks.mjs unconditionally appended .ts to @/ specifiers, so any .ts file that imports a sibling .mjs (like formatTimelineMessages → applyEditTagOverlay.mjs) couldn't be loaded in tests. Preserve explicit extensions; only synthesize .ts when the specifier has no extension. Signed-off-by: Taylor Ho <taylorkmho@gmail.com>
dbb3766 to
4da35ee
Compare
| target_event_id: EventId, | ||
| ) -> Result<EventBuilder, String> { | ||
| let tags = vec![ | ||
| tag(vec!["h", &channel_id.to_string()])?, |
There was a problem hiding this comment.
This h tag makes kind:5 deletes visible to the live channel/DM subscription, which fixes the cache path but also creates a new notification path for deletions.
useLiveChannelUpdates.handleDmEvent currently calls onDmMessage for any external DM event with an h tag, without checking event.kind (desktop/src/features/channels/useLiveChannelUpdates.ts:110-140). AppShell then renders empty-content events as a “New message” desktop notification (desktop/src/app/AppShell.tsx:241-248). So if another participant deletes a message in an inactive DM, this can produce a bogus “New message” notification.
Please gate DM notifications to CHANNEL_MESSAGE_EVENT_KINDS / UNREAD_TRIGGER_KINDS (or explicitly ignore deletion kinds) before tracking/sending the DM notification.
There was a problem hiding this comment.
Good catch — fixed in 5f9a426.
handleDmEvent now short-circuits on non-message kinds via a new isDmNotifiableKind() helper that wraps CHANNEL_MESSAGE_EVENT_KINDS (mirrors the UNREAD_TRIGGER_KINDS pattern in the same file). Gate runs before any side-effecting work in the handler, so kind:5 / 7 / 9005 / 40003 / 40008 / 40099 no longer flow through to onDmMessage → AppShell.handleDmNotification.
Added a regression test covering kind:9 / 40002 / 45001 / 45003 (notify) vs kind:5 / 7 / 9005 / 40003 / 40008 / 40099 (don't notify).
| export const CHANNEL_EVENT_KINDS = [ | ||
| KIND_DELETION, // 5 — NIP-09 event deletions | ||
| KIND_REACTION, // 7 — NIP-25 reactions | ||
| KIND_NIP29_DELETE_EVENT, // 9005 — NIP-29 / Sprout-native deletions |
There was a problem hiding this comment.
This fixes desktop’s subscription set for kind:9005, but the mobile mirror is now out of sync and will still show agent/CLI-deleted messages.
mobile/lib/shared/relay/nostr_models.dart says EventKind.channelEventKinds matches desktop, but it still omits 9005. mobile/lib/features/channels/timeline_message.dart:214-223 also only treats kind:5 as a deletion marker. Result: desktop hides Sprout-native deletes immediately, while mobile neither subscribes to nor filters the kind:9005 delete event.
Either include the mobile kind/formatter update in this PR, or explicitly scope this PR as desktop-only and track the mobile fix immediately.
There was a problem hiding this comment.
Bundled into this PR — mobile is now in sync.
- 0f2bccd — added
EventKind.nip29DeleteEvent(9005) tomobile/lib/shared/relay/nostr_models.dartand included it inchannelEventKinds. TaughtformatTimeline's deletion walker (mobile/lib/features/channels/timeline_message.dart) to accept both kind:5 and kind:9005 with identical e-tag extraction. Mirrors a kind:9005 timeline test against the existing kind:5 case. - cca99eb — also tagged the emit side:
mobile/lib/features/channels/channel_management_provider.dart'sdeleteMessagenow emits kind:5 with the channelhtag alongsidee, matching desktop'sbuild_delete_compat. So deletes originating on mobile are observable to other clients' h-scoped channel subs without needing a non-channel-scoped backfill. - Mobile DM-notif audit: scanned
mobile/lib— no equivalent of desktop'shandleDmEvent → AppShell.handleDmNotificationOS-notification flow exists today, so there's no latent kind-gating bug on the mobile side to fix in parallel.
flutter test 382/382 ✅ • flutter analyze clean.
Address PR #918 review feedback (wesbillman / Pinky). The h-tag fix on kind:5 deletes made them visible to the live DM subscription ({ kinds, "#h":[dmChannelId] }). handleDmEvent never gated by kind, so deleting a DM message could fire a phantom "New message" desktop notification on the receiving side. Reactions (7), kind:9005, edits (40003), diffs (40008), and system messages (40099) all flow through the same subscription post-fix and have the same latent bug. Add isDmNotifiableKind() — wraps CHANNEL_MESSAGE_EVENT_KINDS, the same set already used by UNREAD_TRIGGER_KINDS in the same file for the same semantic question ("is this a real message?"). handleDmEvent short-circuits at the top when the kind isn't notifiable. Also trim the over-explanatory comments on build_delete_compat (both SDK and src-tauri copies), KIND_NIP29_DELETE_EVENT, and the formatTimelineMessages deletion-walker block per Pinky's nit. Tests: regression guard for the DM kind gate covering kind:9 / 40002 / 45001 / 45003 (notify) and kind:5 / 7 / 9005 / 40003 / 40008 / 40099 (don't notify). Signed-off-by: Taylor Ho <taylorkmho@gmail.com>
Bring mobile to parity with desktop's kind:9005 handling. Agents emit kind:9005 deletes via the CLI; without this, mobile keeps rendering agent-deleted messages until manual refresh. Two narrow mobile changes: 1. Add EventKind.nip29DeleteEvent (9005) to nostr_models.dart and include it in channelEventKinds so the channel subscription and history fetch both pick it up. 2. Teach formatTimeline's deletion walker to treat kind:9005 the same as kind:5 — collect e-tag targets from both. Tag shape is identical between the two builders so the walker is unmodified beyond the kind check. No mobile DM-notification path was found that mirrors desktop's handleDmEvent → AppShell.handleDmNotification flow, so no DM kind-gating fix is needed on mobile today. Tests: timeline_message_test.dart adds a kind:9005 deletion case mirroring the existing kind:5 test. Signed-off-by: Taylor Ho <taylorkmho@gmail.com>
Wraps a long assert.equal() to satisfy biome's print-width rule. CI on the merge commit was failing format-check; locally on the branch alone the formatter was happy. Pure formatter delta, no logic change. Signed-off-by: Taylor Ho <taylorkmho@gmail.com>
Mobile's deleteMessage emitted a kind:5 deletion with only an `e` tag, so other clients' h-scoped channel subscriptions never observed the delete until a non-channel-scoped fetch backfilled it. Mirror desktop's build_delete_compat: emit both `['h', channelId]` and `['e', eventId]`. Threads channelId through ChannelActions.deleteMessage and its callers. Tag construction is extracted to buildDeleteMessageTags for testing. Signed-off-by: Taylor Ho <taylorkmho@gmail.com>
Apply dart format to satisfy CI's --set-exit-if-changed gate. Pure formatting; no behavior change. Signed-off-by: Taylor Ho <taylorkmho@gmail.com>
|
@wesbillman alright, some due diligence by testing on iOS simulator. been needing a reason to get the build running!
|


Problem
Agent-deleted messages were not disappearing from the desktop UI until manual refresh. Two issues, two paths:
build_delete_compatproduced kind:5 NIP-09 events with nohtag. Desktop's channel subscription filters by#h, so the event never matched and never reached the cache.sprout messages delete→cmd_delete_message→build_delete_message), which emits kind:9005 (NIP-29 / Sprout-native). Desktop only subscribed to kind:5 andformatTimelineMessagesonly walked kind:5, so 9005 events never registered as deletion markers.Refresh "fixed" it because some non-channel-scoped fetch eventually pulled the deletion in.
Fix
Two complementary commits, kept together because they're the same UX bug from two angles:
c7e5f3ed— tag kind:5 deletions with channel idAdd
["h", channelId]tobuild_delete_compat's tags, mirroringbuild_message. Threadchannel_idthrough the Tauridelete_messagecommand and update all four desktop callers (forum hooks, messages hook, HomeView, tauri.ts shim).dbb37662— recognize kind:9005 in desktop timelineKIND_NIP29_DELETE_EVENT(9005) toCHANNEL_EVENT_KINDSso the channel subscription and history fetch pick it up.formatTimelineMessagesto walk kind:5 and kind:9005 alike when buildingdeletedEventIds. Tag shape is identical (target id in theetag), sogetDeletionTargetsworks unmodified.Why not collapse agents back to kind:5
The relay handles 9005 specifically — soft-deletes the target and emits a kind:40099
message_deletedsystem message used as an audit trail. kind:5's relay handler emits no 40099. Switching agents to kind:5 would silently drop the audit trail. Two kinds, two jobs.Tests
New
desktop/src/features/messages/lib/formatTimelineMessages.test.mjscovers:etags do NOT hide the targetetag value is ignoredAlso fixed a small bug in
desktop/test-loader-hooks.mjsthat unconditionally appended.tsto@/specifiers, breaking.ts → .mjsimports.All 583 desktop tests pass;
pnpm typecheck,pnpm lint, andcargo check -p sprout-sdk -p sprout-cliall clean.Verification plan
sprout messages delete --event <id>against a message visible in the desktop app → should disappear immediately without refresh (kind:9005 path).Reported by @tho. Diagnosis discussed at sprout://message?channel=36411e44-0e2d-4cfe-bd6e-567eb169db9f&id=22ac0a10e5643bc30589e60276abf2a6a0cc8515f9c271b4f79f016d3c85be39