From 7e45042c275c605f6e25bb34d1f5e9a081d95d07 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 20 Apr 2026 15:58:00 +0000 Subject: [PATCH 1/2] fix: refresh Marmot group members list when returning to info screen Replace LaunchedEffect with LifecycleResumeEffect so the members list (and chatroom.memberCount) is re-queried every time MarmotGroupInfoScreen resumes, not just when nostrGroupId changes. Previously, after adding a member via the AddMemberScreen and popping back, the members count displayed in the header and chat top bar stayed stale until the screen was fully recreated. --- .../loggedIn/chats/marmotGroup/MarmotGroupInfoScreen.kt | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) 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 a724f2f53..1979c6076 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 @@ -47,7 +47,6 @@ import androidx.compose.material3.Text import androidx.compose.material3.TextButton import androidx.compose.material3.TopAppBar import androidx.compose.runtime.Composable -import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember @@ -59,6 +58,7 @@ import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp +import androidx.lifecycle.compose.LifecycleResumeEffect import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.vitorpamplona.amethyst.commons.marmot.GroupMemberInfo import com.vitorpamplona.amethyst.ui.navigation.navs.INav @@ -92,9 +92,10 @@ fun MarmotGroupInfoScreen( val myPubkey = accountViewModel.account.signer.pubKey val context = LocalContext.current - LaunchedEffect(nostrGroupId) { + LifecycleResumeEffect(nostrGroupId) { members = accountViewModel.marmotGroupMembers(nostrGroupId) chatroom.memberCount.value = members.size + onPauseOrDispose {} } Scaffold( From b6e31b21c852974d7ce551d771fea74876dc4cb5 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 20 Apr 2026 16:27:31 +0000 Subject: [PATCH 2/2] refactor: make Marmot group members list reactive Expose the member list as MutableStateFlow> on MarmotGroupChatroom, populated by MarmotManager.syncMetadataTo alongside the existing memberCount. MarmotGroupInfoScreen and RemoveMemberScreen now observe it via collectAsStateWithLifecycle, so both remote commits (already routed through syncMetadataTo in DecryptAndIndexProcessor) and local mutations refresh the UI without manual re-queries. Account.addMarmotGroupMember and removeMarmotGroupMember now call syncMetadataTo right after the MLS state is mutated, mirroring the pattern already used by updateMarmotGroupMetadata, so the local change is visible immediately without waiting for the relay round-trip. --- .../com/vitorpamplona/amethyst/model/Account.kt | 9 +++++++++ .../chats/marmotGroup/MarmotGroupInfoScreen.kt | 9 +-------- .../chats/marmotGroup/RemoveMemberScreen.kt | 13 ++++++------- .../amethyst/commons/marmot/MarmotManager.kt | 4 +++- .../model/marmotGroups/MarmotGroupChatroom.kt | 2 ++ 5 files changed, 21 insertions(+), 16 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 79a6d4a8a..aad6c7291 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/Account.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/Account.kt @@ -2070,6 +2070,13 @@ class Account( relays = groupRelays, ) + // The MLS commit has already been applied to the local group state — + // surface the new member list in the chatroom now so observers (e.g. + // MarmotGroupInfoScreen) update without waiting for our own commit to + // loop back through the relay. + val chatroom = marmotGroupList.getOrCreateGroup(nostrGroupId) + manager.syncMetadataTo(nostrGroupId, chatroom) + Log.d("MarmotDbg") { "addMarmotGroupMember: built commit kind=${commitEvent.signedEvent.kind} id=${commitEvent.signedEvent.id.take(8)}… " + "welcomeDelivery=${if (welcomeDelivery != null) "present(giftWrapId=${welcomeDelivery.giftWrapEvent.id.take(8)}…)" else "null"}" @@ -2270,6 +2277,8 @@ class Account( if (!isWriteable()) return val outbound = manager.removeMember(nostrGroupId, targetLeafIndex) + val chatroom = marmotGroupList.getOrCreateGroup(nostrGroupId) + manager.syncMetadataTo(nostrGroupId, chatroom) 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 1979c6076..706e10f64 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 @@ -58,7 +58,6 @@ import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp -import androidx.lifecycle.compose.LifecycleResumeEffect import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.vitorpamplona.amethyst.commons.marmot.GroupMemberInfo import com.vitorpamplona.amethyst.ui.navigation.navs.INav @@ -85,19 +84,13 @@ fun MarmotGroupInfoScreen( val groupDescription by chatroom.description.collectAsStateWithLifecycle() val adminPubkeys by chatroom.adminPubkeys.collectAsStateWithLifecycle() val groupRelays by chatroom.relays.collectAsStateWithLifecycle() - var members by remember { mutableStateOf(emptyList()) } + val members by chatroom.members.collectAsStateWithLifecycle() var showLeaveDialog by remember { mutableStateOf(false) } var isLeaving by remember { mutableStateOf(false) } val scope = rememberCoroutineScope() val myPubkey = accountViewModel.account.signer.pubKey val context = LocalContext.current - LifecycleResumeEffect(nostrGroupId) { - members = accountViewModel.marmotGroupMembers(nostrGroupId) - chatroom.memberCount.value = members.size - onPauseOrDispose {} - } - Scaffold( topBar = { TopAppBar( diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/marmotGroup/RemoveMemberScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/marmotGroup/RemoveMemberScreen.kt index f6b7b56da..9afb0ebb0 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/marmotGroup/RemoveMemberScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/marmotGroup/RemoveMemberScreen.kt @@ -44,7 +44,6 @@ import androidx.compose.material3.Text import androidx.compose.material3.TextButton import androidx.compose.material3.TopAppBar import androidx.compose.runtime.Composable -import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember @@ -55,6 +54,7 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp +import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.vitorpamplona.amethyst.commons.marmot.GroupMemberInfo import com.vitorpamplona.amethyst.ui.navigation.navs.INav import com.vitorpamplona.amethyst.ui.note.UserPicture @@ -71,17 +71,17 @@ fun RemoveMemberScreen( accountViewModel: AccountViewModel, nav: INav, ) { - var members by remember { mutableStateOf(emptyList()) } + val chatroom = + remember(nostrGroupId) { + accountViewModel.account.marmotGroupList.getOrCreateGroup(nostrGroupId) + } + val members by chatroom.members.collectAsStateWithLifecycle() var memberToRemove by remember { mutableStateOf(null) } var isRemoving by remember { mutableStateOf(false) } val scope = rememberCoroutineScope() val myPubkey = accountViewModel.account.signer.pubKey val context = LocalContext.current - LaunchedEffect(nostrGroupId) { - members = accountViewModel.marmotGroupMembers(nostrGroupId) - } - Scaffold( topBar = { TopAppBar( @@ -155,7 +155,6 @@ fun RemoveMemberScreen( scope.launch(Dispatchers.IO) { try { accountViewModel.removeMarmotGroupMember(nostrGroupId, member.leafIndex) - members = accountViewModel.marmotGroupMembers(nostrGroupId) isRemoving = false launch(Dispatchers.Main) { Toast diff --git a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/marmot/MarmotManager.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/marmot/MarmotManager.kt index d56abe2c0..6592db1b5 100644 --- a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/marmot/MarmotManager.kt +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/marmot/MarmotManager.kt @@ -437,7 +437,9 @@ class MarmotManager( chatroom.adminPubkeys.value = metadata.adminPubkeys chatroom.relays.value = metadata.relays } - chatroom.memberCount.value = memberCount(nostrGroupId) + val members = memberPubkeys(nostrGroupId) + chatroom.members.value = members + chatroom.memberCount.value = members.size } } 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 54b7f102e..bf9ec65a0 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 @@ -21,6 +21,7 @@ package com.vitorpamplona.amethyst.commons.model.marmotGroups import androidx.compose.runtime.Stable +import com.vitorpamplona.amethyst.commons.marmot.GroupMemberInfo import com.vitorpamplona.amethyst.commons.model.Channel.Companion.DefaultFeedOrder import com.vitorpamplona.amethyst.commons.model.ListChange import com.vitorpamplona.amethyst.commons.model.Note @@ -46,6 +47,7 @@ class MarmotGroupChatroom( var adminPubkeys = MutableStateFlow>(emptyList()) var relays = MutableStateFlow>(emptyList()) var memberCount = MutableStateFlow(0) + var members = MutableStateFlow>(emptyList()) var newestMessage: Note? = null val unreadCount = MutableStateFlow(0)