refactor: use the Tor onion for the Tor toggle, shield only for sandbox access

Per review, drop the shield-as-Tor (and the security sheet that hosted it):
Tor is now the app's standard ic_tor onion (TorToggleButton, lit when on Tor,
dimmed on the open web), matching how Tor appears on relays and in settings.
The shield is reserved for the nsite/napplet tab, where it genuinely means
"what it can access".

- New shared TorToggleButton (onion).
- EmbeddedTabTopBar takes a `leading` slot: the web tab puts the Tor onion
  there, the napplet/nsite tab puts its access shield.
- BrowserHostActivity (full-screen) uses the same onion toggle instead of the
  shield/sheet. WebAppSecurityDialog and its strings are removed.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MgMpRcWj6y82LxLiwcuzmN
This commit is contained in:
Claude
2026-06-23 19:24:24 +00:00
parent 2d3dd21695
commit 5c37f57a31
6 changed files with 81 additions and 86 deletions
@@ -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))
@@ -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
@@ -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)
},
@@ -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,
)
}
}
@@ -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()))
-3
View File
@@ -681,9 +681,6 @@
<string name="favorite_app_access_title">What this app can access</string>
<string name="favorite_app_access_static">Runs sandboxed with no special access to your account.</string>
<string name="favorite_app_access_show">What it can access</string>
<string name="embedded_tab_security">Security &amp; privacy</string>
<string name="favorite_web_security_body">This site runs sandboxed in a keyless process and signs in per site (NIP-07).</string>
<string name="favorite_web_tor">Load over Tor</string>
<string name="favorite_app_network_tor">Loads over Tor.</string>
<string name="favorite_app_network_open">Loads over the open web.</string>
<string name="favorite_app_unavailable">This app isn\'t loaded yet. Open it from its card, or try again in a moment.</string>