Make request groups collapsible, collapsed by default

Group headers in the multi-request approval screens now expand and
collapse their items. Tapping the header row toggles expansion (with a
rotating chevron); the tri-state checkbox still selects or deselects
the whole group without expanding it. Groups start collapsed so a large
batch reads as a short summary. A homogeneous batch (single group) has
no header and stays fully visible as before.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TioEdhZRp9W78DbsHzEypZ
This commit is contained in:
Claude
2026-07-08 10:58:09 +00:00
parent 6caa68f6e6
commit 690b671a92
3 changed files with 61 additions and 21 deletions
@@ -21,6 +21,7 @@ import androidx.compose.material3.TriStateCheckbox
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateMapOf
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
@@ -161,10 +162,12 @@ fun BunkerMultiEventHomeScreen(
)
}
}
val expandedGroups = remember { mutableStateMapOf<RequestGroupKey, Boolean>() }
LazyColumn(
Modifier.weight(1f),
) {
groups.forEach { (groupKey, groupItems) ->
val expanded = groups.size == 1 || (expandedGroups[groupKey] ?: false)
if (groups.size > 1) {
item(key = "group-header:${groupKey.type.name}:${groupKey.payload?.name ?: ""}:${groupKey.kind ?: ""}") {
val groupState = when {
@@ -176,23 +179,29 @@ fun BunkerMultiEventHomeScreen(
label = groupKey.toLabel(context),
count = groupItems.size,
state = groupState,
expanded = expanded,
onToggle = {
val newValue = groupState != ToggleableState.On
MultiEventScreenIntents.checkedStates.putAll(groupItems.associate { it.request.id to newValue })
},
onExpandToggle = {
expandedGroups[groupKey] = !(expandedGroups[groupKey] ?: false)
},
)
}
}
items(groupItems, key = { it.request.id }) { bunkerRequest ->
BunkerRequestCard(
context = context,
bunkerRequest = bunkerRequest,
checked = MultiEventScreenIntents.checkedStates[bunkerRequest.request.id] ?: true,
onToggleChecked = {
val current = MultiEventScreenIntents.checkedStates[bunkerRequest.request.id] ?: true
MultiEventScreenIntents.checkedStates[bunkerRequest.request.id] = !current
},
)
if (expanded) {
items(groupItems, key = { it.request.id }) { bunkerRequest ->
BunkerRequestCard(
context = context,
bunkerRequest = bunkerRequest,
checked = MultiEventScreenIntents.checkedStates[bunkerRequest.request.id] ?: true,
onToggleChecked = {
val current = MultiEventScreenIntents.checkedStates[bunkerRequest.request.id] ?: true
MultiEventScreenIntents.checkedStates[bunkerRequest.request.id] = !current
},
)
}
}
}
}
@@ -23,6 +23,7 @@ import androidx.compose.material3.TriStateCheckbox
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateMapOf
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
@@ -127,10 +128,12 @@ fun IntentMultiEventHomeScreen(
requestGroupKey(it.type, it.event?.kind, it.encryptedData, it.nip44v3Kind)
}
}
val expandedGroups = remember { mutableStateMapOf<RequestGroupKey, Boolean>() }
LazyColumn(
Modifier.weight(1f),
) {
groups.forEach { (groupKey, groupIntents) ->
val expanded = groups.size == 1 || (expandedGroups[groupKey] ?: false)
if (groups.size > 1) {
item(key = "group-header:${groupKey.type.name}:${groupKey.payload?.name ?: ""}:${groupKey.kind ?: ""}") {
val groupState = when {
@@ -142,23 +145,29 @@ fun IntentMultiEventHomeScreen(
label = groupKey.toLabel(context),
count = groupIntents.size,
state = groupState,
expanded = expanded,
onToggle = {
val newValue = groupState != ToggleableState.On
MultiEventScreenIntents.checkedStates.putAll(groupIntents.associate { it.id to newValue })
},
onExpandToggle = {
expandedGroups[groupKey] = !(expandedGroups[groupKey] ?: false)
},
)
}
}
items(groupIntents, key = { it.id }) { intent ->
IntentRequestCard(
context = context,
intent = intent,
checked = MultiEventScreenIntents.checkedStates[intent.id] ?: true,
onToggleChecked = {
val current = MultiEventScreenIntents.checkedStates[intent.id] ?: true
MultiEventScreenIntents.checkedStates[intent.id] = !current
},
)
if (expanded) {
items(groupIntents, key = { it.id }) { intent ->
IntentRequestCard(
context = context,
intent = intent,
checked = MultiEventScreenIntents.checkedStates[intent.id] ?: true,
onToggleChecked = {
val current = MultiEventScreenIntents.checkedStates[intent.id] ?: true
MultiEventScreenIntents.checkedStates[intent.id] = !current
},
)
}
}
}
}
@@ -1,16 +1,24 @@
package com.greenart7c3.nostrsigner.ui.components
import android.content.Context
import androidx.compose.animation.core.animateFloatAsState
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.ExpandMore
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.material3.TriStateCheckbox
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.rotate
import androidx.compose.ui.state.ToggleableState
import androidx.compose.ui.unit.dp
import com.greenart7c3.nostrsigner.R
import com.greenart7c3.nostrsigner.models.ClearTextEncryptedDataKind
import com.greenart7c3.nostrsigner.models.EncryptedDataKind
@@ -106,13 +114,19 @@ fun RequestGroupHeader(
label: String,
count: Int,
state: ToggleableState,
expanded: Boolean,
onToggle: () -> Unit,
onExpandToggle: () -> Unit,
) {
val rotation by animateFloatAsState(
targetValue = if (expanded) 180f else 0f,
label = "group header chevron rotation",
)
Row(
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier
.fillMaxWidth()
.clickable { onToggle() },
.clickable { onExpandToggle() },
) {
TriStateCheckbox(
state = state,
@@ -121,6 +135,14 @@ fun RequestGroupHeader(
Text(
text = "$label ($count)",
style = MaterialTheme.typography.titleSmall,
modifier = Modifier.weight(1f),
)
Icon(
imageVector = Icons.Default.ExpandMore,
contentDescription = null,
modifier = Modifier
.padding(end = 12.dp)
.rotate(rotation),
)
}
}