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.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<Route.MarmotGroupChat> { MarmotGroupChatScreen(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> {
PublicChatChannelScreen(it.id, it.draftId, it.replyTo, accountViewModel, nav)
}
@@ -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,
@@ -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<String?>(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
@@ -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,
)
}
}
}
@@ -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 },
)
}
}
@@ -80,7 +80,6 @@ fun MarmotGroupInfoScreen(
val displayName by chatroom.displayName.collectAsStateWithLifecycle()
var members by remember { mutableStateOf(emptyList<GroupMemberInfo>()) }
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
@@ -70,7 +70,6 @@ fun MarmotGroupListScreen(
accountViewModel: AccountViewModel,
nav: INav,
) {
var showCreateGroupDialog by remember { mutableStateOf(false) }
var groupList by remember { mutableStateOf(listOf<Pair<HexKey, MarmotGroupChatroom>>()) }
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(