From 7845072f20322c7da82358b10f313d1494c9515e Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 30 Apr 2026 00:56:05 +0000 Subject: [PATCH] fix(nests): evict author from prior room when presence moves to a new room kind-10312 is replaceable per author, but the room a presence points to (`a`-tag) can change when a speaker hops between rooms. The replaceable cache only swaps the addressable's content -- it doesn't know which channel the old version was attached to, so the prior room kept the stale entry in both `notes` and the new `presenceNotes` index. NestsFeedFilter would then falsely surface the prior room as "live" via that author until the entry dropped out of the freshness window. Capture the prior room from the existing addressable before consumeBaseReplaceable swaps it. When the new event is a true replacement (createdAt > prior) and the room differs, drop the author from the prior channel's presenceNotes and remove the prior version note from its main notes index. --- .../amethyst/model/LocalCache.kt | 23 +++++++++++++++++++ .../LiveActivitiesChannel.kt | 11 +++++++++ 2 files changed, 34 insertions(+) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/LocalCache.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/LocalCache.kt index 49bf611fd..10a748f07 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/LocalCache.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/LocalCache.kt @@ -1560,12 +1560,27 @@ object LocalCache : ILocalCache, ICacheProvider { * Also indexed under [LiveActivitiesChannel.presenceNotes] keyed * by author so the Nests feed can answer "are there speakers on * stage?" without scanning the chat-dominated `notes` map. + * + * Cross-room move handling: kind-10312 is replaceable per author, + * but the room a presence points to (`a`-tag) can change when a + * speaker hops between rooms. The replaceable cache only swaps the + * addressable's content — it has no notion of which channel the + * old version was attached to. Without explicit eviction the old + * room would keep surfacing as "live" via the stale entry until it + * dropped out of the freshness window. Capture the prior room + * before replacement and, when it differs, drop the author from + * the old channel's presence index and the old version note from + * its main `notes` index. */ fun consume( event: MeetingRoomPresenceEvent, relay: NormalizedRelayUrl?, wasVerified: Boolean, ): Boolean { + val priorVersion = getAddressableNoteIfExists(event.address())?.event as? MeetingRoomPresenceEvent + val priorRoomAddress = priorVersion?.interactiveRoom()?.address + val isReplacement = priorVersion != null && event.createdAt > priorVersion.createdAt + val new = consumeBaseReplaceable(event, relay, wasVerified) // The replaceable cache keys this on the AUTHOR's address @@ -1574,6 +1589,14 @@ object LocalCache : ILocalCache, ICacheProvider { // note to the room's channel keyed by its kind-30312 address. val roomAddress = event.interactiveRoom()?.address ?: return new if (roomAddress.kind != MeetingSpaceEvent.KIND) return new + + if (isReplacement && priorRoomAddress != null && priorRoomAddress != roomAddress) { + getLiveActivityChannelIfExists(priorRoomAddress)?.let { priorChannel -> + priorChannel.removePresenceNote(event.pubKey) + getNoteIfExists(priorVersion.id)?.let { priorChannel.removeNote(it) } + } + } + val channel = getOrCreateLiveChannel(roomAddress) val versionNote = getOrCreateNote(event.id) channel.addNote(versionNote, relay) diff --git a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/nip53LiveActivities/LiveActivitiesChannel.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/nip53LiveActivities/LiveActivitiesChannel.kt index fc982ddf9..1857bcffc 100644 --- a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/nip53LiveActivities/LiveActivitiesChannel.kt +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/nip53LiveActivities/LiveActivitiesChannel.kt @@ -60,6 +60,17 @@ class LiveActivitiesChannel( presenceNotes.put(author, note) } + /** + * Drop an author's presence entry. Called by `LocalCache` when a + * replaceable kind-10312 from this author lands in a *different* + * room — without this eviction, the old room would keep surfacing + * as "live" via stale presence until it drops out of the + * freshness window. + */ + fun removePresenceNote(author: HexKey) { + presenceNotes.remove(author) + } + fun address() = address override fun relays() = info?.allRelayUrls()?.toSet()?.ifEmpty { null } ?: super.relays()