Refactor UI components and enhance multi-request handling

- Redesign `RememberMyChoice` using a new `LabeledBorderBox` and `AmberToggles` for a more compact selection.
- Introduce `LocalAppIcon` to display application icons and names consistently across screens.
- Implement `EventDetailModal`, `EventSection`, and `TagsSection` to provide detailed, scrollable event information.
- Refactor `MultiEventHomeScreen` to support vertical scrolling and grouped request approval/discard actions.
- Update `EventData`, `SignMessage`, and `EncryptDecryptData` to use the new UI patterns and improve permission messaging.
- Add short-form string resources for time intervals (1m, 5m, 10m) and new L10n strings for event details.
- Optimize button styles and layouts in `AmberButton` and `AcceptRejectButtons`.
- Support `PrivateZapEncryptedDataKind` in decryption flows.
This commit is contained in:
greenart7c3
2026-01-12 17:05:39 -03:00
parent 82d1f5cd7f
commit f669929626
19 changed files with 1477 additions and 1181 deletions
@@ -62,7 +62,6 @@ import com.greenart7c3.nostrsigner.models.Account
import com.greenart7c3.nostrsigner.models.AmberBunkerRequest
import com.greenart7c3.nostrsigner.models.IntentData
import com.greenart7c3.nostrsigner.models.IntentResultType
import com.greenart7c3.nostrsigner.models.SignerType
import com.greenart7c3.nostrsigner.service.crashreports.DisplayCrashMessages
import com.greenart7c3.nostrsigner.ui.actions.AccountBackupScreen
import com.greenart7c3.nostrsigner.ui.actions.AccountsBottomSheet
@@ -341,8 +340,8 @@ fun MainScreen(
Route.IncomingRequest.route,
content = {
val scrollState = rememberScrollState()
val isConnectEvent = intents.firstOrNull()?.type == SignerType.GET_PUBLIC_KEY
val modifier = if (intents.isEmpty() || packageName == null || destinationRoute != Route.IncomingRequest.route || !isConnectEvent) {
val modifier = if (intents.isEmpty() || packageName == null || destinationRoute != Route.IncomingRequest.route) {
Modifier
.fillMaxSize()
.padding(padding)
@@ -30,6 +30,7 @@ import com.greenart7c3.nostrsigner.models.ClearTextEncryptedDataKind
import com.greenart7c3.nostrsigner.models.EventEncryptedDataKind
import com.greenart7c3.nostrsigner.models.Permission
import com.greenart7c3.nostrsigner.models.SignerType
import com.greenart7c3.nostrsigner.models.TagArrayEncryptedDataKind
import com.greenart7c3.nostrsigner.service.BunkerRequestUtils
import com.greenart7c3.nostrsigner.service.MultiEventScreenIntents
import com.greenart7c3.nostrsigner.service.model.AmberEvent
@@ -137,7 +138,13 @@ fun SeeDetailsScreen(
intent.encryptedData.event.content
}
} else {
intent.encryptedData?.result ?: ""
if (intent.encryptedData is TagArrayEncryptedDataKind) {
intent.encryptedData.tagArray.joinToString(separator = ", ") {
"[${it.joinToString(separator = ", ") { tag -> "\"${tag}\"" }}]"
}
} else {
intent.encryptedData?.result ?: ""
}
}
}
@@ -196,7 +203,13 @@ fun SeeDetailsScreen(
bunkerRequest.encryptedData.event.content
}
} else {
bunkerRequest.encryptedData?.result ?: BunkerRequestUtils.getDataFromBunker(bunkerRequest.request)
if (bunkerRequest.encryptedData is TagArrayEncryptedDataKind) {
bunkerRequest.encryptedData.tagArray.joinToString(separator = ", ") {
"[${it.joinToString(separator = ", ") { tag -> "\"${tag}\"" }}]"
}
} else {
bunkerRequest.encryptedData?.result ?: BunkerRequestUtils.getDataFromBunker(bunkerRequest.request)
}
}
}
@@ -1,10 +1,9 @@
package com.greenart7c3.nostrsigner.ui.components
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.ButtonDefaults
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
@@ -18,24 +17,25 @@ fun AcceptRejectButtons(
onAccept: () -> Unit,
onReject: () -> Unit,
) {
Column(
Row(
Modifier
.fillMaxWidth(),
Arrangement.Center,
.fillMaxWidth()
.padding(vertical = 16.dp),
Arrangement.spacedBy(8.dp),
) {
AmberButton(
modifier = Modifier.fillMaxWidth(),
onClick = onAccept,
text = stringResource(R.string.accept),
)
Spacer(modifier = Modifier.width(20.dp))
AmberButton(
Modifier.weight(1f),
onClick = onReject,
colors = ButtonDefaults.buttonColors(
containerColor = Color(0xFFFF6B00),
),
text = stringResource(R.string.reject),
)
AmberButton(
Modifier.weight(1f),
onClick = onAccept,
text = stringResource(R.string.accept),
)
}
}
@@ -51,7 +51,7 @@ fun AmberButton(
CompositionLocalProvider(
LocalDensity provides Density(
LocalDensity.current.density,
1.5f,
1f,
),
) {
Text(
@@ -59,7 +59,7 @@ fun AmberButton(
color = textColor,
textAlign = textAlign,
maxLines = maxLines,
fontWeight = FontWeight.Normal,
fontWeight = FontWeight.Bold,
)
}
}
@@ -96,7 +96,7 @@ fun AmberElevatedButton(
CompositionLocalProvider(
LocalDensity provides Density(
LocalDensity.current.density,
1.5f,
1f,
),
) {
Text(
@@ -104,7 +104,7 @@ fun AmberElevatedButton(
color = textColor,
textAlign = textAlign,
maxLines = maxLines,
fontWeight = FontWeight.Normal,
fontWeight = FontWeight.Bold,
)
}
}
@@ -40,7 +40,6 @@ import com.greenart7c3.nostrsigner.R
import com.greenart7c3.nostrsigner.models.Account
import com.greenart7c3.nostrsigner.models.AmberBunkerRequest
import com.greenart7c3.nostrsigner.models.IntentData
import com.greenart7c3.nostrsigner.models.SignerType
import com.greenart7c3.nostrsigner.service.toShortenHex
import com.greenart7c3.nostrsigner.ui.navigation.Route
import com.greenart7c3.nostrsigner.ui.navigation.routes
@@ -63,8 +62,7 @@ fun AmberTopAppBar(
bunkerRequests: List<AmberBunkerRequest>,
packageName: String?,
) {
val isConnectEvent = intents.firstOrNull()?.type == SignerType.GET_PUBLIC_KEY
if (intents.isEmpty() || packageName == null || destinationRoute != Route.IncomingRequest.route || !isConnectEvent) {
if (intents.isEmpty() || packageName == null || destinationRoute != Route.IncomingRequest.route) {
if (destinationRoute != "login" && destinationRoute != "create" && destinationRoute != "loginPage") {
CenterAlignedTopAppBar(
actions = {
@@ -1,34 +1,26 @@
package com.greenart7c3.nostrsigner.ui.components
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.ButtonDefaults
import androidx.compose.material3.Text
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.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextAlign
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.ui.RememberType
@Composable
fun BunkerGetPubKeyScreen(
modifier: Modifier,
account: Account,
applicationName: String,
onAccept: (List<Permission>?, Int, Boolean?, RememberType) -> Unit,
onReject: (RememberType) -> Unit,
@@ -38,8 +30,6 @@ fun BunkerGetPubKeyScreen(
Column(
modifier,
) {
ProfilePicture(account)
Text(
modifier = Modifier.fillMaxWidth(),
text = applicationName,
@@ -54,42 +44,24 @@ fun BunkerGetPubKeyScreen(
Spacer(modifier = Modifier.weight(1f))
Column(
Modifier
.fillMaxWidth()
.padding(10.dp),
Arrangement.Center,
Alignment.CenterHorizontally,
) {
RememberMyChoice(
alwaysShow = true,
shouldRunAcceptOrReject = null,
onAccept = {},
onReject = onReject,
onChanged = {
rememberType = it
},
packageName = null,
)
RememberMyChoice(
alwaysShow = true,
shouldRunAcceptOrReject = null,
onAccept = {},
onReject = onReject,
onChanged = {
rememberType = it
},
packageName = null,
)
AmberButton(
modifier = Modifier.padding(vertical = 20.dp),
onClick = {
onAccept(null, 1, null, rememberType)
},
text = stringResource(R.string.grant_permissions),
)
AmberButton(
modifier = Modifier.padding(vertical = 20.dp),
onClick = {
onReject(rememberType)
},
colors = ButtonDefaults.buttonColors(
containerColor = Color(0xFFFF6B00),
),
text = stringResource(R.string.reject),
)
}
AcceptRejectButtons(
onAccept = {
onAccept(null, 1, null, rememberType)
},
onReject = {
onReject(rememberType)
},
)
}
}
@@ -22,12 +22,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.ui.RememberType
@Composable
fun BunkerPingScreen(
account: Account,
modifier: Modifier,
shouldRunOnAccept: Boolean?,
appName: String,
@@ -41,8 +39,6 @@ fun BunkerPingScreen(
Column(
modifier,
) {
ProfilePicture(account)
val message = stringResource(R.string.ping)
Text(
@@ -105,7 +105,6 @@ fun BunkerSingleEventHomeScreen(
BunkerPingScreen(
modifier = modifier,
account = account,
appName = appName,
shouldRunOnAccept = acceptOrReject,
onAccept = {
@@ -300,7 +299,6 @@ fun BunkerSingleEventHomeScreen(
is BunkerRequestGetPublicKey -> {
BunkerGetPubKeyScreen(
modifier = modifier,
account = account,
applicationName = appName,
onAccept = { permissions, signPolicy, closeApplication, rememberType ->
val result = account.hexKey
@@ -362,7 +360,6 @@ fun BunkerSingleEventHomeScreen(
}
BunkerEncryptDecryptData(
account = account,
modifier = modifier,
encryptedData = bunkerRequest.encryptedData,
shouldRunOnAccept = acceptOrReject,
@@ -426,7 +423,6 @@ fun BunkerSingleEventHomeScreen(
}
BunkerEncryptDecryptData(
account = account,
modifier = modifier,
encryptedData = bunkerRequest.encryptedData,
shouldRunOnAccept = acceptOrReject,
@@ -492,7 +488,6 @@ fun BunkerSingleEventHomeScreen(
}
BunkerEncryptDecryptData(
account = account,
modifier = modifier,
encryptedData = bunkerRequest.encryptedData,
shouldRunOnAccept = acceptOrReject,
@@ -558,7 +553,6 @@ fun BunkerSingleEventHomeScreen(
}
BunkerEncryptDecryptData(
account = account,
modifier = modifier,
encryptedData = bunkerRequest.encryptedData,
shouldRunOnAccept = acceptOrReject,
@@ -710,7 +704,6 @@ fun BunkerSingleEventHomeScreen(
}
BunkerEncryptDecryptData(
account = account,
modifier = modifier,
encryptedData = bunkerRequest.encryptedData,
shouldRunOnAccept = acceptOrReject,
@@ -772,12 +765,10 @@ fun BunkerSingleEventHomeScreen(
}
BunkerSignMessage(
account = account,
modifier = modifier,
content = bunkerRequest.request.params.first(),
shouldRunOnAccept = acceptOrReject,
appName = appName,
type = type,
onAccept = {
Amber.instance.applicationIOScope.launch(Dispatchers.IO) {
val result = account.signString(bunkerRequest.request.params.first())
@@ -24,17 +24,16 @@ import androidx.compose.ui.text.buildAnnotatedString
import androidx.compose.ui.text.capitalize
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.intl.Locale
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.text.withStyle
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import com.greenart7c3.nostrsigner.Amber
import com.greenart7c3.nostrsigner.R
import com.greenart7c3.nostrsigner.models.Account
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.ui.RememberType
@@ -42,12 +41,200 @@ import com.vitorpamplona.quartz.nip02FollowList.ContactListEvent
@Composable
fun EncryptDecryptData(
account: Account,
modifier: Modifier,
encryptedData: EncryptedDataKind?,
shouldRunOnAccept: Boolean?,
packageName: String?,
applicationName: String?,
type: SignerType,
onAccept: (RememberType) -> Unit,
onReject: (RememberType) -> Unit,
) {
var rememberType by remember {
mutableStateOf(RememberType.NEVER)
}
Column(
modifier,
) {
LocalAppIcon(packageName)
val text = if (type.name.contains("ENCRYPT")) {
when (encryptedData) {
is EventEncryptedDataKind -> {
val permission = Permission("sign_event", encryptedData.event.kind)
stringResource(R.string.wants_to_encrypt_with, permission.toLocalizedString(Amber.instance), type.name.split("_").first())
}
is TagArrayEncryptedDataKind -> {
stringResource(R.string.wants_to_encrypt_this_list_of_tags_with, type.name.split("_").first())
}
else -> stringResource(R.string.wants_to_encrypt_this_text_with, type.name.split("_").first())
}
} else {
when (encryptedData) {
is EventEncryptedDataKind -> {
val permission = Permission("sign_event", encryptedData.event.kind)
stringResource(R.string.wants_to_read_from_encrypted_content, permission.toLocalizedString(Amber.instance), type.name.split("_").first())
}
is TagArrayEncryptedDataKind -> {
stringResource(R.string.wants_to_read_this_list_of_tags_from_encrypted_content, type.name.split("_").first())
}
is PrivateZapEncryptedDataKind -> {
stringResource(R.string.wants_you_to, stringResource(R.string.decrypt_zap_event))
}
else -> stringResource(R.string.wants_to_read_this_text_from_encrypted_content, type.name.split("_").first())
}
}
Text(
text.trim().capitalize(Locale.current),
fontSize = 18.sp,
)
Spacer(Modifier.size(4.dp))
Card(
modifier = Modifier
.fillMaxWidth()
.weight(1f),
colors = CardDefaults.cardColors().copy(
containerColor = MaterialTheme.colorScheme.background,
),
) {
Column {
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),
)
}
}
} 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),
)
}
}
} 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(
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,
)
}
}
} else {
val content = if (type.name.contains("ENCRYPT") && encryptedData is ClearTextEncryptedDataKind) {
encryptedData.text
} else {
encryptedData?.result ?: ""
}
Text(
content,
modifier = Modifier
.fillMaxWidth()
.padding(top = 8.dp),
)
}
}
}
}
if (encryptedData !is TagArrayEncryptedDataKind) Spacer(modifier = Modifier.weight(1f))
RememberMyChoice(
shouldRunOnAccept,
packageName,
false,
onAccept,
onReject,
) {
rememberType = it
}
AcceptRejectButtons(
onAccept = {
onAccept(rememberType)
},
onReject = {
onReject(rememberType)
},
)
}
}
data class TagItem(
val name: String,
val values: List<String>,
)
@Composable
fun BunkerEncryptDecryptData(
modifier: Modifier,
encryptedData: EncryptedDataKind?,
shouldRunOnAccept: Boolean?,
appName: String,
type: SignerType,
onAccept: (RememberType) -> Unit,
@@ -60,26 +247,11 @@ fun EncryptDecryptData(
Column(
modifier,
) {
ProfilePicture(account)
packageName?.let {
Text(
modifier = Modifier
.fillMaxWidth(),
text = it,
fontWeight = FontWeight.Bold,
fontSize = 18.sp,
textAlign = TextAlign.Center,
)
Spacer(Modifier.size(4.dp))
}
Text(
buildAnnotatedString {
withStyle(style = SpanStyle(fontWeight = FontWeight.Bold)) {
append(applicationName ?: appName)
append(appName)
}
if (type.name.contains("ENCRYPT")) {
when (encryptedData) {
is EventEncryptedDataKind -> {
@@ -220,153 +392,6 @@ fun EncryptDecryptData(
if (encryptedData !is TagArrayEncryptedDataKind) Spacer(modifier = Modifier.weight(1f))
RememberMyChoice(
shouldRunOnAccept,
packageName,
false,
onAccept,
onReject,
) {
rememberType = it
}
AcceptRejectButtons(
onAccept = {
onAccept(rememberType)
},
onReject = {
onReject(rememberType)
},
)
}
}
data class TagItem(
val name: String,
val values: List<String>,
)
@Composable
fun BunkerEncryptDecryptData(
account: Account,
modifier: Modifier,
encryptedData: EncryptedDataKind?,
shouldRunOnAccept: Boolean?,
appName: String,
type: SignerType,
onAccept: (RememberType) -> Unit,
onReject: (RememberType) -> Unit,
) {
var rememberType by remember {
mutableStateOf(RememberType.NEVER)
}
Column(
modifier,
) {
ProfilePicture(account)
Text(
buildAnnotatedString {
withStyle(style = SpanStyle(fontWeight = FontWeight.Bold)) {
append(appName)
}
if (type.name.contains("ENCRYPT")) {
when (encryptedData) {
is EventEncryptedDataKind -> {
val permission = Permission("sign_event", encryptedData.event.kind)
append(stringResource(R.string.wants_to_encrypt_with, permission.toLocalizedString(Amber.instance), type.name.split("_").first()))
}
is TagArrayEncryptedDataKind -> {
append(stringResource(R.string.wants_to_encrypt_this_list_of_tags_with, type.name.split("_").first()))
}
else -> append(stringResource(R.string.wants_to_encrypt_this_text_with, type.name.split("_").first()))
}
} else {
when (encryptedData) {
is EventEncryptedDataKind -> {
val permission = Permission("sign_event", encryptedData.event.kind)
append(stringResource(R.string.wants_to_read_from_encrypted_content, permission.toLocalizedString(Amber.instance), type.name.split("_").first()))
}
is TagArrayEncryptedDataKind -> {
append(stringResource(R.string.wants_to_read_this_list_of_tags_from_encrypted_content, type.name.split("_").first()))
}
else -> append(stringResource(R.string.wants_to_read_this_text_from_encrypted_content, type.name.split("_").first()))
}
}
},
fontSize = 18.sp,
)
Spacer(Modifier.size(4.dp))
Card(
modifier = Modifier
.fillMaxWidth(),
) {
Column(Modifier.padding(6.dp)) {
Text(
stringResource(R.string.content).capitalize(Locale.current),
fontWeight = FontWeight.Bold,
)
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),
)
}
}
} 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),
)
}
}
} else {
val content = if (type.name.contains("ENCRYPT") && encryptedData is ClearTextEncryptedDataKind) {
encryptedData.text
} else if (encryptedData is TagArrayEncryptedDataKind) {
encryptedData.tagArray.map { it.toList() }.toList().toString()
} else {
encryptedData?.result ?: ""
}
Text(
content,
modifier = Modifier
.fillMaxWidth()
.padding(top = 8.dp),
)
}
}
}
Spacer(modifier = Modifier.weight(1f))
RememberMyChoice(
shouldRunOnAccept,
null,
@@ -1,28 +1,36 @@
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.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.material3.Card
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.key
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
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.text.intl.Locale
import androidx.compose.ui.text.withStyle
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
@@ -30,22 +38,92 @@ 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.service.model.AmberEvent
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
import com.vitorpamplona.quartz.nip02FollowList.ContactListEvent
@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(
account: Account,
modifier: Modifier,
shouldAcceptOrReject: Boolean?,
packageName: String?,
appName: String,
applicationName: String?,
event: Event,
rawJson: String,
type: SignerType,
onAccept: (RememberType) -> Unit,
onReject: (RememberType) -> Unit,
) {
@@ -60,76 +138,30 @@ fun EventData(
Column(
modifier,
) {
ProfilePicture(account)
LocalAppIcon(packageName)
val permission = Permission("sign_event", event.kind)
val text = stringResource(R.string.wants_you_to_sign_a, permission.toLocalizedString(context))
packageName?.let {
Text(
modifier = Modifier
.fillMaxWidth(),
text = it,
fontWeight = FontWeight.Bold,
fontSize = 18.sp,
textAlign = TextAlign.Center,
)
Spacer(Modifier.size(4.dp))
}
Text(
buildAnnotatedString {
withStyle(style = SpanStyle(fontWeight = FontWeight.Bold)) {
append(applicationName ?: appName)
}
append(" $text")
},
text.capitalize(Locale.current),
fontSize = 18.sp,
)
Spacer(Modifier.size(4.dp))
val content = if (event.kind == 22242) AmberEvent.relay(event) else event.content
if (content.isNotBlank()) {
key("event-data-card") {
Card(
modifier = Modifier
.fillMaxWidth(),
) {
Column(Modifier.padding(16.dp)) {
if (event is ContactListEvent) {
ContactListDetail(
title = stringResource(R.string.following),
text = "${event.verifiedFollowKeySet().size}",
)
ContactListDetail(
title = stringResource(R.string.relays_text),
text = "${event.relays()?.keys?.size ?: 0}",
)
} else {
Text(
"Event content",
fontWeight = FontWeight.Bold,
)
Text(
content,
maxLines = 1,
overflow = TextOverflow.Ellipsis,
modifier = Modifier
.fillMaxWidth()
.padding(top = 8.dp),
)
}
}
}
}
}
RawJsonButton(
onCLick = {
showMore = !showMore
},
if (!showMore) stringResource(R.string.show_details) else stringResource(R.string.hide_details),
stringResource(R.string.show_details),
)
if (showMore) {
RawJson(rawJson, "", Modifier.height(200.dp), type = type)
EventDetailModal(
event = event,
permission = permission,
onDismiss = {
showMore = false
},
)
} else {
Spacer(modifier = Modifier.weight(1f))
}
@@ -178,8 +210,6 @@ fun BunkerEventData(
Column(
modifier,
) {
ProfilePicture(account)
val permission = Permission("sign_event", event.kind)
val text = stringResource(R.string.wants_you_to_sign_a, permission.toLocalizedString(context))
Text(
@@ -193,42 +223,6 @@ fun BunkerEventData(
)
Spacer(Modifier.size(4.dp))
val content = if (event.kind == 22242) AmberEvent.relay(event) else event.content
if (content.isNotBlank()) {
key("event-data-card") {
Card(
modifier = Modifier
.fillMaxWidth(),
) {
Column(Modifier.padding(16.dp)) {
if (event is ContactListEvent) {
ContactListDetail(
title = stringResource(R.string.following),
text = "${event.verifiedFollowKeySet().size}",
)
ContactListDetail(
title = stringResource(R.string.relays_text),
text = "${event.relays()?.keys?.size ?: 0}",
)
} else {
Text(
"Event content",
fontWeight = FontWeight.Bold,
)
Text(
content,
maxLines = 1,
overflow = TextOverflow.Ellipsis,
modifier = Modifier
.fillMaxWidth()
.padding(top = 8.dp),
)
}
}
}
}
}
RawJsonButton(
onCLick = {
showMore = !showMore
@@ -236,7 +230,13 @@ fun BunkerEventData(
if (!showMore) stringResource(R.string.show_details) else stringResource(R.string.hide_details),
)
if (showMore) {
RawJson(rawJson, "", Modifier.height(200.dp), type = type)
EventDetailModal(
event = event,
permission = permission,
onDismiss = {
showMore = false
},
)
} else {
Spacer(modifier = Modifier.weight(1f))
}
@@ -0,0 +1,63 @@
package com.greenart7c3.nostrsigner.ui.components
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.ContentCopy
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import com.greenart7c3.nostrsigner.R
@Composable
fun EventSection(
label: String,
displayValue: String,
) {
Row(
modifier = Modifier
.fillMaxWidth()
.padding(16.dp),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.Top,
) {
Column(modifier = Modifier.weight(1f)) {
Text(
text = label,
style = MaterialTheme.typography.bodyLarge.copy(fontSize = 16.sp, lineHeight = 20.sp),
fontWeight = FontWeight.SemiBold,
color = MaterialTheme.colorScheme.onSurface,
)
Text(
modifier = Modifier.padding(top = 12.dp),
text = displayValue,
style = MaterialTheme.typography.bodyMedium.copy(
lineHeight = 24.sp,
),
maxLines = 1,
overflow = TextOverflow.Ellipsis,
fontWeight = FontWeight.Bold,
)
}
Icon(
modifier = Modifier
.size(16.dp)
.clickable { },
imageVector = Icons.Default.ContentCopy,
contentDescription = stringResource(id = R.string.copy),
)
}
}
@@ -119,14 +119,10 @@ fun IntentSingleEventHomeScreen(
val acceptOrReject = IntentUtils.isRemembered(applicationEntity?.application?.signPolicy, permission)
SignMessage(
account = account,
modifier = modifier,
content = intentData.data,
shouldRunOnAccept = acceptOrReject,
packageName = packageName,
applicationName = applicationName,
appName = appName,
type = intentData.type,
onAccept = {
Amber.instance.applicationIOScope.launch(Dispatchers.IO) {
val result = account.signString(intentData.data)
@@ -182,13 +178,10 @@ fun IntentSingleEventHomeScreen(
val acceptOrReject = IntentUtils.isRemembered(applicationEntity?.application?.signPolicy, permission)
EncryptDecryptData(
account = account,
modifier = modifier,
encryptedData = intentData.encryptedData,
shouldRunOnAccept = acceptOrReject,
packageName = packageName,
applicationName = applicationName,
appName = appName,
type = intentData.type,
onAccept = {
val result =
@@ -259,15 +252,10 @@ fun IntentSingleEventHomeScreen(
val acceptOrReject = IntentUtils.isRemembered(applicationEntity?.application?.signPolicy, permission)
EventData(
account = account,
modifier = modifier,
shouldAcceptOrReject = acceptOrReject,
packageName = packageName,
appName = appName,
applicationName = applicationName,
event = event,
rawJson = event.toJson(),
type = intentData.type,
onAccept = {
if (intentData.unsignedEventKey.isNotBlank() && intentData.unsignedEventKey != account.hexKey && !isPrivateEvent(event.kind, event.tags)) {
accountStateViewModel.toast(
@@ -0,0 +1,62 @@
package com.greenart7c3.nostrsigner.ui.components
import androidx.compose.foundation.Image
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.asImageBitmap
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.core.graphics.drawable.toBitmap
@Composable
fun LocalAppIcon(packageName: String?) {
packageName?.let {
val appDisplayInfo = rememberAppDisplayInfo(packageName)
Column(
modifier = Modifier.padding(vertical = 16.dp),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.spacedBy(8.dp),
) {
if (appDisplayInfo.icon != null) {
Image(
modifier = Modifier
.size(48.dp)
.clip(MaterialTheme.shapes.small),
bitmap = appDisplayInfo.icon.toBitmap().asImageBitmap(),
contentDescription = appDisplayInfo.name,
contentScale = ContentScale.Crop,
)
}
Text(
text = appDisplayInfo.name,
style = MaterialTheme.typography.titleLarge.copy(
fontSize = 18.sp,
lineHeight = 24.sp,
),
fontWeight = FontWeight.Bold,
)
Text(
modifier = Modifier
.fillMaxWidth(),
text = packageName,
fontWeight = FontWeight.Bold,
fontSize = 18.sp,
textAlign = TextAlign.Center,
)
}
}
}
@@ -31,7 +31,6 @@ import androidx.compose.material3.ModalBottomSheet
import androidx.compose.material3.PrimaryTabRow
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.material3.adaptive.currentWindowAdaptiveInfo
import androidx.compose.material3.rememberModalBottomSheetState
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
@@ -55,7 +54,6 @@ import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.core.graphics.drawable.toBitmap
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.window.core.layout.WindowSizeClass.Companion.WIDTH_DP_MEDIUM_LOWER_BOUND
import coil3.compose.AsyncImage
import com.greenart7c3.nostrsigner.Amber
import com.greenart7c3.nostrsigner.BuildFlavorChecker
@@ -68,52 +66,6 @@ import com.greenart7c3.nostrsigner.ui.RememberType
import com.greenart7c3.nostrsigner.ui.navigation.Route
import com.greenart7c3.nostrsigner.ui.theme.fromHex
@Composable
fun ProfilePicture(account: Account) {
val windowSizeClass = currentWindowAdaptiveInfo().windowSizeClass
if (windowSizeClass.isWidthAtLeastBreakpoint(WIDTH_DP_MEDIUM_LOWER_BOUND)) {
Column(
modifier = Modifier.fillMaxWidth(),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center,
) {
val profileUrl by account.picture.collectAsStateWithLifecycle()
if (profileUrl.isNotBlank() && !BuildFlavorChecker.isOfflineFlavor()) {
AsyncImage(
profileUrl,
Route.Accounts.route,
Modifier
.clip(
RoundedCornerShape(50),
)
.height(120.dp)
.width(120.dp),
)
} else {
Icon(
Icons.Outlined.Person,
Route.Accounts.route,
modifier = Modifier
.border(
2.dp,
Color.fromHex(account.hexKey.slice(0..5)),
CircleShape,
)
.height(120.dp)
.width(120.dp),
)
}
val name by account.name.collectAsStateWithLifecycle()
Text(
name.ifBlank { account.npub.toShortenHex() },
fontWeight = FontWeight.Bold,
fontSize = 18.sp,
textAlign = TextAlign.Center,
)
}
}
}
@Composable
fun ProfilePictureIcon(account: Account) {
val profileUrl by account.picture.collectAsStateWithLifecycle()
File diff suppressed because it is too large Load Diff
@@ -1,17 +1,61 @@
package com.greenart7c3.nostrsigner.ui.components
import androidx.compose.foundation.background
import androidx.compose.foundation.border
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableIntStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import com.greenart7c3.nostrsigner.R
import com.greenart7c3.nostrsigner.ui.RememberType
import com.greenart7c3.nostrsigner.ui.SettingsRow
import kotlinx.collections.immutable.persistentListOf
@Composable
fun LabeledBorderBox(
label: String,
modifier: Modifier = Modifier,
content: @Composable () -> Unit,
) {
Box(modifier = modifier) {
Box(
modifier = Modifier
.fillMaxWidth()
.padding(top = 8.dp)
.border(
width = 1.dp,
color = Color.Gray,
shape = RoundedCornerShape(8.dp),
)
.padding(4.dp),
) {
content()
}
Text(
text = label,
modifier = Modifier
.padding(start = 16.dp)
.background(MaterialTheme.colorScheme.background)
.padding(horizontal = 8.dp),
fontSize = 12.sp,
fontWeight = FontWeight.Light,
)
}
}
@Composable
fun RememberMyChoice(
@@ -22,14 +66,6 @@ fun RememberMyChoice(
onReject: (RememberType) -> Unit,
onChanged: (RememberType) -> Unit,
) {
val items =
persistentListOf(
TitleExplainer(stringResource(RememberType.NEVER.resourceId)),
TitleExplainer(stringResource(RememberType.ONE_MINUTE.resourceId)),
TitleExplainer(stringResource(RememberType.FIVE_MINUTES.resourceId)),
TitleExplainer(stringResource(RememberType.TEN_MINUTES.resourceId)),
TitleExplainer(stringResource(RememberType.ALWAYS.resourceId)),
)
var index by remember {
mutableIntStateOf(0)
}
@@ -43,16 +79,60 @@ fun RememberMyChoice(
}
}
if (packageName != null || alwaysShow) {
SettingsRow(
R.string.automatically_sign_this_for,
null,
items,
index,
maxLines = Int.MAX_VALUE,
overflow = TextOverflow.Clip,
LabeledBorderBox(
label = stringResource(R.string.automatically_sign_this_for),
) {
index = it
onChanged(RememberType.entries[it])
AmberToggles(
selectedIndex = index,
count = 5,
content = {
ToggleOption(
modifier = Modifier.width(55.dp),
text = stringResource(RememberType.NEVER.resourceId),
isSelected = RememberType.NEVER == RememberType.entries[index],
onClick = {
index = RememberType.NEVER.screenCode
onChanged(RememberType.NEVER)
},
)
ToggleOption(
modifier = Modifier.width(55.dp),
text = stringResource(R.string.one_minute_short),
isSelected = RememberType.ONE_MINUTE == RememberType.entries[index],
onClick = {
index = RememberType.ONE_MINUTE.screenCode
onChanged(RememberType.ONE_MINUTE)
},
)
ToggleOption(
modifier = Modifier.width(55.dp),
text = stringResource(R.string.five_minutes_short),
isSelected = RememberType.FIVE_MINUTES == RememberType.entries[index],
onClick = {
index = RememberType.FIVE_MINUTES.screenCode
onChanged(RememberType.FIVE_MINUTES)
},
)
ToggleOption(
modifier = Modifier.width(55.dp),
text = stringResource(R.string.ten_minutes_short),
isSelected = RememberType.TEN_MINUTES == RememberType.entries[index],
onClick = {
index = RememberType.TEN_MINUTES.screenCode
onChanged(RememberType.TEN_MINUTES)
},
)
ToggleOption(
modifier = Modifier.width(55.dp),
text = stringResource(RememberType.ALWAYS.resourceId),
isSelected = RememberType.ALWAYS == RememberType.entries[index],
onClick = {
index = RememberType.ALWAYS.screenCode
onChanged(RememberType.entries[RememberType.ALWAYS.screenCode])
},
)
},
)
}
}
}
@@ -1,11 +1,8 @@
package com.greenart7c3.nostrsigner.ui.components
import android.content.ClipData
import android.widget.Toast
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.material3.Card
@@ -14,46 +11,27 @@ import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.ClipEntry
import androidx.compose.ui.platform.LocalClipboard
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.SpanStyle
import androidx.compose.ui.text.buildAnnotatedString
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.text.style.TextOverflow
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.SignerType
import com.greenart7c3.nostrsigner.ui.RememberType
import kotlinx.coroutines.launch
@Composable
fun SignMessage(
account: Account,
modifier: Modifier,
content: String,
shouldRunOnAccept: Boolean?,
packageName: String?,
applicationName: String?,
appName: String,
type: SignerType,
onAccept: (RememberType) -> Unit,
onReject: (RememberType) -> Unit,
) {
var showMore by androidx.compose.runtime.remember {
mutableStateOf(false)
}
val clipboardManager = LocalClipboard.current
val coroutineScope = rememberCoroutineScope()
val context = LocalContext.current
var rememberType by remember {
mutableStateOf(RememberType.NEVER)
}
@@ -61,28 +39,13 @@ fun SignMessage(
Column(
modifier,
) {
ProfilePicture(account)
LocalAppIcon(packageName)
val message = stringResource(R.string.sign_message)
packageName?.let {
Text(
modifier = Modifier
.fillMaxWidth(),
text = it,
fontWeight = FontWeight.Bold,
fontSize = 18.sp,
textAlign = TextAlign.Center,
)
Spacer(Modifier.size(4.dp))
}
Text(
buildAnnotatedString {
withStyle(style = SpanStyle(fontWeight = FontWeight.Bold)) {
append(applicationName ?: appName)
}
append(" requests $message")
append(stringResource(R.string.requests_message, message))
},
fontSize = 18.sp,
)
@@ -100,8 +63,6 @@ fun SignMessage(
Text(
content,
maxLines = 1,
overflow = TextOverflow.Ellipsis,
modifier = Modifier
.fillMaxWidth()
.padding(top = 8.dp),
@@ -109,39 +70,7 @@ fun SignMessage(
}
}
RawJsonButton(
onCLick = {
showMore = !showMore
},
if (!showMore) stringResource(R.string.show_details) else stringResource(R.string.hide_details),
)
if (showMore) {
RawJson(
content,
"",
Modifier.height(200.dp),
stringResource(R.string.content),
type,
) {
coroutineScope.launch {
clipboardManager.setClipEntry(
ClipEntry(
ClipData.newPlainText("", content),
),
)
}
coroutineScope.launch {
Toast.makeText(
context,
context.getString(R.string.data_copied_to_the_clipboard),
Toast.LENGTH_SHORT,
).show()
}
}
} else {
Spacer(modifier = Modifier.weight(1f))
}
Spacer(Modifier.weight(1f))
RememberMyChoice(
shouldRunOnAccept,
@@ -166,21 +95,13 @@ fun SignMessage(
@Composable
fun BunkerSignMessage(
account: Account,
modifier: Modifier,
content: String,
shouldRunOnAccept: Boolean?,
appName: String,
type: SignerType,
onAccept: (RememberType) -> Unit,
onReject: (RememberType) -> Unit,
) {
var showMore by androidx.compose.runtime.remember {
mutableStateOf(false)
}
val clipboardManager = LocalClipboard.current
val coroutineScope = rememberCoroutineScope()
val context = LocalContext.current
var rememberType by remember {
mutableStateOf(RememberType.NEVER)
}
@@ -188,8 +109,6 @@ fun BunkerSignMessage(
Column(
modifier,
) {
ProfilePicture(account)
val message = stringResource(R.string.sign_message)
Text(
@@ -209,14 +128,12 @@ fun BunkerSignMessage(
) {
Column(Modifier.padding(6.dp)) {
Text(
"Event content",
stringResource(R.string.content),
fontWeight = FontWeight.Bold,
)
Text(
content,
maxLines = 1,
overflow = TextOverflow.Ellipsis,
modifier = Modifier
.fillMaxWidth()
.padding(top = 8.dp),
@@ -224,39 +141,7 @@ fun BunkerSignMessage(
}
}
RawJsonButton(
onCLick = {
showMore = !showMore
},
if (!showMore) stringResource(R.string.show_details) else stringResource(R.string.hide_details),
)
if (showMore) {
RawJson(
content,
"",
Modifier.height(200.dp),
stringResource(R.string.content),
type,
) {
coroutineScope.launch {
clipboardManager.setClipEntry(
ClipEntry(
ClipData.newPlainText("", content),
),
)
}
coroutineScope.launch {
Toast.makeText(
context,
context.getString(R.string.data_copied_to_the_clipboard),
Toast.LENGTH_SHORT,
).show()
}
}
} else {
Spacer(modifier = Modifier.weight(1f))
}
Spacer(Modifier.weight(1f))
RememberMyChoice(
shouldRunOnAccept,
@@ -0,0 +1,93 @@
package com.greenart7c3.nostrsigner.ui.components
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.ContentCopy
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
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import com.greenart7c3.nostrsigner.R
@Composable
fun TagsSection(
label: String,
tags: Array<Array<String>>,
onCopy: (String) -> Unit,
) {
val tagsAsStringForCopy = remember(tags) {
tags.toString()
}
val tagsToShow = tags.take(10)
val moreCount = tags.size - 10
Row(
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 16.dp, vertical = 12.dp),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.Top,
) {
Column(modifier = Modifier.weight(1f)) {
Text(
text = label,
style = MaterialTheme.typography.bodyLarge.copy(fontSize = 16.sp, lineHeight = 20.sp),
fontWeight = FontWeight.SemiBold,
color = MaterialTheme.colorScheme.onSurface,
)
if (tags.isNotEmpty()) {
Column(
modifier = Modifier.padding(top = 12.dp),
verticalArrangement = Arrangement.spacedBy(6.dp),
) {
tagsToShow.forEach { tag ->
val formattedTag = tag.joinToString(separator = ", ") {
"\"${it}\""
}
Text(
text = formattedTag,
style = MaterialTheme.typography.bodyMedium.copy(
lineHeight = 24.sp,
),
maxLines = 1,
overflow = TextOverflow.Ellipsis,
fontWeight = FontWeight.Bold,
)
}
if (moreCount > 0) {
Text(
text = stringResource(id = R.string.more, moreCount),
style = MaterialTheme.typography.bodyMedium.copy(
lineHeight = 24.sp,
),
fontWeight = FontWeight.Bold,
)
}
}
}
}
Icon(
modifier = Modifier
.size(16.dp)
.clickable { onCopy(tagsAsStringForCopy) },
imageVector = Icons.Default.ContentCopy,
contentDescription = stringResource(id = R.string.copy_to_clipboard),
)
}
}
+11
View File
@@ -260,6 +260,9 @@
<string name="one_minute">One minute</string>
<string name="five_minutes">Five minutes</string>
<string name="ten_minutes">Ten Minutes</string>
<string name="one_minute_short">1m</string>
<string name="five_minutes_short">5m</string>
<string name="ten_minutes_short">10m</string>
<string name="event_kind_9000">Add user to group</string>
<string name="event_kind_9001">Remove user from group</string>
<string name="event_kind_9002">Edit group metadata</string>
@@ -450,6 +453,7 @@
<string name="of_events">%1$s of %2$s events</string>
<string name="see_details">See details</string>
<string name="is_requiring_some_permissions_please_review_them">%1$s is requiring some permissions, please review them.</string>
<string name="is_requiring_some_permissions_please_review_them2">Is requiring some permissions, please review them.</string>
<string name="always_approve_this_permission">Always approve/reject this permission</string>
<string name="is_requiring_to_sign_these_events_related_to_permission">%1$s is requiring to sign these events related to %2$s permission.</string>
<string name="relay_filter_failed">Relay does not support retrieving Bunker events. Would you like to add it anyway?</string>
@@ -611,4 +615,11 @@
<string name="event_kind_10102">Good wiki relays</string>
<string name="status_service_description">Relay status notification</string>
<string name="status">Relay status</string>
<string name="kind">Kind</string>
<string name="pubkey">Pubkey</string>
<string name="date">Date</string>
<string name="tags">Tags</string>
<string name="discard_all">Discard selected</string>
<string name="approve_all">Approve selected</string>
<string name="requests_message">Requests %1$s</string>
</resources>