mirror of
https://github.com/vitorpamplona/amethyst.git
synced 2026-08-10 08:27:04 +00:00
fix(buzz): honor the relay's channel_deleted signal so deleted channels leave the list
The first pass only recorded a deletion when THIS device published the 9008, so a channel deleted on another Buzz client — or before the fix existed — kept showing and survived restarts. The Buzz relay's handle_delete_group soft-deletes the channel and its 39000/39001/39002 discovery events but emits NO member-removed notification and never retracts the kind-44100 that seeds the browse list; its only cross-device signal is the relay-signed kind-40099 system message with type=channel_deleted (verified against block/buzz side_effects.rs). Consume that signal: LocalCache records a relay-signed 40099 channel_deleted into RelayGroupDeletions (gated on isRelaySignedGroupEvent so a spoofed one can't hide a channel), and BuzzRelayImportViewModel.discover now also fetches the #h-scoped 40099 for its candidate channels and drops deleted ones — the relay keeps re-announcing their 44100 with blank metadata, so this is the only place they leave the list. Cross-device and retroactive: the relay replays the 40099 on subscribe. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016MNVEKhaAu4vQRZnXv3rfG
This commit is contained in:
@@ -41,6 +41,7 @@ import com.vitorpamplona.amethyst.commons.model.emphChat.EphemeralChatChannel
|
||||
import com.vitorpamplona.amethyst.commons.model.geohashChat.GeohashChatChannel
|
||||
import com.vitorpamplona.amethyst.commons.model.nip28PublicChats.PublicChatChannel
|
||||
import com.vitorpamplona.amethyst.commons.model.nip29RelayGroups.RelayGroupChannel
|
||||
import com.vitorpamplona.amethyst.commons.model.nip29RelayGroups.RelayGroupDeletions
|
||||
import com.vitorpamplona.amethyst.commons.model.nip53LiveActivities.LiveActivitiesChannel
|
||||
import com.vitorpamplona.amethyst.commons.model.observables.CreatedAtIdHexComparator
|
||||
import com.vitorpamplona.amethyst.commons.model.observables.EventListMatchingFilter
|
||||
@@ -114,6 +115,7 @@ import com.vitorpamplona.quartz.buzz.stream.StreamMessageScheduledEvent
|
||||
import com.vitorpamplona.quartz.buzz.stream.StreamMessageV2Event
|
||||
import com.vitorpamplona.quartz.buzz.stream.StreamReminderEvent
|
||||
import com.vitorpamplona.quartz.buzz.stream.SystemMessageEvent
|
||||
import com.vitorpamplona.quartz.buzz.stream.SystemMessagePayload
|
||||
import com.vitorpamplona.quartz.buzz.stream.sidecars.ChannelSummaryEvent
|
||||
import com.vitorpamplona.quartz.buzz.stream.sidecars.PresenceSnapshotEvent
|
||||
import com.vitorpamplona.quartz.buzz.teams.TeamEvent
|
||||
@@ -2223,6 +2225,30 @@ object LocalCache : ILocalCache, ICacheProvider, Dao {
|
||||
attachToRelayGroupIfScoped(event, relay)
|
||||
}
|
||||
|
||||
/**
|
||||
* A Buzz kind-40099 system message. It renders as a narration row in the channel feed (via
|
||||
* [consumeBuzzTimelineEvent]), but a `channel_deleted` one is also the **authoritative signal that
|
||||
* a channel is gone**: the relay soft-deletes the channel and its 39000/39001/39002 discovery
|
||||
* events but emits no member-removed notification and never retracts the kind-44100 that seeds the
|
||||
* browse list — so without this the deleted channel keeps re-appearing (a stale 44100 re-announced
|
||||
* every restart, its metadata now blank so it shows optimistically). Recording the delete in
|
||||
* [RelayGroupDeletions] filters it out of every list and persists it across restarts.
|
||||
*
|
||||
* Gated on [isRelaySignedGroupEvent] (the 40099 is signed by the relay keypair) so a spoofed
|
||||
* system message from a stray author can't hide a channel. Cross-device by construction: the relay
|
||||
* replays this on subscribe, so a channel deleted on Buzz web/desktop is honored here too.
|
||||
*/
|
||||
private fun consume(
|
||||
event: SystemMessageEvent,
|
||||
relay: NormalizedRelayUrl?,
|
||||
wasVerified: Boolean,
|
||||
): Boolean =
|
||||
consumeBuzzTimelineEvent(event, relay, wasVerified).also {
|
||||
if (relay != null && isRelaySignedGroupEvent(event, relay) && event.payload()?.type == SystemMessagePayload.CHANNEL_DELETED) {
|
||||
event.channel()?.let { channelId -> RelayGroupDeletions.markDeleted(GroupId(channelId, relay)) }
|
||||
}
|
||||
}
|
||||
|
||||
/** Store-only consume for Buzz kinds that carry no channel timeline row. */
|
||||
private fun consumeBuzzRegularEvent(
|
||||
event: Event,
|
||||
@@ -4798,7 +4824,7 @@ object LocalCache : ILocalCache, ICacheProvider, Dao {
|
||||
is StreamMessageV2Event -> consumeBuzzTimelineEvent(event, relay, wasVerified)
|
||||
is StreamMessageEditEvent -> consume(event, relay, wasVerified)
|
||||
is StreamMessageDiffEvent -> consumeBuzzTimelineEvent(event, relay, wasVerified)
|
||||
is SystemMessageEvent -> consumeBuzzTimelineEvent(event, relay, wasVerified)
|
||||
is SystemMessageEvent -> consume(event, relay, wasVerified)
|
||||
is CanvasEvent -> consume(event, relay, wasVerified)
|
||||
// Forum root (45001) is a thread, not a chat row → Threads collection. Comments (45003)
|
||||
// and votes (45002) are store-only: the forum-thread detail loads them on demand by root.
|
||||
|
||||
+20
-3
@@ -23,11 +23,13 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn.buzz
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import com.vitorpamplona.amethyst.commons.model.buzz.BuzzWorkspaces
|
||||
import com.vitorpamplona.amethyst.commons.model.nip29RelayGroups.RelayGroupDeletions
|
||||
import com.vitorpamplona.amethyst.commons.relayauth.RelayAuthDecision
|
||||
import com.vitorpamplona.amethyst.model.Account
|
||||
import com.vitorpamplona.amethyst.model.LocalCache
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.relayGroup.datasource.RELAY_GROUP_METADATA_KINDS
|
||||
import com.vitorpamplona.quartz.buzz.notifications.MemberAddedNotificationEvent
|
||||
import com.vitorpamplona.quartz.buzz.stream.SystemMessageEvent
|
||||
import com.vitorpamplona.quartz.buzz.workspace.isBuzzDm
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.client.accessories.fetchAllWithHooks
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter
|
||||
@@ -154,21 +156,36 @@ class BuzzRelayImportViewModel : ViewModel() {
|
||||
false
|
||||
}
|
||||
|
||||
// 2. Fetch each channel's NIP-29 metadata (39000-39003) so its name + Buzz `t` type load.
|
||||
// 2. Fetch each channel's NIP-29 metadata (39000-39003, `#d`-scoped) so its name + Buzz
|
||||
// `t` type load, AND the relay's kind-40099 system messages (`#h`-scoped) so a
|
||||
// `channel_deleted` one is seen. The relay soft-deletes a deleted channel's 39000
|
||||
// and never retracts the kind-44100 that seeded `channelIds`, so without pulling the
|
||||
// 40099 a deleted channel — its metadata now blank — would show optimistically here
|
||||
// forever. LocalCache records the delete into RelayGroupDeletions on consume.
|
||||
if (channelIds.isNotEmpty()) {
|
||||
account.client.fetchAllWithHooks(
|
||||
filters = mapOf(relay to listOf(Filter(kinds = RELAY_GROUP_METADATA_KINDS, tags = mapOf("d" to channelIds.toList())))),
|
||||
filters =
|
||||
mapOf(
|
||||
relay to
|
||||
listOf(
|
||||
Filter(kinds = RELAY_GROUP_METADATA_KINDS, tags = mapOf("d" to channelIds.toList())),
|
||||
Filter(kinds = listOf(SystemMessageEvent.KIND), tags = mapOf("h" to channelIds.toList())),
|
||||
),
|
||||
),
|
||||
timeoutMs = 8_000,
|
||||
pendingOnAuthRequired = true,
|
||||
) { _, _ -> false }
|
||||
}
|
||||
|
||||
// 3. Keep only non-DM workspace channels; a channel whose metadata hasn't arrived
|
||||
// (type unknown) is optimistically shown as a workspace channel.
|
||||
// (type unknown) is optimistically shown as a workspace channel. Deleted channels
|
||||
// (kind-40099 `channel_deleted`, recorded above) are dropped — the relay keeps
|
||||
// re-announcing their kind-44100, so this is the only place they leave the list.
|
||||
_channels.value =
|
||||
channelIds
|
||||
.mapNotNull { id ->
|
||||
val groupId = GroupId(id, relay)
|
||||
if (RelayGroupDeletions.isDeleted(groupId)) return@mapNotNull null
|
||||
val channel = LocalCache.getOrCreateRelayGroupChannel(groupId)
|
||||
if (channel.event?.isBuzzDm() == true) null else groupId
|
||||
}.sortedBy { it.id }
|
||||
|
||||
Reference in New Issue
Block a user