From 3e14022e38bdc8142de968694c47c6b3f1972a35 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 5 Apr 2026 21:22:46 +0000 Subject: [PATCH] refactor: convert Marmot dialogs to full route screens MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replace AlertDialog-based CreateGroupDialog and AddMemberDialog with proper full-screen routes that slide up from the bottom, following the existing app patterns (ChannelMetadataEdit, NewGroupDM, etc.). New screens: - CreateGroupScreen: uses CreatingTopBar with close/create buttons, replaces CreateGroupDialog - AddMemberScreen: uses ActionTopBar with close/add buttons, replaces AddMemberDialog New routes: - Route.CreateMarmotGroup → composableFromBottom - Route.MarmotGroupAddMember(nostrGroupId) → composableFromBottomArgs Updated call sites: - MarmotGroupListScreen: FAB navigates to Route.CreateMarmotGroup - MarmotGroupChatScreen: add-member icon navigates to Route.MarmotGroupAddMember - MarmotGroupInfoScreen: add-member icon navigates to Route.MarmotGroupAddMember Deleted old files: - CreateGroupDialog.kt - AddMemberDialog.kt https://claude.ai/code/session_0194SxKfAU61PY92eqP4cCXM --- .../amethyst/ui/navigation/AppNavigation.kt | 5 + .../amethyst/ui/navigation/routes/Routes.kt | 6 + ...{AddMemberDialog.kt => AddMemberScreen.kt} | 121 ++++++++++-------- ...ateGroupDialog.kt => CreateGroupScreen.kt} | 88 +++++++------ .../marmotGroup/MarmotGroupChatScreen.kt | 16 +-- .../marmotGroup/MarmotGroupInfoScreen.kt | 11 +- .../marmotGroup/MarmotGroupListScreen.kt | 14 +- 7 files changed, 129 insertions(+), 132 deletions(-) rename amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/marmotGroup/{AddMemberDialog.kt => AddMemberScreen.kt} (63%) rename amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/marmotGroup/{CreateGroupDialog.kt => CreateGroupScreen.kt} (60%) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/AppNavigation.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/AppNavigation.kt index d56828777..3a75e252c 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/AppNavigation.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/AppNavigation.kt @@ -71,6 +71,8 @@ import com.vitorpamplona.amethyst.ui.screen.loggedIn.bookmarkgroups.list.metadat import com.vitorpamplona.amethyst.ui.screen.loggedIn.bookmarkgroups.membershipManagement.ArticleBookmarkListManagementScreen import com.vitorpamplona.amethyst.ui.screen.loggedIn.bookmarkgroups.membershipManagement.PostBookmarkListManagementScreen import com.vitorpamplona.amethyst.ui.screen.loggedIn.bookmarkgroups.old.OldBookmarkListScreen +import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.marmotGroup.AddMemberScreen +import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.marmotGroup.CreateGroupScreen import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.marmotGroup.MarmotGroupChatScreen import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.marmotGroup.MarmotGroupInfoScreen import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.marmotGroup.MarmotGroupListScreen @@ -289,6 +291,9 @@ fun BuildNavigation( composableFromEndArgs { MarmotGroupChatScreen(it.nostrGroupId, accountViewModel, nav) } composableFromEndArgs { MarmotGroupInfoScreen(it.nostrGroupId, accountViewModel, nav) } + composableFromBottom { CreateGroupScreen(accountViewModel, nav) } + composableFromBottomArgs { AddMemberScreen(it.nostrGroupId, accountViewModel, nav) } + composableFromEndArgs { PublicChatChannelScreen(it.id, it.draftId, it.replyTo, accountViewModel, nav) } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/routes/Routes.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/routes/Routes.kt index a9d4a56b8..58e59fa45 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/routes/Routes.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/routes/Routes.kt @@ -273,6 +273,12 @@ sealed class Route { val nostrGroupId: String, ) : Route() + @Serializable object CreateMarmotGroup : Route() + + @Serializable data class MarmotGroupAddMember( + val nostrGroupId: String, + ) : Route() + @Serializable data class NewGroupDM( val message: String? = null, val attachment: String? = null, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/marmotGroup/AddMemberDialog.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/marmotGroup/AddMemberScreen.kt similarity index 63% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/marmotGroup/AddMemberDialog.kt rename to amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/marmotGroup/AddMemberScreen.kt index 70eb7c6c1..b7fccb22d 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/marmotGroup/AddMemberDialog.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/marmotGroup/AddMemberScreen.kt @@ -21,13 +21,14 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.marmotGroup import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.consumeWindowInsets import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.imePadding import androidx.compose.foundation.layout.padding -import androidx.compose.material3.AlertDialog import androidx.compose.material3.MaterialTheme import androidx.compose.material3.OutlinedTextField +import androidx.compose.material3.Scaffold import androidx.compose.material3.Text -import androidx.compose.material3.TextButton import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf @@ -36,6 +37,8 @@ import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp +import com.vitorpamplona.amethyst.ui.navigation.navs.INav +import com.vitorpamplona.amethyst.ui.navigation.topbars.ActionTopBar import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.quartz.nip01Core.core.HexKey import com.vitorpamplona.quartz.nip19Bech32.Nip19Parser @@ -45,55 +48,22 @@ import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch @Composable -fun AddMemberDialog( +fun AddMemberScreen( nostrGroupId: HexKey, accountViewModel: AccountViewModel, - onDismiss: () -> Unit, + nav: INav, ) { var memberInput by remember { mutableStateOf("") } var statusMessage by remember { mutableStateOf(null) } var isAdding by remember { mutableStateOf(false) } val scope = rememberCoroutineScope() - AlertDialog( - onDismissRequest = onDismiss, - title = { Text("Add Member") }, - text = { - Column { - OutlinedTextField( - value = memberInput, - onValueChange = { - memberInput = it - statusMessage = null - }, - label = { Text("npub or hex pubkey") }, - modifier = Modifier.fillMaxWidth(), - singleLine = true, - ) - Text( - "Enter the member's npub or hex public key. " + - "They must have published a KeyPackage (kind:30443) to be added.", - modifier = Modifier.padding(top = 8.dp), - style = MaterialTheme.typography.bodySmall, - ) - if (statusMessage != null) { - Text( - text = statusMessage!!, - modifier = Modifier.padding(top = 8.dp), - style = MaterialTheme.typography.bodySmall, - color = - if (statusMessage!!.startsWith("Error")) { - MaterialTheme.colorScheme.error - } else { - MaterialTheme.colorScheme.primary - }, - ) - } - } - }, - confirmButton = { - TextButton( - onClick = { + Scaffold( + topBar = { + ActionTopBar( + postRes = com.vitorpamplona.amethyst.R.string.add, + onCancel = { nav.popBack() }, + onPost = { isAdding = true statusMessage = "Looking up KeyPackage..." scope.launch(Dispatchers.IO) { @@ -106,8 +76,8 @@ fun AddMemberDialog( } statusMessage = "Fetching KeyPackage for ${pubkey.take(8)}..." // TODO: Fetch KeyPackage from relays and call addMarmotGroupMember - // For now, show that the flow would proceed here - statusMessage = "Error: KeyPackage fetch not yet implemented. " + + statusMessage = + "Error: KeyPackage fetch not yet implemented. " + "The member's KeyPackage (kind:30443) must be fetched from relays." isAdding = false } catch (e: Exception) { @@ -116,28 +86,67 @@ fun AddMemberDialog( } } }, - enabled = !isAdding && memberInput.isNotBlank(), - ) { - Text(if (isAdding) "Adding..." else "Add") - } + isActive = { !isAdding && memberInput.isNotBlank() }, + ) }, - dismissButton = { - TextButton(onClick = onDismiss) { - Text("Cancel") + ) { padding -> + Column( + modifier = + Modifier + .padding(padding) + .consumeWindowInsets(padding) + .imePadding() + .padding(horizontal = 16.dp), + ) { + Text( + text = "Add Member", + style = MaterialTheme.typography.headlineSmall, + modifier = Modifier.padding(top = 16.dp, bottom = 8.dp), + ) + + OutlinedTextField( + value = memberInput, + onValueChange = { + memberInput = it + statusMessage = null + }, + label = { Text("npub or hex pubkey") }, + modifier = Modifier.fillMaxWidth(), + singleLine = true, + ) + + Text( + "Enter the member's npub or hex public key. " + + "They must have published a KeyPackage (kind:30443) to be added.", + modifier = Modifier.padding(top = 12.dp), + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.onSurfaceVariant, + ) + + if (statusMessage != null) { + Text( + text = statusMessage!!, + modifier = Modifier.padding(top = 12.dp), + style = MaterialTheme.typography.bodySmall, + color = + if (statusMessage!!.startsWith("Error")) { + MaterialTheme.colorScheme.error + } else { + MaterialTheme.colorScheme.primary + }, + ) } - }, - ) + } + } } private fun resolvePubkey(input: String): HexKey? { val trimmed = input.trim() - // Try hex pubkey if (trimmed.length == 64 && trimmed.all { it in '0'..'9' || it in 'a'..'f' }) { return trimmed } - // Try bech32 (npub / nprofile) val entity = Nip19Parser.uriToRoute("nostr:$trimmed")?.entity ?: Nip19Parser.uriToRoute(trimmed)?.entity diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/marmotGroup/CreateGroupDialog.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/marmotGroup/CreateGroupScreen.kt similarity index 60% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/marmotGroup/CreateGroupDialog.kt rename to amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/marmotGroup/CreateGroupScreen.kt index d8f4bb06f..a44befce5 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/marmotGroup/CreateGroupDialog.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/marmotGroup/CreateGroupScreen.kt @@ -21,12 +21,14 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.marmotGroup import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.consumeWindowInsets import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.imePadding import androidx.compose.foundation.layout.padding -import androidx.compose.material3.AlertDialog +import androidx.compose.material3.MaterialTheme import androidx.compose.material3.OutlinedTextField +import androidx.compose.material3.Scaffold import androidx.compose.material3.Text -import androidx.compose.material3.TextButton import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf @@ -35,66 +37,72 @@ import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp +import com.vitorpamplona.amethyst.ui.navigation.navs.INav +import com.vitorpamplona.amethyst.ui.navigation.routes.Route +import com.vitorpamplona.amethyst.ui.navigation.topbars.CreatingTopBar import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel -import com.vitorpamplona.quartz.nip01Core.core.HexKey import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch import kotlin.random.Random @Composable -fun CreateGroupDialog( +fun CreateGroupScreen( accountViewModel: AccountViewModel, - onDismiss: () -> Unit, - onGroupCreated: (HexKey) -> Unit, + nav: INav, ) { var groupName by remember { mutableStateOf("") } var isCreating by remember { mutableStateOf(false) } val scope = rememberCoroutineScope() - AlertDialog( - onDismissRequest = onDismiss, - title = { Text("Create Marmot Group") }, - text = { - Column { - OutlinedTextField( - value = groupName, - onValueChange = { groupName = it }, - label = { Text("Group Name") }, - modifier = Modifier.fillMaxWidth(), - singleLine = true, - ) - Text( - "A new MLS group will be created. You can add members after.", - modifier = Modifier.padding(top = 8.dp), - style = androidx.compose.material3.MaterialTheme.typography.bodySmall, - ) - } - }, - confirmButton = { - TextButton( - onClick = { + Scaffold( + topBar = { + CreatingTopBar( + onCancel = { nav.popBack() }, + onPost = { isCreating = true scope.launch(Dispatchers.IO) { val nostrGroupId = Random.nextBytes(32).joinToString("") { "%02x".format(it) } accountViewModel.createMarmotGroup(nostrGroupId) - // Set display name locally if (groupName.isNotBlank()) { accountViewModel.account.marmotGroupList .getOrCreateGroup(nostrGroupId) .displayName.value = groupName } - onGroupCreated(nostrGroupId) + nav.nav(Route.MarmotGroupChat(nostrGroupId)) } }, - enabled = !isCreating, - ) { - Text(if (isCreating) "Creating..." else "Create") - } + isActive = { !isCreating }, + ) }, - dismissButton = { - TextButton(onClick = onDismiss) { - Text("Cancel") - } - }, - ) + ) { padding -> + Column( + modifier = + Modifier + .padding(padding) + .consumeWindowInsets(padding) + .imePadding() + .padding(horizontal = 16.dp), + ) { + Text( + text = "Create Marmot Group", + style = MaterialTheme.typography.headlineSmall, + modifier = Modifier.padding(top = 16.dp, bottom = 8.dp), + ) + + OutlinedTextField( + value = groupName, + onValueChange = { groupName = it }, + label = { Text("Group Name") }, + modifier = Modifier.fillMaxWidth(), + singleLine = true, + ) + + Text( + "A new MLS group will be created. You can add members after.", + modifier = Modifier.padding(top = 12.dp), + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.onSurfaceVariant, + ) + } + } } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/marmotGroup/MarmotGroupChatScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/marmotGroup/MarmotGroupChatScreen.kt index 2af3843cd..7e4ded188 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/marmotGroup/MarmotGroupChatScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/marmotGroup/MarmotGroupChatScreen.kt @@ -31,13 +31,12 @@ import androidx.compose.material.icons.filled.GroupAdd import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.Icon import androidx.compose.material3.IconButton +import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.material3.TopAppBar import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember -import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.vitorpamplona.amethyst.ui.layouts.DisappearingScaffold @@ -59,7 +58,6 @@ fun MarmotGroupChatScreen( } val displayName by chatroom.displayName.collectAsStateWithLifecycle() val memberCount by chatroom.memberCount.collectAsStateWithLifecycle() - var showAddMemberDialog by remember { mutableStateOf(false) } DisappearingScaffold( isInvertedLayout = true, @@ -84,13 +82,13 @@ fun MarmotGroupChatScreen( if (memberCount > 0) { Text( text = "$memberCount members", - style = androidx.compose.material3.MaterialTheme.typography.bodySmall, + style = MaterialTheme.typography.bodySmall, ) } } }, actions = { - IconButton(onClick = { showAddMemberDialog = true }) { + IconButton(onClick = { nav.nav(Route.MarmotGroupAddMember(nostrGroupId)) }) { Icon( imageVector = Icons.Default.GroupAdd, contentDescription = "Add Member", @@ -109,12 +107,4 @@ fun MarmotGroupChatScreen( ) } } - - if (showAddMemberDialog) { - AddMemberDialog( - nostrGroupId = nostrGroupId, - accountViewModel = accountViewModel, - onDismiss = { showAddMemberDialog = false }, - ) - } } 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 b5764c16c..1ce542814 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 @@ -80,7 +80,6 @@ fun MarmotGroupInfoScreen( val displayName by chatroom.displayName.collectAsStateWithLifecycle() var members by remember { mutableStateOf(emptyList()) } var showLeaveDialog by remember { mutableStateOf(false) } - var showAddMemberDialog by remember { mutableStateOf(false) } val scope = rememberCoroutineScope() val myPubkey = accountViewModel.account.signer.pubKey @@ -102,7 +101,7 @@ fun MarmotGroupInfoScreen( }, title = { Text("Group Info") }, actions = { - IconButton(onClick = { showAddMemberDialog = true }) { + IconButton(onClick = { nav.nav(Route.MarmotGroupAddMember(nostrGroupId)) }) { Icon( imageVector = Icons.Default.GroupAdd, contentDescription = "Add Member", @@ -218,14 +217,6 @@ fun MarmotGroupInfoScreen( onDismiss = { showLeaveDialog = false }, ) } - - if (showAddMemberDialog) { - AddMemberDialog( - nostrGroupId = nostrGroupId, - accountViewModel = accountViewModel, - onDismiss = { showAddMemberDialog = false }, - ) - } } @Composable diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/marmotGroup/MarmotGroupListScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/marmotGroup/MarmotGroupListScreen.kt index 15ef25129..d4a211fe9 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/marmotGroup/MarmotGroupListScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/marmotGroup/MarmotGroupListScreen.kt @@ -70,7 +70,6 @@ fun MarmotGroupListScreen( accountViewModel: AccountViewModel, nav: INav, ) { - var showCreateGroupDialog by remember { mutableStateOf(false) } var groupList by remember { mutableStateOf(listOf>()) } val scope = rememberCoroutineScope() @@ -115,7 +114,7 @@ fun MarmotGroupListScreen( ) }, floatingActionButton = { - FloatingActionButton(onClick = { showCreateGroupDialog = true }) { + FloatingActionButton(onClick = { nav.nav(Route.CreateMarmotGroup) }) { Icon(Icons.Default.Add, contentDescription = "Create Group") } }, @@ -155,17 +154,6 @@ fun MarmotGroupListScreen( } } } - - if (showCreateGroupDialog) { - CreateGroupDialog( - accountViewModel = accountViewModel, - onDismiss = { showCreateGroupDialog = false }, - onGroupCreated = { groupId -> - showCreateGroupDialog = false - nav.nav(Route.MarmotGroupChat(groupId)) - }, - ) - } } private fun loadGroupList(