From 62f4599407419f24383628ef02f97fde0beb4c37 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 23 Apr 2026 16:07:14 +0000 Subject: [PATCH] fix: drop Marmot chatroom on leave so messages clear from caches MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When the local user leaves a Marmot MLS group, MarmotManager already wipes MLS state, the relay subscription and the persisted message log, but the chatroom (and its decrypted inner messages) lingered in marmotGroupList until the UI happened to call removeGroup. The CLI leave path skipped that step entirely, and removeGroup itself only dropped the chatroom from rooms — it left noteToGroupIndex entries and the chatroom's own messages set in place, so notes stayed strongly referenced and kept appearing in the Notification feed. Move the in-memory cleanup into Account.leaveMarmotGroup, and have MarmotGroupList.removeGroup also clear the chatroom's messages and the note→group index. LocalCache holds notes weakly, so cutting the strong refs is enough — GC reclaims them and the existing acceptableEvent filter (which keys off marmotGroupList.rooms) hides anything still in-flight. --- .../com/vitorpamplona/amethyst/model/Account.kt | 6 ++++++ .../chats/marmotGroup/MarmotGroupInfoScreen.kt | 1 - .../model/marmotGroups/MarmotGroupChatroom.kt | 17 +++++++++++++++++ .../model/marmotGroups/MarmotGroupList.kt | 9 ++++++++- 4 files changed, 31 insertions(+), 2 deletions(-) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/Account.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/Account.kt index 04ff884a5..12bac04e4 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/Account.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/Account.kt @@ -2319,6 +2319,12 @@ class Account( } val outbound = manager.leaveGroup(nostrGroupId) + // manager.leaveGroup already wiped MLS state, relay subscriptions and + // the persisted message log. Drop the in-memory chatroom too — that + // releases the strong refs to the decrypted inner notes so LocalCache + // (which holds them weakly) can GC them, and the Notification feed + // (which iterates marmotGroupList.rooms) stops surfacing the group. + marmotGroupList.removeGroup(nostrGroupId) client.publish(outbound.signedEvent, groupRelays) } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/marmotGroup/MarmotGroupInfoScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/marmotGroup/MarmotGroupInfoScreen.kt index b83de760a..46da171b5 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/marmotGroup/MarmotGroupInfoScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/marmotGroup/MarmotGroupInfoScreen.kt @@ -316,7 +316,6 @@ fun MarmotGroupInfoScreen( scope.launch(Dispatchers.IO) { try { accountViewModel.leaveMarmotGroup(nostrGroupId) - accountViewModel.account.marmotGroupList.removeGroup(nostrGroupId) nav.nav(Route.Message) } catch (e: Exception) { isLeaving = false diff --git a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/marmotGroups/MarmotGroupChatroom.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/marmotGroups/MarmotGroupChatroom.kt index cd106aabd..140478395 100644 --- a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/marmotGroups/MarmotGroupChatroom.kt +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/marmotGroups/MarmotGroupChatroom.kt @@ -202,4 +202,21 @@ class MarmotGroupChatroom( changesFlow.get()?.tryEmit(ListChange.SetDeletion(toRemove)) return toRemove } + + /** + * Drop every message from this chatroom. Used when the local user leaves + * the group: cuts the strong references held in [messages] so the + * decrypted inner notes become eligible for GC out of LocalCache (which + * holds them weakly). + */ + @Synchronized + fun clearAllMessagesSync(): Set { + val toRemove = messages + if (toRemove.isEmpty()) return toRemove + messages = emptySet() + newestMessage = null + unreadCount.value = 0 + changesFlow.get()?.tryEmit(ListChange.SetDeletion(toRemove)) + return toRemove + } } diff --git a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/marmotGroups/MarmotGroupList.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/marmotGroups/MarmotGroupList.kt index 5e622ead4..589a40a8e 100644 --- a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/marmotGroups/MarmotGroupList.kt +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/marmotGroups/MarmotGroupList.kt @@ -127,8 +127,15 @@ class MarmotGroupList( } } + /** + * Drop a group from the in-memory list. Also clears the chatroom's own + * message set and the note→group index: LocalCache holds notes weakly, so + * once these strong references go away the decrypted inner messages + * become eligible for GC and stop appearing in the Notification feed. + */ fun removeGroup(nostrGroupId: HexKey) { - rooms.remove(nostrGroupId) + val chatroom = rooms.remove(nostrGroupId) + chatroom?.clearAllMessagesSync()?.forEach { noteToGroupIndex.remove(it.idHex) } _groupListChanges.tryEmit(nostrGroupId) }