From 628aa0ef694aa9201a4f608d85a5cf6c78439ece Mon Sep 17 00:00:00 2001 From: greenart7c3 Date: Fri, 9 Jan 2026 07:11:32 -0300 Subject: [PATCH] Refactor bunker and login request screens to support account selection and improved permission management. - Add tabbed navigation to `BunkerConnectRequestScreen` and `LoginWithPubKey` to separate login and permission settings. - Implement account selection in the login tab, allowing users to choose which account to connect. - Move permission toggle list to a modal bottom sheet for a cleaner UI. - Update `AmberTopAppBar` to display application icons and improved titles for incoming requests. - Add `SignerConnectAppTab` component for consistent tab styling. - Update `LocalPreferences` to use non-suspending versions of account loading methods. - Refactor `AmberBunkerRequest` to include `isNostrConnectUri` flag. - Improve `EnabledPermissions` by using a `LazyColumn` for better performance. - Update string resources for signing policy explainers. --- .../nostrsigner/LocalPreferences.kt | 8 +- .../nostrsigner/models/AmberBunkerRequest.kt | 1 + .../service/EventNotificationConsumer.kt | 1 + .../nostrsigner/service/NostrConnectUtils.kt | 7 + .../greenart7c3/nostrsigner/ui/MainScreen.kt | 16 +- .../ui/components/AmberTopAppBar.kt | 313 +++++++++-------- .../components/BunkerConnectRequestScreen.kt | 324 ++++++++++++----- .../components/BunkerSingleEventHomeScreen.kt | 14 +- .../ui/components/EnabledPermissions.kt | 21 +- .../nostrsigner/ui/components/IconRow.kt | 3 +- .../components/IntentSingleEventHomeScreen.kt | 13 +- .../ui/components/LoginWithPubKey.kt | 325 ++++++++++++++---- .../ui/components/SignerConnectAppTab.kt | 29 ++ app/src/main/res/values/strings.xml | 4 +- 14 files changed, 748 insertions(+), 331 deletions(-) create mode 100644 app/src/main/java/com/greenart7c3/nostrsigner/ui/components/SignerConnectAppTab.kt diff --git a/app/src/main/java/com/greenart7c3/nostrsigner/LocalPreferences.kt b/app/src/main/java/com/greenart7c3/nostrsigner/LocalPreferences.kt index daf77595..6daf1653 100644 --- a/app/src/main/java/com/greenart7c3/nostrsigner/LocalPreferences.kt +++ b/app/src/main/java/com/greenart7c3/nostrsigner/LocalPreferences.kt @@ -186,7 +186,7 @@ object LocalPreferences { } } - suspend fun reloadApp() { + fun reloadApp() { val context = Amber.instance currentAccount = null savedAccounts = null @@ -429,14 +429,14 @@ object LocalPreferences { accountCache.put(account.npub, account) } - suspend fun loadFromEncryptedStorage(context: Context): Account? { + fun loadFromEncryptedStorage(context: Context): Account? { currentAccount(context)?.let { return loadFromEncryptedStorage(context, it) } return null } - suspend fun allAccounts(context: Context): List { + fun allAccounts(context: Context): List { val accountInfos = allSavedAccounts(context) return accountInfos.mapNotNull { loadFromEncryptedStorage(context, it.npub) @@ -493,7 +493,7 @@ object LocalPreferences { HttpClientManager.setDefaultProxyOnPort(port) } - suspend fun loadFromEncryptedStorage(context: Context, npub: String): Account? { + fun loadFromEncryptedStorage(context: Context, npub: String): Account? { if (accountCache.get(npub) != null) { return accountCache.get(npub) } diff --git a/app/src/main/java/com/greenart7c3/nostrsigner/models/AmberBunkerRequest.kt b/app/src/main/java/com/greenart7c3/nostrsigner/models/AmberBunkerRequest.kt index f107f685..3e198b64 100644 --- a/app/src/main/java/com/greenart7c3/nostrsigner/models/AmberBunkerRequest.kt +++ b/app/src/main/java/com/greenart7c3/nostrsigner/models/AmberBunkerRequest.kt @@ -25,4 +25,5 @@ data class AmberBunkerRequest( val checked: MutableState = mutableStateOf(true), val rememberType: MutableState = mutableStateOf(RememberType.NEVER), val encryptionType: EncryptionType, + val isNostrConnectUri: Boolean, ) diff --git a/app/src/main/java/com/greenart7c3/nostrsigner/service/EventNotificationConsumer.kt b/app/src/main/java/com/greenart7c3/nostrsigner/service/EventNotificationConsumer.kt index fd634c4c..eaf8aa9d 100644 --- a/app/src/main/java/com/greenart7c3/nostrsigner/service/EventNotificationConsumer.kt +++ b/app/src/main/java/com/greenart7c3/nostrsigner/service/EventNotificationConsumer.kt @@ -207,6 +207,7 @@ class EventNotificationConsumer(private val applicationContext: Context) { signedEvent = signedEvent, encryptedData = encryptedDataKind, encryptionType = encryptionType, + isNostrConnectUri = false, ) var amberEvent: AmberEvent? = null diff --git a/app/src/main/java/com/greenart7c3/nostrsigner/service/NostrConnectUtils.kt b/app/src/main/java/com/greenart7c3/nostrsigner/service/NostrConnectUtils.kt index 182c68ae..9fec11fc 100644 --- a/app/src/main/java/com/greenart7c3/nostrsigner/service/NostrConnectUtils.kt +++ b/app/src/main/java/com/greenart7c3/nostrsigner/service/NostrConnectUtils.kt @@ -13,6 +13,7 @@ import com.greenart7c3.nostrsigner.models.containsNip import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl import com.vitorpamplona.quartz.nip01Core.relay.normalizer.RelayUrlNormalizer import com.vitorpamplona.quartz.nip46RemoteSigner.BunkerRequestConnect +import java.util.UUID import kotlinx.coroutines.launch object NostrConnectUtils { @@ -22,6 +23,10 @@ object NostrConnectUtils { intent: Intent, account: Account, ) { + if (intent.extras?.getString("id") == null) { + intent.putExtra("id", UUID.randomUUID().toString().substring(0, 6)) + } + try { val data = intent.dataString.toString().replace("nostrconnect://", "") val split = data.split("?") @@ -107,6 +112,7 @@ object NostrConnectUtils { BunkerRequestUtils.addRequest( AmberBunkerRequest( BunkerRequestConnect( + id = intent.extras?.getString("id") ?: UUID.randomUUID().toString().substring(0, 6), remoteKey = pubKey, secret = "", permissions = if (permissions.isNotEmpty()) { @@ -124,6 +130,7 @@ object NostrConnectUtils { signedEvent = null, encryptedData = null, encryptionType = EncryptionType.NIP44, + isNostrConnectUri = true, ), ) } catch (e: Exception) { diff --git a/app/src/main/java/com/greenart7c3/nostrsigner/ui/MainScreen.kt b/app/src/main/java/com/greenart7c3/nostrsigner/ui/MainScreen.kt index ac617c25..3ab0bd29 100644 --- a/app/src/main/java/com/greenart7c3/nostrsigner/ui/MainScreen.kt +++ b/app/src/main/java/com/greenart7c3/nostrsigner/ui/MainScreen.kt @@ -340,14 +340,24 @@ fun MainScreen( Route.IncomingRequest.route, content = { val scrollState = rememberScrollState() - IncomingRequestScreen( - modifier = Modifier + val modifier = if (intents.isEmpty() || packageName == null || destinationRoute != Route.IncomingRequest.route) { + Modifier .fillMaxSize() .padding(padding) .verticalScrollbar(scrollState) .verticalScroll(scrollState) .padding(horizontal = verticalPadding) - .padding(top = verticalPadding * 1.5f), + .padding(top = verticalPadding * 1.5f) + } else { + Modifier + .fillMaxSize() + .padding(padding) + .verticalScrollbar(scrollState) + .verticalScroll(scrollState) + .padding(horizontal = verticalPadding) + } + IncomingRequestScreen( + modifier = modifier, intents = intents, bunkerRequests = bunkerRequests, packageName = packageName, diff --git a/app/src/main/java/com/greenart7c3/nostrsigner/ui/components/AmberTopAppBar.kt b/app/src/main/java/com/greenart7c3/nostrsigner/ui/components/AmberTopAppBar.kt index 560fa139..98b14f4f 100644 --- a/app/src/main/java/com/greenart7c3/nostrsigner/ui/components/AmberTopAppBar.kt +++ b/app/src/main/java/com/greenart7c3/nostrsigner/ui/components/AmberTopAppBar.kt @@ -1,9 +1,11 @@ package com.greenart7c3.nostrsigner.ui.components import android.content.Context +import android.graphics.drawable.Drawable import android.widget.Toast import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.size import androidx.compose.material.icons.Icons import androidx.compose.material.icons.outlined.Shield import androidx.compose.material3.CenterAlignedTopAppBar @@ -26,6 +28,7 @@ import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.vectorResource import androidx.compose.ui.unit.dp import androidx.lifecycle.Lifecycle @@ -41,6 +44,7 @@ import com.greenart7c3.nostrsigner.models.IntentData import com.greenart7c3.nostrsigner.service.toShortenHex import com.greenart7c3.nostrsigner.ui.navigation.Route import com.greenart7c3.nostrsigner.ui.navigation.routes +import com.vitorpamplona.quartz.nip46RemoteSigner.BunkerRequestConnect import java.util.Base64 import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers @@ -59,154 +63,183 @@ fun AmberTopAppBar( bunkerRequests: List, packageName: String?, ) { - if (destinationRoute != "login" && destinationRoute != "create" && destinationRoute != "loginPage") { - CenterAlignedTopAppBar( - actions = { - Row( - horizontalArrangement = Arrangement.spacedBy(4.dp), - ) { - TooltipBox( - positionProvider = TooltipDefaults.rememberTooltipPositionProvider(TooltipAnchorPosition.Above), - tooltip = { - PlainTooltip { - Text(text = context.getString(R.string.reconnect)) - } - }, - state = rememberTooltipState(), - ) { - IconButton( - onClick = { - Amber.instance.applicationIOScope.launch { - Amber.instance.reconnect() - } - }, - content = { - Row( - horizontalArrangement = Arrangement.spacedBy(4.dp), - verticalAlignment = Alignment.CenterVertically, - ) { - val relayStats = Amber.instance.client.relayStatusFlow().collectAsStateWithLifecycle() - Text("${relayStats.value.connected.size}/${relayStats.value.available.size}") - Icon( - imageVector = ImageVector.vectorResource(R.drawable.relays), - contentDescription = context.getString(R.string.reconnect), - tint = Color.Unspecified, - ) - } - }, - ) - } - - if (Amber.instance.settings.useProxy) { - var isProxyEnabled by remember { mutableStateOf(false) } - DisposableEffect(lifecycleOwner) { - val observer = LifecycleEventObserver { _, event -> - when (event) { - Lifecycle.Event.ON_START -> { - Amber.instance.applicationIOScope.launch { - isProxyEnabled = Amber.instance.isSocksProxyAlive("127.0.0.1", Amber.instance.settings.proxyPort) - } - } - - Lifecycle.Event.ON_RESUME -> { - Amber.instance.applicationIOScope.launch { - isProxyEnabled = Amber.instance.isSocksProxyAlive("127.0.0.1", Amber.instance.settings.proxyPort) - } - } - - else -> {} - } - } - lifecycleOwner.lifecycle.addObserver(observer) - - onDispose { lifecycleOwner.lifecycle.removeObserver(observer) } - } - - TooltipBox( - positionProvider = TooltipDefaults.rememberTooltipPositionProvider(TooltipAnchorPosition.Above), - tooltip = { - PlainTooltip { - Text(text = if (isProxyEnabled) context.getString(R.string.proxy_is_connected) else context.getString(R.string.proxy_is_not_working)) - } - }, - state = rememberTooltipState(), + if (intents.isEmpty() || packageName == null || destinationRoute != Route.IncomingRequest.route) { + if (destinationRoute != "login" && destinationRoute != "create" && destinationRoute != "loginPage") { + CenterAlignedTopAppBar( + actions = { + if (intents.isEmpty() || packageName == null || destinationRoute != Route.IncomingRequest.route) { + Row( + horizontalArrangement = Arrangement.spacedBy(4.dp), ) { - IconButton( - onClick = { - scope.launch { - if (isProxyEnabled) { - Toast.makeText(context, context.getString(R.string.proxy_is_connected), Toast.LENGTH_SHORT).show() - } else { - Toast.makeText(context, context.getString(R.string.proxy_is_not_working), Toast.LENGTH_SHORT).show() - } + TooltipBox( + positionProvider = TooltipDefaults.rememberTooltipPositionProvider(TooltipAnchorPosition.Above), + tooltip = { + PlainTooltip { + Text(text = context.getString(R.string.reconnect)) } }, - content = { - Icon( - Icons.Outlined.Shield, - context.getString(R.string.proxy), - tint = if (isProxyEnabled) Color.Green else Color.Red, + state = rememberTooltipState(), + ) { + IconButton( + onClick = { + Amber.instance.applicationIOScope.launch { + Amber.instance.reconnect() + } + }, + content = { + Row( + horizontalArrangement = Arrangement.spacedBy(4.dp), + verticalAlignment = Alignment.CenterVertically, + ) { + val relayStats = Amber.instance.client.relayStatusFlow().collectAsStateWithLifecycle() + Text("${relayStats.value.connected.size}/${relayStats.value.available.size}") + Icon( + imageVector = ImageVector.vectorResource(R.drawable.relays), + contentDescription = context.getString(R.string.reconnect), + tint = Color.Unspecified, + ) + } + }, + ) + } + + if (Amber.instance.settings.useProxy) { + var isProxyEnabled by remember { mutableStateOf(false) } + DisposableEffect(lifecycleOwner) { + val observer = LifecycleEventObserver { _, event -> + when (event) { + Lifecycle.Event.ON_START -> { + Amber.instance.applicationIOScope.launch { + isProxyEnabled = Amber.instance.isSocksProxyAlive("127.0.0.1", Amber.instance.settings.proxyPort) + } + } + + Lifecycle.Event.ON_RESUME -> { + Amber.instance.applicationIOScope.launch { + isProxyEnabled = Amber.instance.isSocksProxyAlive("127.0.0.1", Amber.instance.settings.proxyPort) + } + } + + else -> {} + } + } + lifecycleOwner.lifecycle.addObserver(observer) + + onDispose { lifecycleOwner.lifecycle.removeObserver(observer) } + } + + TooltipBox( + positionProvider = TooltipDefaults.rememberTooltipPositionProvider(TooltipAnchorPosition.Above), + tooltip = { + PlainTooltip { + Text(text = if (isProxyEnabled) context.getString(R.string.proxy_is_connected) else context.getString(R.string.proxy_is_not_working)) + } + }, + state = rememberTooltipState(), + ) { + IconButton( + onClick = { + scope.launch { + if (isProxyEnabled) { + Toast.makeText(context, context.getString(R.string.proxy_is_connected), Toast.LENGTH_SHORT).show() + } else { + Toast.makeText(context, context.getString(R.string.proxy_is_not_working), Toast.LENGTH_SHORT).show() + } + } + }, + content = { + Icon( + Icons.Outlined.Shield, + context.getString(R.string.proxy), + tint = if (isProxyEnabled) Color.Green else Color.Red, + ) + }, ) - }, - ) - } - } - } - }, - title = { - var title by remember { mutableStateOf(routes.find { it.route.startsWith(destinationRoute) }?.title ?: "") } - LaunchedEffect(destinationRoute) { - if (destinationRoute.startsWith("Permission/") || destinationRoute.startsWith("Activity/") || destinationRoute.startsWith("RelayLogScreen/") || destinationRoute.startsWith("qrcode/")) { - launch(Dispatchers.IO) { - navBackStackEntry?.arguments?.getString("content")?.let { - title = Route.QrCode.title - } - navBackStackEntry?.arguments?.getString("packageName")?.let { packageName -> - val application = Amber.instance.getDatabase(account.npub).dao().getByKey(packageName)?.application - title = if (destinationRoute.startsWith("Activity/")) { - "${application?.name?.ifBlank { application.key.toShortenHex() } ?: packageName} - ${routes.find { it.route.startsWith(destinationRoute) }?.title}" - } else { - application?.name?.ifBlank { application.key.toShortenHex() } ?: packageName } } - navBackStackEntry?.arguments?.getString("key")?.let { packageName -> - val application = Amber.instance.getDatabase(account.npub).dao().getByKey(packageName)?.application - title = if (destinationRoute.startsWith("Activity/")) { - "${application?.name?.ifBlank { application.key.toShortenHex() } ?: packageName} - ${routes.find { it.route.startsWith(destinationRoute) }?.title}" - } else { - application?.name?.ifBlank { application.key.toShortenHex() } ?: packageName - } - } - navBackStackEntry?.arguments?.getString("url")?.let { url -> - val localUrl = Base64.getDecoder().decode(url).toString(Charsets.UTF_8) - title = localUrl - } - } - } else { - launch(Dispatchers.IO) { - if (destinationRoute == Route.IncomingRequest.route && (intents.isNotEmpty() || bunkerRequests.isNotEmpty())) { - val key = if (bunkerRequests.isNotEmpty()) { - bunkerRequests.first().localKey - } else { - packageName - } - - val application = Amber.instance.getDatabase(account.npub).dao().getByKey(key ?: "")?.application - val titleTemp = application?.name?.ifBlank { application.key.toShortenHex() } ?: packageName ?: "" - title = if (titleTemp.isBlank()) { - routes.find { it.route == destinationRoute }?.title ?: "" - } else { - "$titleTemp - Request" - } - } else { - title = routes.find { it.route == destinationRoute }?.title ?: "" - } } } - } + }, + title = { + var title by remember { mutableStateOf(routes.find { it.route.startsWith(destinationRoute) }?.title ?: "") } + LaunchedEffect(destinationRoute) { + if (destinationRoute.startsWith("Permission/") || destinationRoute.startsWith("Activity/") || destinationRoute.startsWith("RelayLogScreen/") || destinationRoute.startsWith("qrcode/")) { + launch(Dispatchers.IO) { + navBackStackEntry?.arguments?.getString("content")?.let { + title = Route.QrCode.title + } + navBackStackEntry?.arguments?.getString("packageName")?.let { packageName -> + val application = Amber.instance.getDatabase(account.npub).dao().getByKey(packageName)?.application + title = if (destinationRoute.startsWith("Activity/")) { + "${application?.name?.ifBlank { application.key.toShortenHex() } ?: packageName} - ${routes.find { it.route.startsWith(destinationRoute) }?.title}" + } else { + application?.name?.ifBlank { application.key.toShortenHex() } ?: packageName + } + } + navBackStackEntry?.arguments?.getString("key")?.let { packageName -> + val application = Amber.instance.getDatabase(account.npub).dao().getByKey(packageName)?.application + title = if (destinationRoute.startsWith("Activity/")) { + "${application?.name?.ifBlank { application.key.toShortenHex() } ?: packageName} - ${routes.find { it.route.startsWith(destinationRoute) }?.title}" + } else { + application?.name?.ifBlank { application.key.toShortenHex() } ?: packageName + } + } + navBackStackEntry?.arguments?.getString("url")?.let { url -> + val localUrl = Base64.getDecoder().decode(url).toString(Charsets.UTF_8) + title = localUrl + } + } + } else { + launch(Dispatchers.IO) { + if (destinationRoute == Route.IncomingRequest.route && (bunkerRequests.isNotEmpty())) { + val request = bunkerRequests.first() + val key = bunkerRequests.first().localKey - Text(title) - }, - ) + var application = Amber.instance.getDatabase(account.npub).dao().getByKey(key)?.application + if (application == null && request.request is BunkerRequestConnect) { + val secret = request.request.secret + if (secret != null) { + application = Amber.instance.getDatabase(account.npub).dao().getByKey(secret)?.application + } + } + val titleTemp = application?.name?.ifBlank { request.name.ifBlank { application.key.toShortenHex() } } ?: request.name + title = titleTemp.ifBlank { + routes.find { it.route == destinationRoute }?.title ?: "" + } + } else { + title = routes.find { it.route == destinationRoute }?.title ?: "" + } + } + } + } + + Text(title) + }, + ) + } } } + +@Composable +fun rememberAppDisplayInfo(packageName: String): AppDisplayInfo { + val context = LocalContext.current + return remember(packageName) { + val appInfo = runCatching { + context.packageManager.getApplicationInfo(packageName, 0) + }.getOrNull() + + if (appInfo != null) { + AppDisplayInfo( + name = context.packageManager.getApplicationLabel(appInfo).toString(), + icon = context.packageManager.getApplicationIcon(appInfo), + ) + } else { + AppDisplayInfo(name = packageName, icon = null) + } + } +} + +data class AppDisplayInfo( + val name: String, + val icon: Drawable? = null, +) diff --git a/app/src/main/java/com/greenart7c3/nostrsigner/ui/components/BunkerConnectRequestScreen.kt b/app/src/main/java/com/greenart7c3/nostrsigner/ui/components/BunkerConnectRequestScreen.kt index 74ef0904..81cf6d7f 100644 --- a/app/src/main/java/com/greenart7c3/nostrsigner/ui/components/BunkerConnectRequestScreen.kt +++ b/app/src/main/java/com/greenart7c3/nostrsigner/ui/components/BunkerConnectRequestScreen.kt @@ -1,5 +1,7 @@ package com.greenart7c3.nostrsigner.ui.components +import androidx.compose.foundation.background +import androidx.compose.foundation.border import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box @@ -7,11 +9,26 @@ 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.foundation.selection.selectable +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.BottomAppBar import androidx.compose.material3.ButtonDefaults +import androidx.compose.material3.ElevatedButton +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.HorizontalDivider +import androidx.compose.material3.ListItem +import androidx.compose.material3.ListItemDefaults +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.ModalBottomSheet +import androidx.compose.material3.PrimaryTabRow +import androidx.compose.material3.Scaffold import androidx.compose.material3.Switch import androidx.compose.material3.Text +import androidx.compose.material3.rememberModalBottomSheetState import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableIntStateOf @@ -21,15 +38,23 @@ import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.res.stringResource +import androidx.compose.ui.res.vectorResource 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.lifecycle.compose.collectAsStateWithLifecycle +import com.greenart7c3.nostrsigner.Amber +import com.greenart7c3.nostrsigner.LocalPreferences import com.greenart7c3.nostrsigner.R import com.greenart7c3.nostrsigner.models.Account +import com.greenart7c3.nostrsigner.models.AmberBunkerRequest import com.greenart7c3.nostrsigner.models.Permission +import com.greenart7c3.nostrsigner.service.toShortenHex import com.greenart7c3.nostrsigner.ui.DeleteAfterType import com.greenart7c3.nostrsigner.ui.RememberType import com.greenart7c3.nostrsigner.ui.SettingsRow @@ -38,14 +63,15 @@ import com.greenart7c3.nostrsigner.ui.parseDeleteAfterType import kotlin.collections.forEach import kotlinx.collections.immutable.persistentListOf +@OptIn(ExperimentalMaterial3Api::class) @Composable fun BunkerConnectRequestScreen( modifier: Modifier, shouldCloseApp: Boolean, account: Account, - appName: String, + bunkerRequest: AmberBunkerRequest, permissions: List?, - onAccept: (List?, Int, Boolean?, RememberType, Long) -> Unit, + onAccept: (List?, Int, Boolean?, RememberType, Long, Account) -> Unit, onReject: (RememberType) -> Unit, ) { val localPermissions = remember { @@ -55,117 +81,245 @@ fun BunkerConnectRequestScreen( } snapshot } - + var selectedTabIndex by remember { mutableIntStateOf(0) } + var selectedOption by remember { mutableIntStateOf(account.signPolicy) } + val accounts = remember { + val snapshot = mutableStateListOf() + if (bunkerRequest.isNostrConnectUri) { + LocalPreferences.allAccounts(Amber.instance).forEach { + snapshot.add(it) + } + } else { + snapshot.add(account) + } + snapshot + } + var selectedAccountIndex by remember { mutableIntStateOf(accounts.indexOf(account)) } var rememberType by remember { mutableStateOf(RememberType.NEVER) } + val deleteAfterItems = + persistentListOf( + TitleExplainer(stringResource(DeleteAfterType.NEVER.resourceId)), + TitleExplainer(stringResource(DeleteAfterType.FIVE_MINUTES.resourceId)), + TitleExplainer(stringResource(DeleteAfterType.TEN_MINUTES.resourceId)), + TitleExplainer(stringResource(DeleteAfterType.ONE_HOUR.resourceId)), + TitleExplainer(stringResource(DeleteAfterType.ONE_DAY.resourceId)), + TitleExplainer(stringResource(DeleteAfterType.ONE_WEEK.resourceId)), + ) + var deleteAfterIndex by remember { mutableIntStateOf(DeleteAfterType.NEVER.screenCode) } + var closeApp by remember { mutableStateOf(shouldCloseApp) } Column( modifier, ) { - ProfilePicture(account) - - Text( - modifier = Modifier.fillMaxWidth(), - text = appName, - fontSize = 24.sp, - fontWeight = FontWeight.Bold, - textAlign = TextAlign.Center, - ) - - Text( - stringResource(R.string.would_like_your_permission_to_read_your_public_key_and_sign_events_on_your_behalf), - ) - - var selectedOption by remember { mutableIntStateOf(account.signPolicy) } - val deleteAfterItems = - persistentListOf( - TitleExplainer(stringResource(DeleteAfterType.NEVER.resourceId)), - TitleExplainer(stringResource(DeleteAfterType.FIVE_MINUTES.resourceId)), - TitleExplainer(stringResource(DeleteAfterType.TEN_MINUTES.resourceId)), - TitleExplainer(stringResource(DeleteAfterType.ONE_HOUR.resourceId)), - TitleExplainer(stringResource(DeleteAfterType.ONE_DAY.resourceId)), - TitleExplainer(stringResource(DeleteAfterType.ONE_WEEK.resourceId)), + PrimaryTabRow( + selectedTabIndex = selectedTabIndex, + containerColor = Color.Transparent, + contentColor = MaterialTheme.colorScheme.onSurface, + indicator = { + Box( + modifier = Modifier + .tabIndicatorOffset(selectedTabIndex) + .height(6.dp) + .padding(horizontal = 16.dp) + .padding(bottom = 2.dp) + .clip(CircleShape) + .background(color = MaterialTheme.colorScheme.primary), + ) + }, + divider = { + HorizontalDivider( + thickness = 0.5.dp, + color = MaterialTheme.colorScheme.outline, + ) + }, + ) { + SignerConnectAppTab( + text = stringResource(id = R.string.login).uppercase(), + selected = selectedTabIndex == 0, + onClick = { + selectedTabIndex = 0 + }, ) - var deleteAfterIndex by remember { mutableIntStateOf(DeleteAfterType.NEVER.screenCode) } - Text( - text = stringResource(R.string.handle_application_permissions), - ) + SignerConnectAppTab( + text = stringResource(id = R.string.permissions).uppercase(), + selected = selectedTabIndex == 0, + onClick = { + selectedTabIndex = 1 + }, + ) + } Spacer(modifier = Modifier.size(8.dp)) - var closeApp by remember { mutableStateOf(shouldCloseApp) } - Row( - horizontalArrangement = Arrangement.SpaceBetween, - verticalAlignment = Alignment.CenterVertically, - modifier = Modifier - .padding(vertical = 8.dp) - .clickable { - closeApp = !closeApp - }, - ) { - Text( - modifier = Modifier.weight(1f), - text = stringResource(R.string.close_application), - ) - Switch( - checked = closeApp, - onCheckedChange = { - closeApp = it - }, - ) - } + if (selectedTabIndex == 0) { + accounts.forEachIndexed { index, acc -> + ListItem( + modifier = Modifier + .border( + width = 1.dp, + color = if (selectedAccountIndex == index) { + MaterialTheme.colorScheme.primary + } else { + Color.Transparent + }, + shape = RoundedCornerShape(8.dp), + ) + .padding(4.dp) + .selectable( + selected = selectedAccountIndex == index, + onClick = { + selectedAccountIndex = index + }, + ), + colors = ListItemDefaults.colors( + containerColor = MaterialTheme.colorScheme.background, - Box( - Modifier.padding(4.dp), - ) { - SettingsRow( - R.string.delete_after, - null, - deleteAfterItems, - deleteAfterIndex, + ), + leadingContent = { + ProfilePictureIcon( + account = acc, + ) + }, + headlineContent = { + val name by acc.name.collectAsStateWithLifecycle() + Text( + name.ifBlank { acc.npub.toShortenHex() }, + fontWeight = FontWeight.Bold, + fontSize = 18.sp, + textAlign = TextAlign.Center, + ) + }, + ) + } + } else { + var showModal by remember { mutableStateOf(false) } + val sheetState = rememberModalBottomSheetState( + skipPartiallyExpanded = true, + ) + + Row( + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier + .padding(vertical = 8.dp) + .clickable { + closeApp = !closeApp + }, ) { - deleteAfterIndex = it + Text( + modifier = Modifier.weight(1f), + text = stringResource(R.string.close_application), + ) + Switch( + checked = closeApp, + onCheckedChange = { + closeApp = it + }, + ) + } + + Box( + Modifier.padding(4.dp), + ) { + SettingsRow( + R.string.delete_after, + null, + deleteAfterItems, + deleteAfterIndex, + ) { + deleteAfterIndex = it + } + } + + ChooseSignPolicy( + selectedOption = selectedOption, + onSelected = { + selectedOption = it + }, + ) + + if (selectedOption == 1 && localPermissions.isNotEmpty()) { + Box( + Modifier.fillMaxWidth(), + contentAlignment = Alignment.Center, + ) { + ElevatedButton( + colors = ButtonDefaults.buttonColors().copy( + contentColor = Color.Black, + ), + shape = RoundedCornerShape(20), + content = { + Text(stringResource(R.string.permissions)) + }, + onClick = { + showModal = true + }, + ) + } + if (showModal) { + ModalBottomSheet( + sheetState = sheetState, + onDismissRequest = { + showModal = false + }, + ) { + Scaffold( + bottomBar = { + BottomAppBar { + IconRow( + center = true, + title = stringResource(R.string.go_back), + icon = ImageVector.vectorResource(R.drawable.back), + onClick = { + showModal = false + }, + tint = MaterialTheme.colorScheme.onSurfaceVariant, + ) + } + }, + ) { + EnabledPermissions( + Modifier.padding(it), + localPermissions, + ) + } + } + } } } - ChooseSignPolicy( - selectedOption = selectedOption, - onSelected = { - selectedOption = it - }, - ) - Spacer(modifier = Modifier.weight(1f)) - Column( + Row( Modifier .fillMaxWidth() .padding(10.dp), - Arrangement.Center, - Alignment.CenterHorizontally, + Arrangement.spacedBy(8.dp), + Alignment.CenterVertically, ) { - if (selectedOption == 1) { - EnabledPermissions(localPermissions) - } - AmberButton( - modifier = Modifier.padding(vertical = 20.dp), - onClick = { - val deleteAfter = deleteAfterToSeconds(parseDeleteAfterType(deleteAfterIndex)) - onAccept(localPermissions, selectedOption, closeApp, rememberType, deleteAfter) - }, - text = stringResource(R.string.grant_permissions), - ) - - AmberButton( - modifier = Modifier.padding(vertical = 20.dp), + modifier = Modifier + .padding(vertical = 20.dp) + .weight(1f), onClick = { onReject(rememberType) }, colors = ButtonDefaults.buttonColors( containerColor = Color(0xFFFF6B00), ), - text = stringResource(R.string.reject), + text = stringResource(R.string.cancel), + ) + + AmberButton( + modifier = Modifier + .padding(vertical = 20.dp) + .weight(1f), + onClick = { + val deleteAfter = deleteAfterToSeconds(parseDeleteAfterType(deleteAfterIndex)) + onAccept(localPermissions, selectedOption, closeApp, rememberType, deleteAfter, accounts[selectedAccountIndex]) + }, + text = stringResource(R.string.connect), ) } } diff --git a/app/src/main/java/com/greenart7c3/nostrsigner/ui/components/BunkerSingleEventHomeScreen.kt b/app/src/main/java/com/greenart7c3/nostrsigner/ui/components/BunkerSingleEventHomeScreen.kt index 97647ae9..45e5924d 100644 --- a/app/src/main/java/com/greenart7c3/nostrsigner/ui/components/BunkerSingleEventHomeScreen.kt +++ b/app/src/main/java/com/greenart7c3/nostrsigner/ui/components/BunkerSingleEventHomeScreen.kt @@ -148,6 +148,7 @@ fun BunkerSingleEventHomeScreen( var localRememberType by remember { mutableStateOf(RememberType.NEVER) } var existingAppKey by remember { mutableStateOf("") } var localDeleteAfter by remember { mutableLongStateOf(0L) } + var selectedAccount by remember { mutableStateOf(account) } if (showExistingAppDialog) { AlertDialog( @@ -169,7 +170,7 @@ fun BunkerSingleEventHomeScreen( BunkerRequestUtils.sendResult( oldKey = existingAppKey, context = context, - account = account, + account = selectedAccount, key = key, response = result, bunkerRequest = bunkerRequest, @@ -192,7 +193,7 @@ fun BunkerSingleEventHomeScreen( BunkerRequestUtils.sendResult( context = context, - account = account, + account = selectedAccount, key = key, response = result, bunkerRequest = bunkerRequest, @@ -222,7 +223,7 @@ fun BunkerSingleEventHomeScreen( modifier = modifier, shouldCloseApp = applicationEntity?.application?.closeApplication ?: bunkerRequest.closeApplication, account = account, - appName = appName, + bunkerRequest = bunkerRequest, permissions = bunkerRequest.request.permissions?.split(",")?.map { val split = it.split(":") if (split.size > 1) { @@ -231,21 +232,22 @@ fun BunkerSingleEventHomeScreen( Permission(split[0].trim(), null) } }, - onAccept = { permissions, signPolicy, closeApplication, rememberType, deleteAfter -> + onAccept = { permissions, signPolicy, closeApplication, rememberType, deleteAfter, acc -> val result = bunkerRequest.nostrConnectSecret.ifBlank { "ack" } localPermissions = permissions localSignPolicy = signPolicy localCloseApplication = closeApplication localRememberType = rememberType + selectedAccount = acc if (bunkerRequest.name.isNotBlank()) { Amber.instance.applicationIOScope.launch { - val existingApp = Amber.instance.getDatabase(account.npub).dao().getByName(bunkerRequest.name) + val existingApp = Amber.instance.getDatabase(acc.npub).dao().getByName(bunkerRequest.name) if (existingApp == null) { BunkerRequestUtils.sendResult( context = context, - account = account, + account = acc, key = key, response = result, bunkerRequest = bunkerRequest, diff --git a/app/src/main/java/com/greenart7c3/nostrsigner/ui/components/EnabledPermissions.kt b/app/src/main/java/com/greenart7c3/nostrsigner/ui/components/EnabledPermissions.kt index 606e36ee..cb06f0bb 100644 --- a/app/src/main/java/com/greenart7c3/nostrsigner/ui/components/EnabledPermissions.kt +++ b/app/src/main/java/com/greenart7c3/nostrsigner/ui/components/EnabledPermissions.kt @@ -6,6 +6,8 @@ import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.itemsIndexed import androidx.compose.material3.Card import androidx.compose.material3.CardDefaults import androidx.compose.material3.Checkbox @@ -23,13 +25,18 @@ import com.greenart7c3.nostrsigner.models.Permission @Composable fun EnabledPermissions( + modifier: Modifier = Modifier, localPermissions: List, ) { val enabledPermissions = localPermissions.map { remember { mutableStateOf(it.checked) } } - if (localPermissions.isNotEmpty()) { - localPermissions.forEachIndexed { index, permission -> + LazyColumn( + modifier + .fillMaxWidth() + .padding(8.dp), + ) { + itemsIndexed(localPermissions) { index, item -> Card( modifier = Modifier .fillMaxWidth() @@ -44,20 +51,20 @@ fun EnabledPermissions( verticalAlignment = Alignment.CenterVertically, modifier = Modifier .clickable { - permission.checked = !permission.checked - enabledPermissions[index].value = permission.checked + item.checked = !item.checked + enabledPermissions[index].value = item.checked }, ) { Checkbox( checked = enabledPermissions[index].value, onCheckedChange = { _ -> - permission.checked = !permission.checked - enabledPermissions[index].value = permission.checked + item.checked = !item.checked + enabledPermissions[index].value = item.checked }, ) Text( modifier = Modifier.weight(1f), - text = permission.toLocalizedString(LocalContext.current), + text = item.toLocalizedString(LocalContext.current), ) } } diff --git a/app/src/main/java/com/greenart7c3/nostrsigner/ui/components/IconRow.kt b/app/src/main/java/com/greenart7c3/nostrsigner/ui/components/IconRow.kt index b46fca5f..307a9697 100644 --- a/app/src/main/java/com/greenart7c3/nostrsigner/ui/components/IconRow.kt +++ b/app/src/main/java/com/greenart7c3/nostrsigner/ui/components/IconRow.kt @@ -22,6 +22,7 @@ import androidx.compose.ui.unit.sp @OptIn(ExperimentalFoundationApi::class) @Composable fun IconRow( + modifier: Modifier = Modifier, center: Boolean = false, title: String, icon: ImageVector, @@ -31,7 +32,7 @@ fun IconRow( onLongClick: (() -> Unit)? = null, ) { Row( - modifier = Modifier + modifier = modifier .fillMaxWidth() .combinedClickable( onClick = onClick, diff --git a/app/src/main/java/com/greenart7c3/nostrsigner/ui/components/IntentSingleEventHomeScreen.kt b/app/src/main/java/com/greenart7c3/nostrsigner/ui/components/IntentSingleEventHomeScreen.kt index 83920f6d..2ba204d8 100644 --- a/app/src/main/java/com/greenart7c3/nostrsigner/ui/components/IntentSingleEventHomeScreen.kt +++ b/app/src/main/java/com/greenart7c3/nostrsigner/ui/components/IntentSingleEventHomeScreen.kt @@ -70,24 +70,21 @@ fun IntentSingleEventHomeScreen( when (intentData.type) { SignerType.GET_PUBLIC_KEY -> { LoginWithPubKey( - shouldCloseApp = applicationEntity?.application?.closeApplication != false, + packageName = packageName, modifier = modifier, account = account, - packageName = packageName, - appName = appName, - applicationName = applicationName, permissions = intentData.permissions, - onAccept = { permissions, signPolicy, closeApplication, rememberType -> + onAccept = { permissions, signPolicy, closeApplication, rememberType, acc -> val result = if (packageName == null) { - account.hexKey + acc.hexKey } else { - account.npub + acc.npub } IntentUtils.sendResult( context, packageName, - account, + acc, key, clipboardManager, result, diff --git a/app/src/main/java/com/greenart7c3/nostrsigner/ui/components/LoginWithPubKey.kt b/app/src/main/java/com/greenart7c3/nostrsigner/ui/components/LoginWithPubKey.kt index df4a5cd1..8b453ade 100644 --- a/app/src/main/java/com/greenart7c3/nostrsigner/ui/components/LoginWithPubKey.kt +++ b/app/src/main/java/com/greenart7c3/nostrsigner/ui/components/LoginWithPubKey.kt @@ -1,8 +1,10 @@ package com.greenart7c3.nostrsigner.ui.components +import androidx.compose.foundation.Image +import androidx.compose.foundation.background import androidx.compose.foundation.border -import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer @@ -11,15 +13,26 @@ import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.width +import androidx.compose.foundation.selection.selectable import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material.icons.Icons import androidx.compose.material.icons.outlined.Person +import androidx.compose.material3.BottomAppBar import androidx.compose.material3.ButtonDefaults +import androidx.compose.material3.ElevatedButton +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.Icon -import androidx.compose.material3.Switch +import androidx.compose.material3.ListItem +import androidx.compose.material3.ListItemDefaults +import androidx.compose.material3.MaterialTheme +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 import androidx.compose.runtime.mutableIntStateOf @@ -31,15 +44,22 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.asImageBitmap +import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.res.stringResource +import androidx.compose.ui.res.vectorResource 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 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 +import com.greenart7c3.nostrsigner.LocalPreferences import com.greenart7c3.nostrsigner.R import com.greenart7c3.nostrsigner.models.Account import com.greenart7c3.nostrsigner.models.Permission @@ -94,16 +114,44 @@ fun ProfilePicture(account: Account) { } } +@Composable +fun ProfilePictureIcon(account: Account) { + val profileUrl by account.picture.collectAsStateWithLifecycle() + if (profileUrl.isNotBlank() && !BuildFlavorChecker.isOfflineFlavor()) { + AsyncImage( + profileUrl, + Route.Accounts.route, + Modifier + .clip( + RoundedCornerShape(50), + ) + .height(40.dp) + .width(40.dp), + ) + } else { + Icon( + Icons.Outlined.Person, + Route.Accounts.route, + modifier = Modifier + .border( + 2.dp, + Color.fromHex(account.hexKey.slice(0..5)), + CircleShape, + ) + .height(40.dp) + .width(40.dp), + ) + } +} + +@OptIn(ExperimentalMaterial3Api::class) @Composable fun LoginWithPubKey( - shouldCloseApp: Boolean, modifier: Modifier, account: Account, packageName: String?, - appName: String, - applicationName: String?, permissions: List?, - onAccept: (List?, Int, Boolean?, RememberType) -> Unit, + onAccept: (List?, Int, Boolean?, RememberType, Account) -> Unit, onReject: (RememberType) -> Unit, ) { val localPermissions = remember { @@ -115,105 +163,232 @@ fun LoginWithPubKey( } var rememberType by remember { mutableStateOf(RememberType.NEVER) } + var selectedTabIndex by remember { mutableIntStateOf(0) } + var selectedOption by remember { mutableIntStateOf(account.signPolicy) } + val accounts = remember { + val snapshot = mutableStateListOf() + LocalPreferences.allAccounts(Amber.instance).forEach { + snapshot.add(it) + } + snapshot + } + var selectedAccountIndex by remember { mutableIntStateOf(accounts.indexOf(account)) } 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)) + 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, + ) + } } - Text( - modifier = Modifier.fillMaxWidth(), - text = applicationName ?: appName, - fontSize = 24.sp, - fontWeight = FontWeight.Bold, - textAlign = TextAlign.Center, - ) + PrimaryTabRow( + selectedTabIndex = selectedTabIndex, + containerColor = Color.Transparent, + contentColor = MaterialTheme.colorScheme.onSurface, + indicator = { + Box( + modifier = Modifier + .tabIndicatorOffset(selectedTabIndex) + .height(6.dp) + .padding(horizontal = 16.dp) + .padding(bottom = 2.dp) + .clip(CircleShape) + .background(color = MaterialTheme.colorScheme.primary), + ) + }, + divider = { + HorizontalDivider( + thickness = 0.5.dp, + color = MaterialTheme.colorScheme.outline, + ) + }, + ) { + SignerConnectAppTab( + text = stringResource(id = R.string.login).uppercase(), + selected = selectedTabIndex == 0, + onClick = { + selectedTabIndex = 0 + }, + ) - Text( - stringResource(R.string.would_like_your_permission_to_read_your_public_key_and_sign_events_on_your_behalf), - ) - - var selectedOption by remember { mutableIntStateOf(account.signPolicy) } - - Text( - text = stringResource(R.string.handle_application_permissions), - ) + SignerConnectAppTab( + text = stringResource(id = R.string.permissions).uppercase(), + selected = selectedTabIndex == 0, + onClick = { + selectedTabIndex = 1 + }, + ) + } Spacer(modifier = Modifier.size(8.dp)) - var closeApp by remember { mutableStateOf(shouldCloseApp) } - if (packageName == null) { - Row( - horizontalArrangement = Arrangement.SpaceBetween, - verticalAlignment = Alignment.CenterVertically, - modifier = Modifier - .padding(vertical = 8.dp) - .clickable { - closeApp = !closeApp + if (selectedTabIndex == 0) { + accounts.forEachIndexed { index, acc -> + ListItem( + modifier = Modifier + .border( + width = 1.dp, + color = if (selectedAccountIndex == index) { + MaterialTheme.colorScheme.primary + } else { + Color.Transparent + }, + shape = RoundedCornerShape(8.dp), + ) + .padding(4.dp) + .selectable( + selected = selectedAccountIndex == index, + onClick = { + selectedAccountIndex = index + }, + ), + colors = ListItemDefaults.colors( + containerColor = MaterialTheme.colorScheme.background, + + ), + leadingContent = { + ProfilePictureIcon( + account = acc, + ) }, - ) { - Text( - modifier = Modifier.weight(1f), - text = stringResource(R.string.close_application), - ) - Switch( - checked = closeApp, - onCheckedChange = { - closeApp = it + headlineContent = { + val name by acc.name.collectAsStateWithLifecycle() + Text( + name.ifBlank { acc.npub.toShortenHex() }, + fontWeight = FontWeight.Bold, + fontSize = 18.sp, + textAlign = TextAlign.Center, + ) }, ) } + } else { + var showModal by remember { mutableStateOf(false) } + val sheetState = rememberModalBottomSheetState( + skipPartiallyExpanded = true, + ) + + ChooseSignPolicy( + selectedOption = selectedOption, + onSelected = { + selectedOption = it + }, + ) + + if (selectedOption == 1 && localPermissions.isNotEmpty()) { + Box( + Modifier.fillMaxWidth(), + contentAlignment = Alignment.Center, + ) { + ElevatedButton( + colors = ButtonDefaults.buttonColors().copy( + contentColor = Color.Black, + ), + shape = RoundedCornerShape(20), + content = { + Text(stringResource(R.string.permissions)) + }, + onClick = { + showModal = true + }, + ) + } + if (showModal) { + ModalBottomSheet( + sheetState = sheetState, + onDismissRequest = { + showModal = false + }, + ) { + Scaffold( + bottomBar = { + BottomAppBar { + IconRow( + center = true, + title = stringResource(R.string.go_back), + icon = ImageVector.vectorResource(R.drawable.back), + onClick = { + showModal = false + }, + tint = MaterialTheme.colorScheme.onSurfaceVariant, + ) + } + }, + ) { + EnabledPermissions( + Modifier.padding(it), + localPermissions, + ) + } + } + } + } } - ChooseSignPolicy( - selectedOption = selectedOption, - onSelected = { - selectedOption = it - }, - ) - Spacer(modifier = Modifier.weight(1f)) - Column( + Row( Modifier .fillMaxWidth() .padding(10.dp), - Arrangement.Center, - Alignment.CenterHorizontally, + Arrangement.spacedBy(8.dp), + Alignment.CenterVertically, ) { - if (selectedOption == 1) { - EnabledPermissions(localPermissions) - } - AmberButton( - modifier = Modifier.padding(vertical = 20.dp), - onClick = { - onAccept(localPermissions, selectedOption, closeApp, rememberType) - }, - text = stringResource(R.string.grant_permissions), - ) - - AmberButton( - modifier = Modifier.padding(vertical = 20.dp), + modifier = Modifier + .padding(vertical = 20.dp) + .weight(1f), onClick = { onReject(rememberType) }, colors = ButtonDefaults.buttonColors( containerColor = Color(0xFFFF6B00), ), - text = stringResource(R.string.reject), + text = stringResource(R.string.cancel), + ) + + AmberButton( + modifier = Modifier + .padding(vertical = 20.dp) + .weight(1f), + onClick = { + onAccept(localPermissions, selectedOption, true, rememberType, accounts[selectedAccountIndex]) + }, + text = stringResource(R.string.connect), ) } } diff --git a/app/src/main/java/com/greenart7c3/nostrsigner/ui/components/SignerConnectAppTab.kt b/app/src/main/java/com/greenart7c3/nostrsigner/ui/components/SignerConnectAppTab.kt new file mode 100644 index 00000000..d17b51ae --- /dev/null +++ b/app/src/main/java/com/greenart7c3/nostrsigner/ui/components/SignerConnectAppTab.kt @@ -0,0 +1,29 @@ +package com.greenart7c3.nostrsigner.ui.components + +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.Tab +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.dp + +@Composable +fun SignerConnectAppTab( + text: String, + selected: Boolean, + onClick: () -> Unit, + modifier: Modifier = Modifier, +) { + Tab( + modifier = modifier.padding(horizontal = 16.dp), + selected = selected, + onClick = onClick, + text = { + Text( + text = text, + fontWeight = if (selected) FontWeight.SemiBold else FontWeight.Normal, + ) + }, + ) +} diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 1dfb4287..31dbcd29 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -347,7 +347,7 @@ Next Add your key Approve basic actions - Recommended for most people. This will minimize the number of interruptions during your app usage. + Auto-approve most common requests. Manually approve each app Recommended for privacy-minded people who would like control over each app. Choosing this will prompt you set a preference every time you try a new app. Select a signing policy @@ -359,7 +359,7 @@ Sign policy How to handle this application permissions? Manually approve each permission - Recommended for privacy-minded people who would like control over each permission. Choosing this will prompt you set a preference every time you receive a new permission. + Ask me to approve each request. I fully trust this application Sign automatically every request DM relays