diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/browser/BrowserHostActivity.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/browser/BrowserHostActivity.kt index 18b1f49a01..9418e503d1 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/browser/BrowserHostActivity.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/browser/BrowserHostActivity.kt @@ -49,6 +49,7 @@ import com.vitorpamplona.amethyst.Amethyst import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.commons.icons.symbols.Icon import com.vitorpamplona.amethyst.commons.icons.symbols.MaterialSymbols +import com.vitorpamplona.amethyst.ui.screen.loggedIn.embed.TorToggleButton import com.vitorpamplona.amethyst.ui.theme.AmethystTheme /** @@ -108,7 +109,6 @@ private fun BrowserHostScreen( val proxyAvailable = remember { Amethyst.instance.torManager.activePortOrNull.value != null } var torOn by remember { mutableStateOf(proxyAvailable) } - var showSecurity by remember { mutableStateOf(false) } val controller = rememberBrowserController(startUrl = startUrl) { url, back -> @@ -119,19 +119,6 @@ private fun BrowserHostScreen( // In-page back first; once there's no page history, the system back closes the activity. BackHandler(enabled = canGoBack) { controller.back() } - if (showSecurity) { - WebAppSecurityDialog( - host = hostOf(currentUrl), - proxyAvailable = proxyAvailable, - torOn = torOn, - onToggleTor = { - torOn = !torOn - controller.setTor(torOn) - }, - onDismiss = { showSecurity = false }, - ) - } - Scaffold( topBar = { TopAppBar( @@ -148,9 +135,11 @@ private fun BrowserHostScreen( ) }, actions = { - // Same shield = "Security & privacy" (Tor lives inside) as the embedded web tab. - IconButton(onClick = { showSecurity = true }) { - Icon(MaterialSymbols.Security, contentDescription = stringResource(R.string.embedded_tab_security)) + if (proxyAvailable) { + TorToggleButton(torOn) { + torOn = !torOn + controller.setTor(torOn) + } } IconButton(onClick = { controller.reload() }) { Icon(MaterialSymbols.Refresh, contentDescription = stringResource(R.string.browser_reload)) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/browser/FavoriteWebAppScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/browser/FavoriteWebAppScreen.kt index f300113f54..af9f89f5f0 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/browser/FavoriteWebAppScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/browser/FavoriteWebAppScreen.kt @@ -25,18 +25,11 @@ import android.os.Build import androidx.activity.compose.BackHandler import androidx.annotation.RequiresApi 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.fillMaxSize -import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding -import androidx.compose.material3.AlertDialog import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Scaffold -import androidx.compose.material3.Switch import androidx.compose.material3.Text -import androidx.compose.material3.TextButton import androidx.compose.runtime.Composable import androidx.compose.runtime.DisposableEffect import androidx.compose.runtime.SideEffect @@ -50,7 +43,6 @@ import androidx.compose.ui.layout.boundsInWindow import androidx.compose.ui.layout.onGloballyPositioned import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.stringResource -import androidx.compose.ui.unit.dp import com.vitorpamplona.amethyst.Amethyst import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.favorites.FavoriteAppLauncher @@ -61,13 +53,14 @@ import com.vitorpamplona.amethyst.ui.navigation.routes.Route import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.amethyst.ui.screen.loggedIn.embed.EmbeddedTabHost import com.vitorpamplona.amethyst.ui.screen.loggedIn.embed.EmbeddedTabTopBar +import com.vitorpamplona.amethyst.ui.screen.loggedIn.embed.TorToggleButton /** * A pinned web client rendered as an **in-app tab**. The embedded `:napplet` browser surface is drawn * by the persistent [EmbeddedTabHost]/[com.vitorpamplona.amethyst.ui.screen.loggedIn.embed.EmbeddedTabLayer] * layer, which keeps the session warm across tab swaps. This screen owns only the chrome — the shared - * [EmbeddedTabTopBar] (sandbox shield + reload + pop-out), identical to the nsite/napplet tab — plus a - * security & privacy sheet where Tor lives. Deliberately no editable address bar. + * [EmbeddedTabTopBar] with the Tor onion as its leading affordance, plus reload + pop-out. Deliberately + * no editable address bar. * * Only bottom-row favorites stay warm; if this URL isn't a bottom-bar favorite, its session is evicted * (restarted) when the screen leaves. Requires API 30+ for the cross-process surface. @@ -103,7 +96,6 @@ private fun EmbeddedFavoriteTab( var currentUrl by remember { mutableStateOf(url) } var canGoBack by remember { mutableStateOf(false) } - var showSecurity by remember { mutableStateOf(false) } val proxyAvailable = remember { Amethyst.instance.torManager.activePortOrNull.value != null } var torOn by remember { mutableStateOf(proxyAvailable) } @@ -136,24 +128,18 @@ private fun EmbeddedFavoriteTab( BackHandler(enabled = canGoBack) { controller.back() } - if (showSecurity) { - WebAppSecurityDialog( - host = hostLabel(currentUrl), - proxyAvailable = proxyAvailable, - torOn = torOn, - onToggleTor = { - torOn = !torOn - controller.setTor(torOn) - }, - onDismiss = { showSecurity = false }, - ) - } - Scaffold( topBar = { EmbeddedTabTopBar( title = hostLabel(currentUrl), - onSecurity = { showSecurity = true }, + leading = { + if (proxyAvailable) { + TorToggleButton(torOn) { + torOn = !torOn + controller.setTor(torOn) + } + } + }, onReload = { controller.reload() }, onPopOut = { FavoriteAppLauncher.launchUrl(context, url) }, ) @@ -172,34 +158,5 @@ private fun EmbeddedFavoriteTab( } } -@Composable -internal fun WebAppSecurityDialog( - host: String, - proxyAvailable: Boolean, - torOn: Boolean, - onToggleTor: () -> Unit, - onDismiss: () -> Unit, -) { - AlertDialog( - onDismissRequest = onDismiss, - title = { Text(host) }, - text = { - Column { - Text(stringResource(R.string.favorite_web_security_body)) - if (proxyAvailable) { - Spacer(Modifier.height(12.dp)) - Row(verticalAlignment = Alignment.CenterVertically) { - Text(stringResource(R.string.favorite_web_tor), modifier = Modifier.weight(1f)) - Switch(checked = torOn, onCheckedChange = { onToggleTor() }) - } - } - } - }, - confirmButton = { - TextButton(onClick = onDismiss) { Text(stringResource(android.R.string.ok)) } - }, - ) -} - /** The host of [url] for the tab title, falling back to the raw string. */ internal fun hostLabel(url: String): String = runCatching { Uri.parse(url).host }.getOrNull()?.takeIf { it.isNotBlank() } ?: url diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/embed/EmbeddedTabTopBar.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/embed/EmbeddedTabTopBar.kt index ebf3423fe8..818f91a3ee 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/embed/EmbeddedTabTopBar.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/embed/EmbeddedTabTopBar.kt @@ -33,27 +33,23 @@ import com.vitorpamplona.amethyst.commons.icons.symbols.MaterialSymbols /** * The shared chrome for an embedded web tab — the browser/web-app tab and the nsite/napplet tab use the - * exact same bar so they read as one consistent surface: a **sandbox shield** (security & privacy: what - * the app can access, and its network/Tor routing), the title, **reload**, and **open-in-own-window**. + * same bar so they read as one consistent surface: a **leading** affordance specific to the surface + * (the napplet/nsite tab puts its sandbox-access **shield** here; the web-app tab puts the Tor onion), + * the title, **reload**, and **open-in-own-window**. * - * Tor deliberately lives *inside* the shield's sheet rather than as its own top-bar icon, so the shield - * is never confused with a Tor toggle (the onion `ic_tor` is the Tor mark elsewhere; the shield is the - * sandbox/security mark). + * Tor uses the app's onion (`ic_tor`) rather than the shield, so the shield always means "sandbox + * access" and is never mistaken for a Tor toggle. */ @OptIn(ExperimentalMaterial3Api::class) @Composable fun EmbeddedTabTopBar( title: String, - onSecurity: () -> Unit, + leading: @Composable () -> Unit, onReload: () -> Unit, onPopOut: () -> Unit, ) { TopAppBar( - navigationIcon = { - IconButton(onClick = onSecurity) { - Icon(MaterialSymbols.Security, contentDescription = stringResource(R.string.embedded_tab_security)) - } - }, + navigationIcon = leading, title = { Text(text = title, maxLines = 1, overflow = TextOverflow.Ellipsis) }, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/embed/TorToggleButton.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/embed/TorToggleButton.kt new file mode 100644 index 0000000000..7493335a6c --- /dev/null +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/embed/TorToggleButton.kt @@ -0,0 +1,48 @@ +/* + * Copyright (c) 2025 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.amethyst.ui.screen.loggedIn.embed + +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.MaterialTheme +import androidx.compose.runtime.Composable +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import com.vitorpamplona.amethyst.R + +/** + * The Tor routing toggle for an embedded web surface, drawn with the app's standard Tor onion + * (`ic_tor`) — the same mark used on relays and in settings — so it's never confused with the sandbox + * shield. Lit (primary) when routing over Tor, dimmed when loading over the open web. + */ +@Composable +fun TorToggleButton( + torOn: Boolean, + onToggle: () -> Unit, +) { + IconButton(onClick = onToggle) { + Icon( + painter = painterResource(R.drawable.ic_tor), + contentDescription = stringResource(if (torOn) R.string.browser_tor_on else R.string.browser_tor_off), + tint = if (torOn) MaterialTheme.colorScheme.primary else MaterialTheme.colorScheme.onSurfaceVariant, + ) + } +} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/favorites/FavoriteNappletScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/favorites/FavoriteNappletScreen.kt index b5cf17d82c..0aa659f115 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/favorites/FavoriteNappletScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/favorites/FavoriteNappletScreen.kt @@ -29,6 +29,7 @@ import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.padding import androidx.compose.material3.AlertDialog import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.IconButton import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Scaffold import androidx.compose.material3.Text @@ -54,6 +55,8 @@ import androidx.lifecycle.LifecycleEventObserver import androidx.lifecycle.compose.LocalLifecycleOwner import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.commons.favorites.FavoriteApp +import com.vitorpamplona.amethyst.commons.icons.symbols.Icon +import com.vitorpamplona.amethyst.commons.icons.symbols.MaterialSymbols import com.vitorpamplona.amethyst.favorites.FavoriteAppLauncher import com.vitorpamplona.amethyst.napplethost.NappletEmbedContract import com.vitorpamplona.amethyst.napplethost.NappletHostContract @@ -172,7 +175,12 @@ private fun EmbeddedNappletTab( topBar = { EmbeddedTabTopBar( title = title, - onSecurity = { showAccess = true }, + leading = { + // The shield is the sandbox-access affordance here (capabilities + network). + IconButton(onClick = { showAccess = true }) { + Icon(MaterialSymbols.Security, contentDescription = stringResource(R.string.favorite_app_access_show)) + } + }, onReload = { controller.reload() }, onPopOut = { FavoriteAppLauncher.launch(context, FavoriteApp.NostrApp(coordinate, title, System.currentTimeMillis())) diff --git a/amethyst/src/main/res/values/strings.xml b/amethyst/src/main/res/values/strings.xml index ca9a8ae2c8..97925adaec 100644 --- a/amethyst/src/main/res/values/strings.xml +++ b/amethyst/src/main/res/values/strings.xml @@ -681,9 +681,6 @@ What this app can access Runs sandboxed with no special access to your account. What it can access - Security & privacy - This site runs sandboxed in a keyless process and signs in per site (NIP-07). - Load over Tor Loads over Tor. Loads over the open web. This app isn\'t loaded yet. Open it from its card, or try again in a moment.