diff --git a/crates/nip29/src/group.rs b/crates/nip29/src/group.rs index 2a41e138..2b38d146 100644 --- a/crates/nip29/src/group.rs +++ b/crates/nip29/src/group.rs @@ -485,6 +485,18 @@ impl Group { cx.notify(); } + pub(crate) fn forget(&mut self, id: EventId, cx: &mut Context) { + self.window.remove(id); + + if !self.index.remove(&id) { + return; + } + + self.messages.retain(|message| message.id != id); + cx.emit(GroupEvent::Updated); + cx.notify(); + } + pub(crate) fn eose(&mut self, cx: &mut Context) { self.loading = false; cx.emit(GroupEvent::Eose); diff --git a/crates/nip29/src/lib.rs b/crates/nip29/src/lib.rs index be42bdeb..663d3d5b 100644 --- a/crates/nip29/src/lib.rs +++ b/crates/nip29/src/lib.rs @@ -506,8 +506,12 @@ impl GroupsRegistry { key.clone(), cx.subscribe(&group, |this, group, event, cx| match event { GroupEvent::Sent(event) => { - let key = group.read(cx).key().clone(); - this.reload(key, Some(event.clone()), cx); + if event.kind == Kind::GroupDeleteEvent { + this.forget_event(group, event.clone(), cx); + } else { + let key = group.read(cx).key().clone(); + this.reload(key, Some(event.clone()), cx); + } } GroupEvent::Updated => { this.refresh_entry(group, cx); @@ -676,6 +680,40 @@ impl GroupsRegistry { })); } + fn forget_event(&mut self, group: Entity, event: Event, cx: &mut Context) { + let Some(target) = event.tags.event_ids().next() else { + return; + }; + + let key = group.read(cx).key().clone(); + let cache_tag = key.cache_tag(); + + let nostr = NostrRegistry::global(cx); + let client = nostr.read(cx).client(); + + let purge = cx.background_spawn(async move { + let filter = Filter::new() + .kind(Kind::ApplicationSpecificData) + .identifier(target.to_hex()) + .custom_tag(SingleLetterTag::LOWERCASE_R, cache_tag); + + client.database().delete(filter).await + }); + + self.tasks.push(cx.spawn(async move |this, cx| { + if let Err(error) = purge.await { + log::warn!("nip29: dropping the deleted envelope failed: {error}"); + } + this.update(cx, |this, cx| { + group.update(cx, |group, cx| { + group.forget(target, cx); + }); + this.reload(key, Some(event), cx); + }) + .ok(); + })); + } + fn handle_event(&mut self, id: SubscriptionId, event: Event, cx: &mut Context) { if id == list_subscription_id() { match GroupList::parse(&event) { diff --git a/crates/nip29/src/protocol.rs b/crates/nip29/src/protocol.rs index a1f8bf2a..a5647a74 100644 --- a/crates/nip29/src/protocol.rs +++ b/crates/nip29/src/protocol.rs @@ -819,6 +819,10 @@ impl TimelineWindow { self.0.truncate(TIMELINE_WINDOW); } + pub fn remove(&mut self, id: EventId) { + self.0.retain(|event| event.id != id); + } + pub fn previous_tag(&self, me: PublicKey) -> Option { let references: Vec = self .0 diff --git a/docs/nip29-usage.md b/docs/nip29-usage.md index 7eace0a1..7c4eaa64 100644 --- a/docs/nip29-usage.md +++ b/docs/nip29-usage.md @@ -353,10 +353,9 @@ reject anything it does not want. must carry every child tag, because a `9002` that drops one is rejected by the relay; the builder re-emits them from the struct. -`delete_event` is a **relay** instruction only. The relay stops serving the event, -but the local timeline keeps it: the backend does not evict the envelope or the -message, so a deletion the relay accepted is still visible locally until the UI -phase drops it. That is the one moderation write left incomplete (see below). +`delete_event` sends `9005`, and once the relay accepts it the registry drops the +target's cached envelope, evicts the row and re-reads the group, so the timeline stops +showing it. Remote deletions are not folded in: the standing REQ does not carry `9005`. ## Pins @@ -551,10 +550,8 @@ before they reach the consumer, so a replay cannot double-hydrate. `chat::Message` rendering lifted (or copied) out of `ChatPanel`: the data type is already shared, only the row rendering is not. `crates/workspace/src/panels/browse.rs` is the stub for browsing. -- **`9005` delete does not touch local state.** The relay stops serving the - event, but the envelope and the row stay until a reload. Dropping it needs a - filter on the target's `d` tag plus the group's `r` cache tag, and an eviction - from `messages` and the id index; nothing does that today. +- **Remote deletions are not folded in.** A `9005` from another admin never reaches the + standing REQ, so only our own accepted deletes evict the row. - **The LiveKit response shape is a guess.** The NIP names no fields, so `LivekitSession::parse` reads `token` (or `jwt`) and `url` (or `server_url`) and fails loudly on anything else. Re-check it against the relay actually targeted