refactor: convert Marmot dialogs to full route screens

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
This commit is contained in:
Claude
2026-04-05 21:22:46 +00:00
parent ddca55fae6
commit 3e14022e38
7 changed files with 129 additions and 132 deletions
@@ -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.ArticleBookmarkListManagementScreen
import com.vitorpamplona.amethyst.ui.screen.loggedIn.bookmarkgroups.membershipManagement.PostBookmarkListManagementScreen 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.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.MarmotGroupChatScreen
import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.marmotGroup.MarmotGroupInfoScreen import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.marmotGroup.MarmotGroupInfoScreen
import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.marmotGroup.MarmotGroupListScreen import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.marmotGroup.MarmotGroupListScreen
@@ -289,6 +291,9 @@ fun BuildNavigation(
composableFromEndArgs<Route.MarmotGroupChat> { MarmotGroupChatScreen(it.nostrGroupId, accountViewModel, nav) } composableFromEndArgs<Route.MarmotGroupChat> { MarmotGroupChatScreen(it.nostrGroupId, accountViewModel, nav) }
composableFromEndArgs<Route.MarmotGroupInfo> { MarmotGroupInfoScreen(it.nostrGroupId, accountViewModel, nav) } composableFromEndArgs<Route.MarmotGroupInfo> { MarmotGroupInfoScreen(it.nostrGroupId, accountViewModel, nav) }
composableFromBottom<Route.CreateMarmotGroup> { CreateGroupScreen(accountViewModel, nav) }
composableFromBottomArgs<Route.MarmotGroupAddMember> { AddMemberScreen(it.nostrGroupId, accountViewModel, nav) }
composableFromEndArgs<Route.PublicChatChannel> { composableFromEndArgs<Route.PublicChatChannel> {
PublicChatChannelScreen(it.id, it.draftId, it.replyTo, accountViewModel, nav) PublicChatChannelScreen(it.id, it.draftId, it.replyTo, accountViewModel, nav)
} }
@@ -273,6 +273,12 @@ sealed class Route {
val nostrGroupId: String, val nostrGroupId: String,
) : Route() ) : Route()
@Serializable object CreateMarmotGroup : Route()
@Serializable data class MarmotGroupAddMember(
val nostrGroupId: String,
) : Route()
@Serializable data class NewGroupDM( @Serializable data class NewGroupDM(
val message: String? = null, val message: String? = null,
val attachment: String? = null, val attachment: String? = null,
@@ -21,13 +21,14 @@
package com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.marmotGroup package com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.marmotGroup
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.consumeWindowInsets
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.imePadding
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.material3.AlertDialog
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.OutlinedTextField import androidx.compose.material3.OutlinedTextField
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
@@ -36,6 +37,8 @@ import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp 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.amethyst.ui.screen.loggedIn.AccountViewModel
import com.vitorpamplona.quartz.nip01Core.core.HexKey import com.vitorpamplona.quartz.nip01Core.core.HexKey
import com.vitorpamplona.quartz.nip19Bech32.Nip19Parser import com.vitorpamplona.quartz.nip19Bech32.Nip19Parser
@@ -45,55 +48,22 @@ import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
@Composable @Composable
fun AddMemberDialog( fun AddMemberScreen(
nostrGroupId: HexKey, nostrGroupId: HexKey,
accountViewModel: AccountViewModel, accountViewModel: AccountViewModel,
onDismiss: () -> Unit, nav: INav,
) { ) {
var memberInput by remember { mutableStateOf("") } var memberInput by remember { mutableStateOf("") }
var statusMessage by remember { mutableStateOf<String?>(null) } var statusMessage by remember { mutableStateOf<String?>(null) }
var isAdding by remember { mutableStateOf(false) } var isAdding by remember { mutableStateOf(false) }
val scope = rememberCoroutineScope() val scope = rememberCoroutineScope()
AlertDialog( Scaffold(
onDismissRequest = onDismiss, topBar = {
title = { Text("Add Member") }, ActionTopBar(
text = { postRes = com.vitorpamplona.amethyst.R.string.add,
Column { onCancel = { nav.popBack() },
OutlinedTextField( onPost = {
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 = {
isAdding = true isAdding = true
statusMessage = "Looking up KeyPackage..." statusMessage = "Looking up KeyPackage..."
scope.launch(Dispatchers.IO) { scope.launch(Dispatchers.IO) {
@@ -106,8 +76,8 @@ fun AddMemberDialog(
} }
statusMessage = "Fetching KeyPackage for ${pubkey.take(8)}..." statusMessage = "Fetching KeyPackage for ${pubkey.take(8)}..."
// TODO: Fetch KeyPackage from relays and call addMarmotGroupMember // TODO: Fetch KeyPackage from relays and call addMarmotGroupMember
// For now, show that the flow would proceed here statusMessage =
statusMessage = "Error: KeyPackage fetch not yet implemented. " + "Error: KeyPackage fetch not yet implemented. " +
"The member's KeyPackage (kind:30443) must be fetched from relays." "The member's KeyPackage (kind:30443) must be fetched from relays."
isAdding = false isAdding = false
} catch (e: Exception) { } catch (e: Exception) {
@@ -116,28 +86,67 @@ fun AddMemberDialog(
} }
} }
}, },
enabled = !isAdding && memberInput.isNotBlank(), isActive = { !isAdding && memberInput.isNotBlank() },
) { )
Text(if (isAdding) "Adding..." else "Add")
}
}, },
dismissButton = { ) { padding ->
TextButton(onClick = onDismiss) { Column(
Text("Cancel") 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? { private fun resolvePubkey(input: String): HexKey? {
val trimmed = input.trim() val trimmed = input.trim()
// Try hex pubkey
if (trimmed.length == 64 && trimmed.all { it in '0'..'9' || it in 'a'..'f' }) { if (trimmed.length == 64 && trimmed.all { it in '0'..'9' || it in 'a'..'f' }) {
return trimmed return trimmed
} }
// Try bech32 (npub / nprofile)
val entity = val entity =
Nip19Parser.uriToRoute("nostr:$trimmed")?.entity Nip19Parser.uriToRoute("nostr:$trimmed")?.entity
?: Nip19Parser.uriToRoute(trimmed)?.entity ?: Nip19Parser.uriToRoute(trimmed)?.entity
@@ -21,12 +21,14 @@
package com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.marmotGroup package com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.marmotGroup
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.consumeWindowInsets
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.imePadding
import androidx.compose.foundation.layout.padding 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.OutlinedTextField
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
@@ -35,66 +37,72 @@ import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp 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.amethyst.ui.screen.loggedIn.AccountViewModel
import com.vitorpamplona.quartz.nip01Core.core.HexKey
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlin.random.Random import kotlin.random.Random
@Composable @Composable
fun CreateGroupDialog( fun CreateGroupScreen(
accountViewModel: AccountViewModel, accountViewModel: AccountViewModel,
onDismiss: () -> Unit, nav: INav,
onGroupCreated: (HexKey) -> Unit,
) { ) {
var groupName by remember { mutableStateOf("") } var groupName by remember { mutableStateOf("") }
var isCreating by remember { mutableStateOf(false) } var isCreating by remember { mutableStateOf(false) }
val scope = rememberCoroutineScope() val scope = rememberCoroutineScope()
AlertDialog( Scaffold(
onDismissRequest = onDismiss, topBar = {
title = { Text("Create Marmot Group") }, CreatingTopBar(
text = { onCancel = { nav.popBack() },
Column { onPost = {
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 = {
isCreating = true isCreating = true
scope.launch(Dispatchers.IO) { scope.launch(Dispatchers.IO) {
val nostrGroupId = Random.nextBytes(32).joinToString("") { "%02x".format(it) } val nostrGroupId = Random.nextBytes(32).joinToString("") { "%02x".format(it) }
accountViewModel.createMarmotGroup(nostrGroupId) accountViewModel.createMarmotGroup(nostrGroupId)
// Set display name locally
if (groupName.isNotBlank()) { if (groupName.isNotBlank()) {
accountViewModel.account.marmotGroupList accountViewModel.account.marmotGroupList
.getOrCreateGroup(nostrGroupId) .getOrCreateGroup(nostrGroupId)
.displayName.value = groupName .displayName.value = groupName
} }
onGroupCreated(nostrGroupId) nav.nav(Route.MarmotGroupChat(nostrGroupId))
} }
}, },
enabled = !isCreating, isActive = { !isCreating },
) { )
Text(if (isCreating) "Creating..." else "Create")
}
}, },
dismissButton = { ) { padding ->
TextButton(onClick = onDismiss) { Column(
Text("Cancel") 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,
)
}
}
} }
@@ -31,13 +31,12 @@ import androidx.compose.material.icons.filled.GroupAdd
import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBar import androidx.compose.material3.TopAppBar
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.vitorpamplona.amethyst.ui.layouts.DisappearingScaffold import com.vitorpamplona.amethyst.ui.layouts.DisappearingScaffold
@@ -59,7 +58,6 @@ fun MarmotGroupChatScreen(
} }
val displayName by chatroom.displayName.collectAsStateWithLifecycle() val displayName by chatroom.displayName.collectAsStateWithLifecycle()
val memberCount by chatroom.memberCount.collectAsStateWithLifecycle() val memberCount by chatroom.memberCount.collectAsStateWithLifecycle()
var showAddMemberDialog by remember { mutableStateOf(false) }
DisappearingScaffold( DisappearingScaffold(
isInvertedLayout = true, isInvertedLayout = true,
@@ -84,13 +82,13 @@ fun MarmotGroupChatScreen(
if (memberCount > 0) { if (memberCount > 0) {
Text( Text(
text = "$memberCount members", text = "$memberCount members",
style = androidx.compose.material3.MaterialTheme.typography.bodySmall, style = MaterialTheme.typography.bodySmall,
) )
} }
} }
}, },
actions = { actions = {
IconButton(onClick = { showAddMemberDialog = true }) { IconButton(onClick = { nav.nav(Route.MarmotGroupAddMember(nostrGroupId)) }) {
Icon( Icon(
imageVector = Icons.Default.GroupAdd, imageVector = Icons.Default.GroupAdd,
contentDescription = "Add Member", contentDescription = "Add Member",
@@ -109,12 +107,4 @@ fun MarmotGroupChatScreen(
) )
} }
} }
if (showAddMemberDialog) {
AddMemberDialog(
nostrGroupId = nostrGroupId,
accountViewModel = accountViewModel,
onDismiss = { showAddMemberDialog = false },
)
}
} }
@@ -80,7 +80,6 @@ fun MarmotGroupInfoScreen(
val displayName by chatroom.displayName.collectAsStateWithLifecycle() val displayName by chatroom.displayName.collectAsStateWithLifecycle()
var members by remember { mutableStateOf(emptyList<GroupMemberInfo>()) } var members by remember { mutableStateOf(emptyList<GroupMemberInfo>()) }
var showLeaveDialog by remember { mutableStateOf(false) } var showLeaveDialog by remember { mutableStateOf(false) }
var showAddMemberDialog by remember { mutableStateOf(false) }
val scope = rememberCoroutineScope() val scope = rememberCoroutineScope()
val myPubkey = accountViewModel.account.signer.pubKey val myPubkey = accountViewModel.account.signer.pubKey
@@ -102,7 +101,7 @@ fun MarmotGroupInfoScreen(
}, },
title = { Text("Group Info") }, title = { Text("Group Info") },
actions = { actions = {
IconButton(onClick = { showAddMemberDialog = true }) { IconButton(onClick = { nav.nav(Route.MarmotGroupAddMember(nostrGroupId)) }) {
Icon( Icon(
imageVector = Icons.Default.GroupAdd, imageVector = Icons.Default.GroupAdd,
contentDescription = "Add Member", contentDescription = "Add Member",
@@ -218,14 +217,6 @@ fun MarmotGroupInfoScreen(
onDismiss = { showLeaveDialog = false }, onDismiss = { showLeaveDialog = false },
) )
} }
if (showAddMemberDialog) {
AddMemberDialog(
nostrGroupId = nostrGroupId,
accountViewModel = accountViewModel,
onDismiss = { showAddMemberDialog = false },
)
}
} }
@Composable @Composable
@@ -70,7 +70,6 @@ fun MarmotGroupListScreen(
accountViewModel: AccountViewModel, accountViewModel: AccountViewModel,
nav: INav, nav: INav,
) { ) {
var showCreateGroupDialog by remember { mutableStateOf(false) }
var groupList by remember { mutableStateOf(listOf<Pair<HexKey, MarmotGroupChatroom>>()) } var groupList by remember { mutableStateOf(listOf<Pair<HexKey, MarmotGroupChatroom>>()) }
val scope = rememberCoroutineScope() val scope = rememberCoroutineScope()
@@ -115,7 +114,7 @@ fun MarmotGroupListScreen(
) )
}, },
floatingActionButton = { floatingActionButton = {
FloatingActionButton(onClick = { showCreateGroupDialog = true }) { FloatingActionButton(onClick = { nav.nav(Route.CreateMarmotGroup) }) {
Icon(Icons.Default.Add, contentDescription = "Create Group") 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( private fun loadGroupList(