From 6caa68f6e6a44fafe7f8c85753c4fc811bbce0ee Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 7 Jul 2026 19:30:39 +0000 Subject: [PATCH 1/2] Group multi-request approval lists by type and kind When several intents or bunker requests are shown at once, the list is now sectioned by request type and kind instead of arrival order: - sign_event requests group by event kind - encrypt/decrypt requests separate by payload shape (event, tag array, clear text, private zap) and, for event payloads, by embedded kind - NIP-44 v3 requests group by their explicit kind parameter Each section header shows a localized label with a count and a tri-state checkbox to select or deselect the whole group. Grouping is presentation-only; approve/discard behavior is unchanged. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01TioEdhZRp9W78DbsHzEypZ --- .../components/BunkerMultiEventHomeScreen.kt | 50 +++++-- .../components/IntentMultiEventHomeScreen.kt | 45 +++++-- .../ui/components/RequestGroupUtils.kt | 126 ++++++++++++++++++ .../ui/components/RequestGroupUtilsTest.kt | 118 ++++++++++++++++ 4 files changed, 319 insertions(+), 20 deletions(-) create mode 100644 app/src/main/java/com/greenart7c3/nostrsigner/ui/components/RequestGroupUtils.kt create mode 100644 app/src/test/java/com/greenart7c3/nostrsigner/ui/components/RequestGroupUtilsTest.kt diff --git a/app/src/main/java/com/greenart7c3/nostrsigner/ui/components/BunkerMultiEventHomeScreen.kt b/app/src/main/java/com/greenart7c3/nostrsigner/ui/components/BunkerMultiEventHomeScreen.kt index 67aab33e..939bed41 100644 --- a/app/src/main/java/com/greenart7c3/nostrsigner/ui/components/BunkerMultiEventHomeScreen.kt +++ b/app/src/main/java/com/greenart7c3/nostrsigner/ui/components/BunkerMultiEventHomeScreen.kt @@ -151,19 +151,49 @@ fun BunkerMultiEventHomeScreen( Text(stringResource(R.string.select_deselect_all)) } + val groups = remember(bunkerRequests) { + groupRequests(bunkerRequests) { + requestGroupKey( + type = BunkerRequestUtils.getTypeFromBunker(it.request), + eventKind = (it.request as? BunkerRequestSign)?.event?.kind, + encryptedData = it.encryptedData, + nip44v3Kind = BunkerRequestUtils.getNip44v3Kind(it.request), + ) + } + } LazyColumn( Modifier.weight(1f), ) { - items(bunkerRequests, 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 - }, - ) + groups.forEach { (groupKey, groupItems) -> + if (groups.size > 1) { + item(key = "group-header:${groupKey.type.name}:${groupKey.payload?.name ?: ""}:${groupKey.kind ?: ""}") { + val groupState = when { + groupItems.all { MultiEventScreenIntents.checkedStates[it.request.id] ?: true } -> ToggleableState.On + groupItems.none { MultiEventScreenIntents.checkedStates[it.request.id] ?: true } -> ToggleableState.Off + else -> ToggleableState.Indeterminate + } + RequestGroupHeader( + label = groupKey.toLabel(context), + count = groupItems.size, + state = groupState, + onToggle = { + val newValue = groupState != ToggleableState.On + MultiEventScreenIntents.checkedStates.putAll(groupItems.associate { it.request.id to newValue }) + }, + ) + } + } + 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 + }, + ) + } } } diff --git a/app/src/main/java/com/greenart7c3/nostrsigner/ui/components/IntentMultiEventHomeScreen.kt b/app/src/main/java/com/greenart7c3/nostrsigner/ui/components/IntentMultiEventHomeScreen.kt index 8c64b095..7e01e16c 100644 --- a/app/src/main/java/com/greenart7c3/nostrsigner/ui/components/IntentMultiEventHomeScreen.kt +++ b/app/src/main/java/com/greenart7c3/nostrsigner/ui/components/IntentMultiEventHomeScreen.kt @@ -122,19 +122,44 @@ fun IntentMultiEventHomeScreen( Text(stringResource(R.string.select_deselect_all)) } + val groups = remember(intents) { + groupRequests(intents) { + requestGroupKey(it.type, it.event?.kind, it.encryptedData, it.nip44v3Kind) + } + } LazyColumn( Modifier.weight(1f), ) { - items(intents, 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 - }, - ) + groups.forEach { (groupKey, groupIntents) -> + if (groups.size > 1) { + item(key = "group-header:${groupKey.type.name}:${groupKey.payload?.name ?: ""}:${groupKey.kind ?: ""}") { + val groupState = when { + groupIntents.all { MultiEventScreenIntents.checkedStates[it.id] ?: true } -> ToggleableState.On + groupIntents.none { MultiEventScreenIntents.checkedStates[it.id] ?: true } -> ToggleableState.Off + else -> ToggleableState.Indeterminate + } + RequestGroupHeader( + label = groupKey.toLabel(context), + count = groupIntents.size, + state = groupState, + onToggle = { + val newValue = groupState != ToggleableState.On + MultiEventScreenIntents.checkedStates.putAll(groupIntents.associate { it.id to newValue }) + }, + ) + } + } + 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 + }, + ) + } } } diff --git a/app/src/main/java/com/greenart7c3/nostrsigner/ui/components/RequestGroupUtils.kt b/app/src/main/java/com/greenart7c3/nostrsigner/ui/components/RequestGroupUtils.kt new file mode 100644 index 00000000..a1e5844b --- /dev/null +++ b/app/src/main/java/com/greenart7c3/nostrsigner/ui/components/RequestGroupUtils.kt @@ -0,0 +1,126 @@ +package com.greenart7c3.nostrsigner.ui.components + +import android.content.Context +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.material3.TriStateCheckbox +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.state.ToggleableState +import com.greenart7c3.nostrsigner.R +import com.greenart7c3.nostrsigner.models.ClearTextEncryptedDataKind +import com.greenart7c3.nostrsigner.models.EncryptedDataKind +import com.greenart7c3.nostrsigner.models.EventEncryptedDataKind +import com.greenart7c3.nostrsigner.models.Permission +import com.greenart7c3.nostrsigner.models.PrivateZapEncryptedDataKind +import com.greenart7c3.nostrsigner.models.SignerType +import com.greenart7c3.nostrsigner.models.TagArrayEncryptedDataKind +import com.greenart7c3.nostrsigner.models.encryptDecryptSignerTypes + +enum class RequestPayloadShape { + EVENT, + TAG_ARRAY, + CLEAR_TEXT, + PRIVATE_ZAP, +} + +data class RequestGroupKey( + val type: SignerType, + val kind: Int?, + val payload: RequestPayloadShape?, +) + +fun requestGroupKey( + type: SignerType, + eventKind: Int?, + encryptedData: EncryptedDataKind?, + nip44v3Kind: Int?, +): RequestGroupKey = when { + type == SignerType.SIGN_EVENT -> RequestGroupKey(type, eventKind, null) + type == SignerType.NIP44_V3_ENCRYPT || type == SignerType.NIP44_V3_DECRYPT -> RequestGroupKey(type, nip44v3Kind, null) + type in encryptDecryptSignerTypes -> when (encryptedData) { + is EventEncryptedDataKind -> RequestGroupKey(type, encryptedData.event.kind, RequestPayloadShape.EVENT) + is TagArrayEncryptedDataKind -> RequestGroupKey(type, null, RequestPayloadShape.TAG_ARRAY) + is ClearTextEncryptedDataKind -> RequestGroupKey(type, null, RequestPayloadShape.CLEAR_TEXT) + is PrivateZapEncryptedDataKind -> RequestGroupKey(type, null, RequestPayloadShape.PRIVATE_ZAP) + else -> RequestGroupKey(type, null, null) + } + else -> RequestGroupKey(type, null, null) +} + +fun groupRequests( + items: List, + keyOf: (T) -> RequestGroupKey, +): List>> = items.groupBy(keyOf).toList() + .sortedWith( + compareBy( + { it.first.type.ordinal }, + { it.first.payload?.ordinal ?: -1 }, + { it.first.kind ?: -1 }, + ), + ) + +fun RequestGroupKey.toLabel(context: Context): String { + val isEncrypt = type.name.contains("ENCRYPT") + val nip = type.name.split("_").first() + return when { + type == SignerType.CONNECT -> context.getString(R.string.connect) + type == SignerType.SIGN_EVENT -> Permission("sign_event", kind).toLocalizedString(context) + payload == RequestPayloadShape.EVENT -> { + val kindLabel = Permission("sign_event", kind).toLocalizedString(context) + if (isEncrypt) { + context.getString(R.string.encrypt_with, kindLabel, nip) + } else { + context.getString(R.string.read_from_encrypted_content, kindLabel, nip) + } + } + payload == RequestPayloadShape.TAG_ARRAY -> if (isEncrypt) { + context.getString(R.string.encrypt_this_list_of_tags_with, nip) + } else { + context.getString(R.string.read_this_list_of_tags_from_encrypted_content, nip) + } + payload == RequestPayloadShape.CLEAR_TEXT -> if (isEncrypt) { + context.getString(R.string.encrypt_this_text_with, nip) + } else { + context.getString(R.string.read_this_text_from_encrypted_content, nip) + } + payload == RequestPayloadShape.PRIVATE_ZAP -> context.getString(R.string.decrypt_zap_event).replaceFirstChar { it.uppercase() } + (type == SignerType.NIP44_V3_ENCRYPT || type == SignerType.NIP44_V3_DECRYPT) && kind != null -> { + val kindLabel = Permission("sign_event", kind).toLocalizedString(context) + if (isEncrypt) { + context.getString(R.string.encrypt_with, kindLabel, nip) + } else { + context.getString(R.string.read_from_encrypted_content, kindLabel, nip) + } + } + else -> Permission(type.name.lowercase(), null).toLocalizedString(context) + } +} + +@Composable +fun RequestGroupHeader( + label: String, + count: Int, + state: ToggleableState, + onToggle: () -> Unit, +) { + Row( + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier + .fillMaxWidth() + .clickable { onToggle() }, + ) { + TriStateCheckbox( + state = state, + onClick = onToggle, + ) + Text( + text = "$label ($count)", + style = MaterialTheme.typography.titleSmall, + ) + } +} diff --git a/app/src/test/java/com/greenart7c3/nostrsigner/ui/components/RequestGroupUtilsTest.kt b/app/src/test/java/com/greenart7c3/nostrsigner/ui/components/RequestGroupUtilsTest.kt new file mode 100644 index 00000000..a40f10e1 --- /dev/null +++ b/app/src/test/java/com/greenart7c3/nostrsigner/ui/components/RequestGroupUtilsTest.kt @@ -0,0 +1,118 @@ +package com.greenart7c3.nostrsigner.ui.components + +import com.greenart7c3.nostrsigner.models.ClearTextEncryptedDataKind +import com.greenart7c3.nostrsigner.models.EventEncryptedDataKind +import com.greenart7c3.nostrsigner.models.PrivateZapEncryptedDataKind +import com.greenart7c3.nostrsigner.models.SignerType +import com.greenart7c3.nostrsigner.models.TagArrayEncryptedDataKind +import com.greenart7c3.nostrsigner.service.model.AmberEvent +import org.junit.Assert.assertEquals +import org.junit.Test + +class RequestGroupUtilsTest { + + private fun amberEvent(kind: Int) = AmberEvent("id", "pubkey", 0L, kind, arrayOf(), "", "") + + private fun eventData(kind: Int) = EventEncryptedDataKind(amberEvent(kind), null, "result") + + @Test + fun `sign event groups by event kind`() { + val key1 = requestGroupKey(SignerType.SIGN_EVENT, 1, null, null) + val key1b = requestGroupKey(SignerType.SIGN_EVENT, 1, null, null) + val key22242 = requestGroupKey(SignerType.SIGN_EVENT, 22242, null, null) + + assertEquals(key1, key1b) + assertEquals(1, key1.kind) + assertEquals(22242, key22242.kind) + assertEquals(SignerType.SIGN_EVENT, key1.type) + assertEquals(null, key1.payload) + } + + @Test + fun `encrypt decrypt separates by payload shape`() { + val eventKey = requestGroupKey(SignerType.NIP44_DECRYPT, null, eventData(4), null) + val tagKey = requestGroupKey(SignerType.NIP44_DECRYPT, null, TagArrayEncryptedDataKind(arrayOf(), "r"), null) + val textKey = requestGroupKey(SignerType.NIP44_DECRYPT, null, ClearTextEncryptedDataKind("t", "r"), null) + val zapKey = requestGroupKey(SignerType.DECRYPT_ZAP_EVENT, null, PrivateZapEncryptedDataKind("r"), null) + + assertEquals(RequestPayloadShape.EVENT, eventKey.payload) + assertEquals(4, eventKey.kind) + assertEquals(RequestPayloadShape.TAG_ARRAY, tagKey.payload) + assertEquals(null, tagKey.kind) + assertEquals(RequestPayloadShape.CLEAR_TEXT, textKey.payload) + assertEquals(RequestPayloadShape.PRIVATE_ZAP, zapKey.payload) + } + + @Test + fun `encrypt decrypt event payloads separate by embedded event kind`() { + val kind1 = requestGroupKey(SignerType.NIP44_ENCRYPT, null, eventData(1), null) + val kind1b = requestGroupKey(SignerType.NIP44_ENCRYPT, null, eventData(1), null) + val kind7 = requestGroupKey(SignerType.NIP44_ENCRYPT, null, eventData(7), null) + + assertEquals(kind1, kind1b) + assertEquals(1, kind1.kind) + assertEquals(7, kind7.kind) + } + + @Test + fun `nip44 v3 groups by explicit kind`() { + val enc9 = requestGroupKey(SignerType.NIP44_V3_ENCRYPT, null, null, 9) + val dec9 = requestGroupKey(SignerType.NIP44_V3_DECRYPT, null, null, 9) + val decNull = requestGroupKey(SignerType.NIP44_V3_DECRYPT, null, null, null) + + assertEquals(9, enc9.kind) + assertEquals(9, dec9.kind) + assertEquals(null, decNull.kind) + assertEquals(null, enc9.payload) + } + + @Test + fun `other types group by type alone`() { + val connect = requestGroupKey(SignerType.CONNECT, null, null, null) + val pubKey = requestGroupKey(SignerType.GET_PUBLIC_KEY, null, null, null) + + assertEquals(RequestGroupKey(SignerType.CONNECT, null, null), connect) + assertEquals(RequestGroupKey(SignerType.GET_PUBLIC_KEY, null, null), pubKey) + } + + @Test + fun `groupRequests sorts by type ordinal then payload then kind`() { + val items = listOf( + RequestGroupKey(SignerType.SIGN_EVENT, 22242, null), + RequestGroupKey(SignerType.NIP44_DECRYPT, null, RequestPayloadShape.CLEAR_TEXT), + RequestGroupKey(SignerType.CONNECT, null, null), + RequestGroupKey(SignerType.SIGN_EVENT, 1, null), + RequestGroupKey(SignerType.NIP44_DECRYPT, 4, RequestPayloadShape.EVENT), + ) + + val groups = groupRequests(items) { it } + + assertEquals( + listOf( + RequestGroupKey(SignerType.CONNECT, null, null), + RequestGroupKey(SignerType.SIGN_EVENT, 1, null), + RequestGroupKey(SignerType.SIGN_EVENT, 22242, null), + RequestGroupKey(SignerType.NIP44_DECRYPT, 4, RequestPayloadShape.EVENT), + RequestGroupKey(SignerType.NIP44_DECRYPT, null, RequestPayloadShape.CLEAR_TEXT), + ), + groups.map { it.first }, + ) + } + + @Test + fun `groupRequests preserves input order within a group`() { + data class Item(val id: String, val kind: Int) + + val items = listOf( + Item("a", 1), + Item("b", 7), + Item("c", 1), + Item("d", 1), + ) + + val groups = groupRequests(items) { RequestGroupKey(SignerType.SIGN_EVENT, it.kind, null) } + + assertEquals(listOf("a", "c", "d"), groups.first { it.first.kind == 1 }.second.map { it.id }) + assertEquals(listOf("b"), groups.first { it.first.kind == 7 }.second.map { it.id }) + } +} From 690b671a920a653a2c0be288ebed4109d33f4c14 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 8 Jul 2026 10:58:09 +0000 Subject: [PATCH 2/2] 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 Claude-Session: https://claude.ai/code/session_01TioEdhZRp9W78DbsHzEypZ --- .../components/BunkerMultiEventHomeScreen.kt | 29 ++++++++++++------- .../components/IntentMultiEventHomeScreen.kt | 29 ++++++++++++------- .../ui/components/RequestGroupUtils.kt | 24 ++++++++++++++- 3 files changed, 61 insertions(+), 21 deletions(-) diff --git a/app/src/main/java/com/greenart7c3/nostrsigner/ui/components/BunkerMultiEventHomeScreen.kt b/app/src/main/java/com/greenart7c3/nostrsigner/ui/components/BunkerMultiEventHomeScreen.kt index 939bed41..399700fd 100644 --- a/app/src/main/java/com/greenart7c3/nostrsigner/ui/components/BunkerMultiEventHomeScreen.kt +++ b/app/src/main/java/com/greenart7c3/nostrsigner/ui/components/BunkerMultiEventHomeScreen.kt @@ -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() } 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 + }, + ) + } } } } diff --git a/app/src/main/java/com/greenart7c3/nostrsigner/ui/components/IntentMultiEventHomeScreen.kt b/app/src/main/java/com/greenart7c3/nostrsigner/ui/components/IntentMultiEventHomeScreen.kt index 7e01e16c..132ac5c0 100644 --- a/app/src/main/java/com/greenart7c3/nostrsigner/ui/components/IntentMultiEventHomeScreen.kt +++ b/app/src/main/java/com/greenart7c3/nostrsigner/ui/components/IntentMultiEventHomeScreen.kt @@ -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() } 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 + }, + ) + } } } } diff --git a/app/src/main/java/com/greenart7c3/nostrsigner/ui/components/RequestGroupUtils.kt b/app/src/main/java/com/greenart7c3/nostrsigner/ui/components/RequestGroupUtils.kt index a1e5844b..71171be3 100644 --- a/app/src/main/java/com/greenart7c3/nostrsigner/ui/components/RequestGroupUtils.kt +++ b/app/src/main/java/com/greenart7c3/nostrsigner/ui/components/RequestGroupUtils.kt @@ -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), ) } }