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()