Refactor event detail handling and improve UI components

- Extract `EventDetailModal` into a dedicated component to reduce code duplication.
- Add clipboard support to `EventSection` and `EventDetailModal` for copying kind, pubkey, date, and content.
- Implement `EncryptedTagArraySection` for better display and copying of encrypted tag data.
- Refactor `AmberEvent` by moving `toEvent()` from a companion object to an instance method.
- Update `EncryptDecryptData` and `EventData` to utilize the new modal and tag sections.
- Clean up unused parameters and imports across several UI components.
This commit is contained in:
greenart7c3
2026-01-14 09:02:40 -03:00
parent c104b8bc43
commit 475c7552db
10 changed files with 240 additions and 225 deletions
@@ -198,7 +198,7 @@ object BunkerRequestUtils {
"connect" -> "ack"
"sign_event" -> {
val amberEvent = AmberEvent.fromJson(bunkerRequest.params.first())
AmberEvent.toEvent(amberEvent).toJson()
amberEvent.toEvent().toJson()
}
"nip04_encrypt", "nip04_decrypt", "nip44_encrypt", "nip44_decrypt", "decrypt_zap_event" -> bunkerRequest.params.getOrElse(1) { "" }
"ping" -> "pong"
@@ -638,7 +638,7 @@ object IntentUtils {
)
}
return AmberEvent.toEvent(event)
return event.toEvent()
}
fun sendResult(
@@ -77,6 +77,16 @@ open class AmberEvent(
fun verifiedFollowKeySet(): Set<HexKey> = tags.mapNotNullTo(HashSet(), ContactTag::parseValidKey)
fun toEvent(): Event = Event(
id,
pubKey,
createdAt,
kind,
tags,
content,
sig,
)
companion object {
val mapper: ObjectMapper =
jacksonObjectMapper()
@@ -91,16 +101,6 @@ open class AmberEvent(
fun fromJson(json: String): AmberEvent = mapper.readValue(json, AmberEvent::class.java)
fun toEvent(amberEvent: AmberEvent): Event = Event(
amberEvent.id,
amberEvent.pubKey,
amberEvent.createdAt,
amberEvent.kind,
amberEvent.tags,
amberEvent.content,
amberEvent.sig,
)
fun relay(event: Event): String = event.tags.filter { it.size > 1 && it[0] == "relay" }.map { it[1] }.first()
}
}
@@ -633,13 +633,10 @@ fun BunkerSingleEventHomeScreen(
}
BunkerEventData(
account = account,
modifier = modifier,
shouldAcceptOrReject = acceptOrReject,
appName = appName,
event = event,
rawJson = event.toJson(),
type = type,
onAccept = {
if (event.pubKey != account.hexKey && !isPrivateEvent(event.kind, event.tags)) {
coroutineScope.launch {
@@ -5,11 +5,8 @@ import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.material3.Card
import androidx.compose.material3.CardDefaults
import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
@@ -108,83 +105,52 @@ fun EncryptDecryptData(
if (encryptedData is EventEncryptedDataKind) {
if (encryptedData.sealEncryptedDataKind != null) {
if (encryptedData.sealEncryptedDataKind is EventEncryptedDataKind) {
val content = if (encryptedData.sealEncryptedDataKind.event.kind == 22242) encryptedData.sealEncryptedDataKind.event.relay() else encryptedData.sealEncryptedDataKind.event.content
if (encryptedData.event.kind == ContactListEvent.KIND) {
ContactListDetail(
title = stringResource(R.string.following),
text = "${encryptedData.event.verifiedFollowKeySet().size}",
)
} else {
Text(
content,
modifier = Modifier
.fillMaxWidth()
.padding(top = 8.dp),
var showDetails by remember {
mutableStateOf(false)
}
RawJsonButton(
onCLick = {
showDetails = !showDetails
},
stringResource(R.string.show_details),
)
if (showDetails) {
EventDetailModal(
encryptedData.sealEncryptedDataKind.event.toEvent(),
{
showDetails = false
},
)
}
}
} else {
val content = if (encryptedData.event.kind == 22242) encryptedData.event.relay() else encryptedData.event.content
if (encryptedData.event.kind == ContactListEvent.KIND) {
ContactListDetail(
title = stringResource(R.string.following),
text = "${encryptedData.event.verifiedFollowKeySet().size}",
)
} else {
Text(
content,
modifier = Modifier
.fillMaxWidth()
.padding(top = 8.dp),
var showDetails by remember {
mutableStateOf(false)
}
RawJsonButton(
onCLick = {
showDetails = !showDetails
},
stringResource(R.string.show_details),
)
if (showDetails) {
EventDetailModal(
encryptedData.event.toEvent(),
{
showDetails = false
},
)
}
}
} else {
if (encryptedData is TagArrayEncryptedDataKind) {
val mappedTags = encryptedData.tagArray.mapNotNull { tag ->
if (tag.isEmpty()) return@mapNotNull null
TagItem(
name = tag.first(),
values = tag.drop(1),
)
}
LazyColumn(
EncryptedTagArraySection(
modifier = Modifier
.fillMaxWidth()
.weight(1f)
.padding(top = 8.dp),
) {
items(mappedTags) { item ->
Column(
modifier = Modifier
.fillMaxWidth(),
) {
Text(
text = item.name,
fontWeight = FontWeight.Bold,
fontSize = 18.sp,
modifier = Modifier
.fillMaxWidth()
.padding(bottom = 4.dp),
)
item.values.forEach { value ->
Text(
fontSize = 16.sp,
text = value,
modifier = Modifier
.fillMaxWidth()
.padding(bottom = 2.dp),
)
}
}
HorizontalDivider(
color = MaterialTheme.colorScheme.primary,
)
}
}
encryptedData.tagArray,
)
} else {
val content = if (type.name.contains("ENCRYPT") && encryptedData is ClearTextEncryptedDataKind) {
encryptedData.text
@@ -329,50 +295,13 @@ fun BunkerEncryptDecryptData(
}
} else {
if (encryptedData is TagArrayEncryptedDataKind) {
val mappedTags = encryptedData.tagArray.mapNotNull { tag ->
if (tag.isEmpty()) return@mapNotNull null
TagItem(
name = tag.first(),
values = tag.drop(1),
)
}
LazyColumn(
EncryptedTagArraySection(
modifier = Modifier
.fillMaxWidth()
.weight(1f)
.padding(top = 8.dp),
) {
items(mappedTags) { item ->
Column(
modifier = Modifier
.fillMaxWidth(),
) {
Text(
text = item.name,
fontWeight = FontWeight.Bold,
fontSize = 18.sp,
modifier = Modifier
.fillMaxWidth()
.padding(bottom = 4.dp),
)
item.values.forEach { value ->
Text(
fontSize = 16.sp,
text = value,
modifier = Modifier
.fillMaxWidth()
.padding(bottom = 2.dp),
)
}
}
HorizontalDivider(
color = MaterialTheme.colorScheme.primary,
)
}
}
encryptedData.tagArray,
)
} else {
val content = if (type.name.contains("ENCRYPT") && encryptedData is ClearTextEncryptedDataKind) {
encryptedData.text
@@ -0,0 +1,47 @@
package com.greenart7c3.nostrsigner.ui.components
import android.content.ClipData
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.ClipEntry
import androidx.compose.ui.platform.LocalClipboard
import androidx.compose.ui.res.stringResource
import com.greenart7c3.nostrsigner.Amber
import com.greenart7c3.nostrsigner.R
import com.vitorpamplona.quartz.nip01Core.core.TagArray
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
@Composable
fun EncryptedTagArraySection(
modifier: Modifier,
tags: TagArray,
) {
val clipboard = LocalClipboard.current
LazyColumn(
modifier = modifier,
) {
item {
TagsSection(
stringResource(R.string.tags),
tags,
{
Amber.instance.applicationIOScope.launch(Dispatchers.Main) {
clipboard.setClipEntry(
ClipEntry(
ClipData.newPlainText("Tags", tags.joinToString(separator = ", ") { "[${it.joinToString(separator = ", ") { tag -> "\"${tag}\"" }}]" }),
),
)
}
},
)
HorizontalDivider(
color = MaterialTheme.colorScheme.primary,
)
}
}
}
@@ -1,31 +1,20 @@
package com.greenart7c3.nostrsigner.ui.components
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.ModalBottomSheet
import androidx.compose.material3.ModalBottomSheetProperties
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.material3.rememberModalBottomSheetState
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.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.res.vectorResource
import androidx.compose.ui.text.SpanStyle
import androidx.compose.ui.text.buildAnnotatedString
import androidx.compose.ui.text.capitalize
@@ -35,88 +24,10 @@ import androidx.compose.ui.text.withStyle
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import com.greenart7c3.nostrsigner.R
import com.greenart7c3.nostrsigner.models.Account
import com.greenart7c3.nostrsigner.models.Permission
import com.greenart7c3.nostrsigner.models.SignerType
import com.greenart7c3.nostrsigner.models.TimeUtils
import com.greenart7c3.nostrsigner.service.toShortenHex
import com.greenart7c3.nostrsigner.ui.RememberType
import com.greenart7c3.nostrsigner.ui.verticalScrollbar
import com.vitorpamplona.quartz.nip01Core.core.Event
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun EventDetailModal(
event: Event,
permission: Permission,
onDismiss: () -> Unit,
) {
val sheetState = rememberModalBottomSheetState(
skipPartiallyExpanded = true,
)
ModalBottomSheet(
sheetGesturesEnabled = false,
properties = ModalBottomSheetProperties(
shouldDismissOnBackPress = false,
shouldDismissOnClickOutside = false,
),
sheetMaxWidth = Int.MAX_VALUE.dp,
sheetState = sheetState,
onDismissRequest = onDismiss,
) {
Scaffold(
bottomBar = {
Box(
Modifier.background(MaterialTheme.colorScheme.primary),
) {
IconRow(
center = true,
title = stringResource(R.string.go_back),
icon = ImageVector.vectorResource(R.drawable.back),
onClick = onDismiss,
tint = MaterialTheme.colorScheme.onSurfaceVariant,
)
}
},
) {
val scrollState = rememberScrollState()
Column(
Modifier
.padding(it)
.verticalScrollbar(scrollState)
.verticalScroll(scrollState),
) {
EventSection(
stringResource(R.string.kind),
"${event.kind} - ${permission.toLocalizedString(LocalContext.current)}",
)
EventSection(
stringResource(R.string.pubkey),
event.pubKey.toShortenHex(),
)
EventSection(
stringResource(R.string.date),
TimeUtils.formatLongToCustomDateTimeWithSeconds(event.createdAt),
)
if (event.content.isNotEmpty()) {
EventSection(
stringResource(R.string.content),
event.content,
)
}
if (event.tags.isNotEmpty()) {
TagsSection(
label = stringResource(R.string.tags),
tags = event.tags,
onCopy = {
},
)
}
}
}
}
}
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun EventData(
@@ -157,7 +68,6 @@ fun EventData(
if (showMore) {
EventDetailModal(
event = event,
permission = permission,
onDismiss = {
showMore = false
},
@@ -189,13 +99,10 @@ fun EventData(
@Composable
fun BunkerEventData(
account: Account,
modifier: Modifier,
shouldAcceptOrReject: Boolean?,
appName: String,
event: Event,
rawJson: String,
type: SignerType,
onAccept: (RememberType) -> Unit,
onReject: (RememberType) -> Unit,
) {
@@ -227,12 +134,11 @@ fun BunkerEventData(
onCLick = {
showMore = !showMore
},
if (!showMore) stringResource(R.string.show_details) else stringResource(R.string.hide_details),
stringResource(R.string.show_details),
)
if (showMore) {
EventDetailModal(
event = event,
permission = permission,
onDismiss = {
showMore = false
},
@@ -0,0 +1,138 @@
package com.greenart7c3.nostrsigner.ui.components
import android.content.ClipData
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.ModalBottomSheet
import androidx.compose.material3.ModalBottomSheetProperties
import androidx.compose.material3.Scaffold
import androidx.compose.material3.rememberModalBottomSheetState
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.platform.ClipEntry
import androidx.compose.ui.platform.Clipboard
import androidx.compose.ui.platform.LocalClipboard
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.res.vectorResource
import androidx.compose.ui.unit.dp
import com.greenart7c3.nostrsigner.Amber
import com.greenart7c3.nostrsigner.R
import com.greenart7c3.nostrsigner.models.Permission
import com.greenart7c3.nostrsigner.models.TimeUtils
import com.greenart7c3.nostrsigner.service.toShortenHex
import com.greenart7c3.nostrsigner.ui.verticalScrollbar
import com.vitorpamplona.quartz.nip01Core.core.Event
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
fun copyToClipboard(clipboard: Clipboard, text: String) {
Amber.instance.applicationIOScope.launch(Dispatchers.Main) {
clipboard.setClipEntry(
ClipEntry(
ClipData.newPlainText("Tags", text),
),
)
}
}
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun EventDetailModal(
event: Event,
onDismiss: () -> Unit,
) {
val clipboard = LocalClipboard.current
val permission = Permission(
"sign_event",
event.kind,
)
val context = LocalContext.current
val sheetState = rememberModalBottomSheetState(
skipPartiallyExpanded = true,
)
ModalBottomSheet(
sheetGesturesEnabled = false,
properties = ModalBottomSheetProperties(
shouldDismissOnBackPress = false,
shouldDismissOnClickOutside = false,
),
sheetMaxWidth = Int.MAX_VALUE.dp,
sheetState = sheetState,
onDismissRequest = onDismiss,
) {
Scaffold(
bottomBar = {
Box(
Modifier.background(MaterialTheme.colorScheme.primary),
) {
IconRow(
center = true,
title = stringResource(R.string.go_back),
icon = ImageVector.vectorResource(R.drawable.back),
onClick = onDismiss,
tint = MaterialTheme.colorScheme.onSurfaceVariant,
)
}
},
) {
val scrollState = rememberScrollState()
Column(
Modifier
.padding(it)
.verticalScrollbar(scrollState)
.verticalScroll(scrollState),
) {
EventSection(
stringResource(R.string.kind),
"${event.kind} - ${permission.toLocalizedString(LocalContext.current)}",
{
copyToClipboard(clipboard, "${event.kind} - ${permission.toLocalizedString(context)}")
},
)
EventSection(
stringResource(R.string.pubkey),
event.pubKey.toShortenHex(),
{
copyToClipboard(clipboard, event.pubKey)
},
)
EventSection(
stringResource(R.string.date),
TimeUtils.formatLongToCustomDateTimeWithSeconds(event.createdAt),
{
copyToClipboard(clipboard, "${event.createdAt}")
},
)
if (event.content.isNotEmpty()) {
EventSection(
stringResource(R.string.content),
event.content,
{
copyToClipboard(clipboard, event.content)
},
)
}
if (event.tags.isNotEmpty()) {
TagsSection(
label = stringResource(R.string.tags),
tags = event.tags,
onCopy = {
copyToClipboard(
clipboard,
event.tags.joinToString(separator = ", ") { "[${it.joinToString(separator = ", ") { tag -> "\"${tag}\"" }}]" },
)
},
)
}
}
}
}
}
@@ -26,6 +26,7 @@ import com.greenart7c3.nostrsigner.R
fun EventSection(
label: String,
displayValue: String,
onCopy: () -> Unit,
) {
Row(
modifier = Modifier
@@ -55,7 +56,9 @@ fun EventSection(
Icon(
modifier = Modifier
.size(16.dp)
.clickable { },
.clickable {
onCopy()
},
imageVector = Icons.Default.ContentCopy,
contentDescription = stringResource(id = R.string.copy),
)
@@ -13,7 +13,6 @@ import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
@@ -27,12 +26,8 @@ import com.greenart7c3.nostrsigner.R
fun TagsSection(
label: String,
tags: Array<Array<String>>,
onCopy: (String) -> Unit,
onCopy: () -> Unit,
) {
val tagsAsStringForCopy = remember(tags) {
tags.toString()
}
val tagsToShow = tags.take(10)
val moreCount = tags.size - 10
@@ -85,7 +80,7 @@ fun TagsSection(
Icon(
modifier = Modifier
.size(16.dp)
.clickable { onCopy(tagsAsStringForCopy) },
.clickable { onCopy() },
imageVector = Icons.Default.ContentCopy,
contentDescription = stringResource(id = R.string.copy_to_clipboard),
)