feat: pin add-member field to bottom of Marmots group info with add button
Move the add-member search to a fixed position at the bottom of the screen (matching the New Short Note pattern) so it stays visible regardless of how far the user has scrolled through a large member list. Suggestions now render above the input, and each suggestion row shows an explicit PersonAdd icon button so the add action has a clear visual affordance. Adds an optional trailingContent slot to ShowUserSuggestionList / UserLine so callers can attach per-row actions without forking the component. https://claude.ai/code/session_01JbRycm4g3LrqatnCVWVdGh
This commit is contained in:
+6
-2
@@ -67,6 +67,7 @@ fun ShowUserSuggestionList(
|
|||||||
accountViewModel: AccountViewModel,
|
accountViewModel: AccountViewModel,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
onEmpty: @Composable () -> Unit = {},
|
onEmpty: @Composable () -> Unit = {},
|
||||||
|
trailingContent: (@Composable (User) -> Unit)? = null,
|
||||||
) {
|
) {
|
||||||
UserSearchDataSourceSubscription(userSuggestions, accountViewModel)
|
UserSearchDataSourceSubscription(userSuggestions, accountViewModel)
|
||||||
|
|
||||||
@@ -87,7 +88,7 @@ fun ShowUserSuggestionList(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
WatchResponses(userSuggestions, listState, onSelect, accountViewModel, modifier, onEmpty)
|
WatchResponses(userSuggestions, listState, onSelect, accountViewModel, modifier, onEmpty, trailingContent)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
@@ -110,6 +111,7 @@ fun WatchResponses(
|
|||||||
accountViewModel: AccountViewModel,
|
accountViewModel: AccountViewModel,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
onEmpty: @Composable () -> Unit = {},
|
onEmpty: @Composable () -> Unit = {},
|
||||||
|
trailingContent: (@Composable (User) -> Unit)? = null,
|
||||||
) {
|
) {
|
||||||
val suggestions by userSuggestions.results.collectAsStateWithLifecycle(emptyList())
|
val suggestions by userSuggestions.results.collectAsStateWithLifecycle(emptyList())
|
||||||
|
|
||||||
@@ -120,7 +122,7 @@ fun WatchResponses(
|
|||||||
state = listState,
|
state = listState,
|
||||||
) {
|
) {
|
||||||
itemsIndexed(suggestions, key = { _, item -> item.pubkeyHex }) { _, item ->
|
itemsIndexed(suggestions, key = { _, item -> item.pubkeyHex }) { _, item ->
|
||||||
UserLine(item, accountViewModel) { onSelect(item) }
|
UserLine(item, accountViewModel, trailingContent) { onSelect(item) }
|
||||||
HorizontalDivider(
|
HorizontalDivider(
|
||||||
thickness = DividerThickness,
|
thickness = DividerThickness,
|
||||||
)
|
)
|
||||||
@@ -135,6 +137,7 @@ fun WatchResponses(
|
|||||||
fun UserLine(
|
fun UserLine(
|
||||||
baseUser: User,
|
baseUser: User,
|
||||||
accountViewModel: AccountViewModel,
|
accountViewModel: AccountViewModel,
|
||||||
|
trailingContent: (@Composable (User) -> Unit)? = null,
|
||||||
onClick: () -> Unit,
|
onClick: () -> Unit,
|
||||||
) {
|
) {
|
||||||
SlimListItem(
|
SlimListItem(
|
||||||
@@ -153,6 +156,7 @@ fun UserLine(
|
|||||||
WatchAndDisplayNip05Row(baseUser, accountViewModel)
|
WatchAndDisplayNip05Row(baseUser, accountViewModel)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
trailingContent = trailingContent?.let { { it(baseUser) } },
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+71
-63
@@ -32,10 +32,9 @@ import androidx.compose.foundation.layout.Column
|
|||||||
import androidx.compose.foundation.layout.ExperimentalLayoutApi
|
import androidx.compose.foundation.layout.ExperimentalLayoutApi
|
||||||
import androidx.compose.foundation.layout.FlowRow
|
import androidx.compose.foundation.layout.FlowRow
|
||||||
import androidx.compose.foundation.layout.Row
|
import androidx.compose.foundation.layout.Row
|
||||||
import androidx.compose.foundation.layout.Spacer
|
|
||||||
import androidx.compose.foundation.layout.fillMaxSize
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
import androidx.compose.foundation.layout.height
|
import androidx.compose.foundation.layout.imePadding
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.foundation.layout.size
|
import androidx.compose.foundation.layout.size
|
||||||
import androidx.compose.foundation.lazy.LazyColumn
|
import androidx.compose.foundation.lazy.LazyColumn
|
||||||
@@ -45,6 +44,7 @@ import androidx.compose.material.icons.Icons
|
|||||||
import androidx.compose.material.icons.automirrored.filled.ArrowBack
|
import androidx.compose.material.icons.automirrored.filled.ArrowBack
|
||||||
import androidx.compose.material.icons.automirrored.filled.ExitToApp
|
import androidx.compose.material.icons.automirrored.filled.ExitToApp
|
||||||
import androidx.compose.material.icons.filled.Edit
|
import androidx.compose.material.icons.filled.Edit
|
||||||
|
import androidx.compose.material.icons.filled.PersonAdd
|
||||||
import androidx.compose.material.icons.filled.PersonRemove
|
import androidx.compose.material.icons.filled.PersonRemove
|
||||||
import androidx.compose.material3.AlertDialog
|
import androidx.compose.material3.AlertDialog
|
||||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||||
@@ -165,14 +165,14 @@ fun MarmotGroupInfoScreen(
|
|||||||
)
|
)
|
||||||
},
|
},
|
||||||
) { padding ->
|
) { padding ->
|
||||||
LazyColumn(
|
Column(
|
||||||
modifier =
|
modifier =
|
||||||
Modifier
|
Modifier
|
||||||
.fillMaxSize()
|
.fillMaxSize()
|
||||||
.padding(padding),
|
.padding(padding)
|
||||||
|
.imePadding(),
|
||||||
) {
|
) {
|
||||||
// Group header section
|
// Group header section (fixed at top)
|
||||||
item {
|
|
||||||
Column(
|
Column(
|
||||||
modifier =
|
modifier =
|
||||||
Modifier
|
Modifier
|
||||||
@@ -212,10 +212,37 @@ fun MarmotGroupInfoScreen(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
HorizontalDivider()
|
HorizontalDivider()
|
||||||
|
|
||||||
|
// Members list (scrollable, takes remaining vertical space)
|
||||||
|
LazyColumn(modifier = Modifier.weight(1f).fillMaxWidth()) {
|
||||||
|
item {
|
||||||
|
Text(
|
||||||
|
text = "Members",
|
||||||
|
style = MaterialTheme.typography.titleMedium,
|
||||||
|
fontWeight = FontWeight.Bold,
|
||||||
|
modifier = Modifier.padding(horizontal = 16.dp, vertical = 12.dp),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Inline add-member search
|
items(members, key = { it.leafIndex }) { member ->
|
||||||
item {
|
val isMe = member.pubkey == myPubkey
|
||||||
|
val isAdmin = member.pubkey in adminPubkeys
|
||||||
|
MemberRow(
|
||||||
|
member = member,
|
||||||
|
isMe = isMe,
|
||||||
|
isAdmin = isAdmin,
|
||||||
|
canRemove = !isMe,
|
||||||
|
accountViewModel = accountViewModel,
|
||||||
|
nav = nav,
|
||||||
|
onRemoveClick = { memberToRemove = member },
|
||||||
|
)
|
||||||
|
HorizontalDivider()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
HorizontalDivider()
|
||||||
|
|
||||||
|
// Suggestions + add-member input (fixed at bottom)
|
||||||
AddMemberInline(
|
AddMemberInline(
|
||||||
nostrGroupId = nostrGroupId,
|
nostrGroupId = nostrGroupId,
|
||||||
searchInput = addSearchInput,
|
searchInput = addSearchInput,
|
||||||
@@ -260,34 +287,6 @@ fun MarmotGroupInfoScreen(
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
HorizontalDivider()
|
|
||||||
}
|
|
||||||
|
|
||||||
// Members section header
|
|
||||||
item {
|
|
||||||
Text(
|
|
||||||
text = "Members",
|
|
||||||
style = MaterialTheme.typography.titleMedium,
|
|
||||||
fontWeight = FontWeight.Bold,
|
|
||||||
modifier = Modifier.padding(horizontal = 16.dp, vertical = 12.dp),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Member list with inline remove button
|
|
||||||
items(members, key = { it.leafIndex }) { member ->
|
|
||||||
val isMe = member.pubkey == myPubkey
|
|
||||||
val isAdmin = member.pubkey in adminPubkeys
|
|
||||||
MemberRow(
|
|
||||||
member = member,
|
|
||||||
isMe = isMe,
|
|
||||||
isAdmin = isAdmin,
|
|
||||||
canRemove = !isMe,
|
|
||||||
accountViewModel = accountViewModel,
|
|
||||||
nav = nav,
|
|
||||||
onRemoveClick = { memberToRemove = member },
|
|
||||||
)
|
|
||||||
HorizontalDivider()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -363,18 +362,32 @@ private fun AddMemberInline(
|
|||||||
onAdd: (com.vitorpamplona.amethyst.model.User) -> Unit,
|
onAdd: (com.vitorpamplona.amethyst.model.User) -> Unit,
|
||||||
) {
|
) {
|
||||||
Column(modifier = Modifier.fillMaxWidth()) {
|
Column(modifier = Modifier.fillMaxWidth()) {
|
||||||
OutlinedTextField(
|
if (!isAdding && searchInput.length > 2) {
|
||||||
value = searchInput,
|
ShowUserSuggestionList(
|
||||||
onValueChange = onSearchInputChange,
|
userSuggestions = userSuggestions,
|
||||||
label = { Text("Add member") },
|
onSelect = { user -> onAdd(user) },
|
||||||
placeholder = { Text("Name, npub, or NIP-05") },
|
accountViewModel = accountViewModel,
|
||||||
modifier =
|
modifier = SuggestionListDefaultHeightChat,
|
||||||
Modifier
|
onEmpty = {
|
||||||
.fillMaxWidth()
|
Text(
|
||||||
.padding(horizontal = 16.dp, vertical = 8.dp),
|
"They must have published a KeyPackage (kind:30443) to be added.",
|
||||||
singleLine = true,
|
modifier = Modifier.padding(horizontal = 16.dp, vertical = 8.dp),
|
||||||
enabled = !isAdding,
|
style = MaterialTheme.typography.bodySmall,
|
||||||
|
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||||
)
|
)
|
||||||
|
},
|
||||||
|
trailingContent = { user ->
|
||||||
|
IconButton(onClick = { onAdd(user) }) {
|
||||||
|
Icon(
|
||||||
|
imageVector = Icons.Default.PersonAdd,
|
||||||
|
contentDescription = "Add to group",
|
||||||
|
tint = MaterialTheme.colorScheme.primary,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
|
HorizontalDivider()
|
||||||
|
}
|
||||||
|
|
||||||
if (statusMessage != null) {
|
if (statusMessage != null) {
|
||||||
Text(
|
Text(
|
||||||
@@ -390,23 +403,18 @@ private fun AddMemberInline(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!isAdding && searchInput.length > 2) {
|
OutlinedTextField(
|
||||||
Spacer(modifier = Modifier.height(4.dp))
|
value = searchInput,
|
||||||
ShowUserSuggestionList(
|
onValueChange = onSearchInputChange,
|
||||||
userSuggestions = userSuggestions,
|
label = { Text("Add member") },
|
||||||
onSelect = { user -> onAdd(user) },
|
placeholder = { Text("Name, npub, or NIP-05") },
|
||||||
accountViewModel = accountViewModel,
|
modifier =
|
||||||
modifier = SuggestionListDefaultHeightChat,
|
Modifier
|
||||||
onEmpty = {
|
.fillMaxWidth()
|
||||||
Text(
|
.padding(horizontal = 16.dp, vertical = 8.dp),
|
||||||
"They must have published a KeyPackage (kind:30443) to be added.",
|
singleLine = true,
|
||||||
modifier = Modifier.padding(horizontal = 16.dp, vertical = 8.dp),
|
enabled = !isAdding,
|
||||||
style = MaterialTheme.typography.bodySmall,
|
|
||||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
|
||||||
)
|
)
|
||||||
},
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user