From 9a064984a58c509b9f915e68c39c6f87dbd42b06 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 25 Jun 2026 15:39:37 +0000 Subject: [PATCH 1/2] refactor: rename web-app surfaces to distinguish them from the native App Store The in-app "app" surfaces had colliding, sometimes inaccurate names. NIP-89 native apps showed as "Apps" while the in-app favorites showed as "Favorite apps", and the single-app host screens were named `FavoriteWebAppScreen` / `FavoriteNappletScreen` even though they open any url/coordinate (favorited or not) and the "Napplet" one also renders nSites. Establish one taxonomy: - Native app store -> "App Store" (NIP-89), in-app Nostr web client -> "Web app", plus the existing nApplet / nSite. "Favorite" is now only the star toggle and the pinned grid, not a screen. - Code axis: `WebApp` (url-based) and `NostrApp` (coordinate-based nSite/nApplet). The cross-process sandbox infra (`napplet/`, `nappletHost/`) keeps "Napplet". Renames: - Routes `FavoriteWebApp`/`FavoriteNostrApp` -> `WebApp`/`NostrApp` - Screens `FavoriteWebAppScreen`/`FavoriteNappletScreen` -> `WebAppScreen`/`NostrAppScreen` - Controllers `EmbeddedBrowserController`/`EmbeddedNappletController` -> `EmbeddedWebAppController`/`EmbeddedNostrAppController` - Factory `acquireBrowser`/`acquireNapplet` -> `acquireWebApp`/`acquireNostrApp` - Model `FavoriteApp.WebUrl` -> `FavoriteApp.WebApp`; `WebUrlNetworkRegistry` -> `WebAppNetworkRegistry` - Strings: `software_apps`/`route_software_apps` "Apps" -> "App Store"; `favorite_apps_empty` reworded to name web app / nApplet / nSite Persistence is untouched: favorite id prefixes ("url:"/"nostr:"), DataStore names ("favorite_apps"/"weburl_network"), and serialized type tags ("url"/ "nostr") are all kept stable. Taxonomy documented in amethyst/plans/2026-06-25-web-app-naming.md. Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_01HrxLCMcQADUPnm8Sj9ZJ63 --- amethyst/plans/2026-06-25-web-app-naming.md | 63 ++++++++++++++++++ .../com/vitorpamplona/amethyst/Amethyst.kt | Bin 8135 -> 8135 bytes .../amethyst/favorites/FavoriteAppLauncher.kt | 8 +-- .../favorites/FavoriteAppsRegistry.kt | 4 +- .../amethyst/napplet/NappletBrokerService.kt | 6 +- ...rkRegistry.kt => WebAppNetworkRegistry.kt} | 2 +- .../amethyst/ui/navigation/AppNavigation.kt | 8 +-- .../ui/navigation/bottombars/AppBottomBar.kt | 8 +-- .../navigation/bottombars/BottomBarEntry.kt | 2 +- .../amethyst/ui/navigation/routes/Routes.kt | 4 +- .../screen/loggedIn/browser/BrowserScreen.kt | 6 +- ...troller.kt => EmbeddedWebAppController.kt} | 2 +- ...avoriteWebAppScreen.kt => WebAppScreen.kt} | 25 +++---- .../ui/screen/loggedIn/embed/ConsoleBridge.kt | 2 +- .../embed/EmbeddedSurfaceController.kt | 4 +- .../loggedIn/embed/EmbeddedTabFactory.kt | 42 ++++++------ .../loggedIn/embed/EmbeddedTabPreloader.kt | 6 +- ...oller.kt => EmbeddedNostrAppController.kt} | 4 +- .../loggedIn/favorites/FavoriteAppsScreen.kt | 2 +- ...riteNappletScreen.kt => NostrAppScreen.kt} | 15 +++-- amethyst/src/main/res/values/strings.xml | 8 +-- .../amethyst/commons/favorites/FavoriteApp.kt | 14 ++-- 22 files changed, 151 insertions(+), 84 deletions(-) create mode 100644 amethyst/plans/2026-06-25-web-app-naming.md rename amethyst/src/main/java/com/vitorpamplona/amethyst/napplet/{WebUrlNetworkRegistry.kt => WebAppNetworkRegistry.kt} (99%) rename amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/browser/{EmbeddedBrowserController.kt => EmbeddedWebAppController.kt} (99%) rename amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/browser/{FavoriteWebAppScreen.kt => WebAppScreen.kt} (90%) rename amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/favorites/{EmbeddedNappletController.kt => EmbeddedNostrAppController.kt} (99%) rename amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/favorites/{FavoriteNappletScreen.kt => NostrAppScreen.kt} (95%) diff --git a/amethyst/plans/2026-06-25-web-app-naming.md b/amethyst/plans/2026-06-25-web-app-naming.md new file mode 100644 index 0000000000..6920651436 --- /dev/null +++ b/amethyst/plans/2026-06-25-web-app-naming.md @@ -0,0 +1,63 @@ +# Web-app naming overhaul (favorites / browser / app surfaces) + +## Problem + +The in-app "app" surfaces had colliding, sometimes inaccurate names: + +- `software_apps` (NIP-89 native Android apps, an install-from-a-store flow) showed as + **"Apps"** — colliding with the in-app favorites, which showed as **"Favorite apps"**. +- The **host screens** that render a single web client / nSite / nApplet were named + `FavoriteWebAppScreen` / `FavoriteNappletScreen`. But they open *any* url/coordinate, + favorited or not — "Favorite" described how they happened to be reached (a pinned + bottom-bar tab), not what they are. And `FavoriteNappletScreen` also renders **nSites** + (website-mode), so "Napplet" was narrower than reality. +- Three vocabularies for two model cases: model `WebUrl`/`NostrApp`, route + `FavoriteWebApp`/`FavoriteNostrApp`, screen `FavoriteWebApp`/`FavoriteNapplet`. + +## Taxonomy (decided with maintainer) + +User-facing terms, now distinct: + +| Concept | User-facing | What it is | +|---|---|---| +| Native app store | **App Store** | NIP-89 native Android apps you install off-device | +| Nostr web client | **Web app** | an `https://` client that runs in-app (WebView) | +| nApplet | **nApplet** | NIP-5D sandboxed JS app | +| nSite | **nSite** | NIP-5A static website | +| Pinned set | **Favorite** | a cross-cutting attribute (the star), *not* a screen | + +Code axis (favorites / route / screen / embedded-controller layer): **`WebApp`** (url-based, +no nostr identity) and **`NostrApp`** (coordinate-based nSite *or* nApplet). The cross-process +sandbox infra (`napplet/`, `nappletHost/`, `NappletHostService`, `NappletEmbedContract`) +keeps **"Napplet"** — that process genuinely is the napplet host (it serves nSites in +website-mode too, but the host *is* the napplet runtime). + +"Favorite" is reserved for the **grid of pinned apps** (`FavoriteAppsScreen` / +`Route.FavoriteApps`) and the star toggle — the only things that are actually about favorites. + +## Renames + +Routes: `FavoriteWebApp(url)` → `WebApp(url)`; `FavoriteNostrApp(coordinate)` → `NostrApp(coordinate)`. +Screens: `FavoriteWebAppScreen` → `WebAppScreen`; `FavoriteNappletScreen` → `NostrAppScreen`. +Controllers: `EmbeddedBrowserController` → `EmbeddedWebAppController`; +`EmbeddedNappletController` → `EmbeddedNostrAppController`. +Factory: `acquireBrowser`/`browserId` → `acquireWebApp`/`webAppId`; +`acquireNapplet`/`nappletId` → `acquireNostrApp`/`nostrAppId`. +Model: `FavoriteApp.WebUrl` → `FavoriteApp.WebApp`. Registry: `WebUrlNetworkRegistry` → +`WebAppNetworkRegistry`. + +Strings: `software_apps` "Apps" → "App Store"; `favorite_apps_empty` reworded to name +nApplet/nSite. + +## Stable (do NOT change — persistence / wire compat) + +- Favorite `id` prefixes `"url:"` / `"nostr:"` (persisted dedup + bottom-bar keys). +- DataStore names `"favorite_apps"`, `"weburl_network"`; serialized type tags `"url"` / `"nostr"`. +- `napplet/` + `nappletHost/` sandbox infra names and IPC contracts. + +## Follow-ups (not in this pass) + +- Move `NostrAppScreen` + `EmbeddedNostrAppController` out of the `ui/...favorites/` + package (they are no longer favorites-specific) into a host package alongside the web side. +- Recent nApplets / nSites (parallel to the browser's recent web apps), surfaced on the + discovery screens. diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/Amethyst.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/Amethyst.kt index ca1de8490bc6e107dda7332ee9e32700d47d41e1..e5531f56b81d0e0619f8915b008629c6905c5f55 100644 GIT binary patch delta 25 dcmX?Zf82h launchUrl(context, app.url) + is FavoriteApp.WebApp -> launchUrl(context, app.url) is FavoriteApp.NostrApp -> launchNostrApp(context, app.coordinate) } } @@ -78,7 +78,7 @@ object FavoriteAppLauncher { preferTor: Boolean = false, ) { val proxyPort = Amethyst.instance.torManager.activePortOrNull.value ?: -1 - val useTor = proxyPort > 0 && (preferTor || WebUrlNetworkRegistry.useTor(url)) + val useTor = proxyPort > 0 && (preferTor || WebAppNetworkRegistry.useTor(url)) val themeType = Amethyst.instance.uiPrefs.value.theme.value val theme = when (themeType) { diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/favorites/FavoriteAppsRegistry.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/favorites/FavoriteAppsRegistry.kt index e9bb04861b..2b63b36309 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/favorites/FavoriteAppsRegistry.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/favorites/FavoriteAppsRegistry.kt @@ -128,7 +128,7 @@ object FavoriteAppsRegistry { list.map { when (it) { is FavoriteApp.NostrApp -> Entry(TYPE_NOSTR, it.coordinate, it.label, it.addedAt, it.iconUrl) - is FavoriteApp.WebUrl -> Entry(TYPE_URL, it.url, it.label, it.addedAt, it.iconUrl) + is FavoriteApp.WebApp -> Entry(TYPE_URL, it.url, it.label, it.addedAt, it.iconUrl) } }, ) @@ -138,7 +138,7 @@ object FavoriteAppsRegistry { JsonMapper.fromJson>(json).mapNotNull { entry -> when (entry.type) { TYPE_NOSTR -> FavoriteApp.NostrApp(entry.ref, entry.label, entry.addedAt, entry.iconUrl) - TYPE_URL -> FavoriteApp.WebUrl(entry.ref, entry.label, entry.addedAt, entry.iconUrl) + TYPE_URL -> FavoriteApp.WebApp(entry.ref, entry.label, entry.addedAt, entry.iconUrl) else -> null } } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/napplet/NappletBrokerService.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/napplet/NappletBrokerService.kt index 8f2dba934d..d27bd89e3d 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/napplet/NappletBrokerService.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/napplet/NappletBrokerService.kt @@ -187,7 +187,7 @@ class NappletBrokerService : Service() { if (FavoriteAppsRegistry.isFavorite(id)) { FavoriteAppsRegistry.remove(id) } else { - FavoriteAppsRegistry.add(FavoriteApp.WebUrl(url, label, System.currentTimeMillis())) + FavoriteAppsRegistry.add(FavoriteApp.WebApp(url, label, System.currentTimeMillis())) } return true } @@ -196,8 +196,8 @@ class NappletBrokerService : Service() { if (msg.what == NappletIpc.MSG_SET_WEB_TOR) { val data = msg.data ?: return true val host = data.getString(NappletIpc.KEY_WEB_HOST)?.takeIf { it.isNotBlank() } ?: return true - WebUrlNetworkRegistry.init(applicationContext) - WebUrlNetworkRegistry.set(host, data.getBoolean(NappletIpc.KEY_NETWORK_USE_TOR, true)) + WebAppNetworkRegistry.init(applicationContext) + WebAppNetworkRegistry.set(host, data.getBoolean(NappletIpc.KEY_NETWORK_USE_TOR, true)) return true } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/napplet/WebUrlNetworkRegistry.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/napplet/WebAppNetworkRegistry.kt similarity index 99% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/napplet/WebUrlNetworkRegistry.kt rename to amethyst/src/main/java/com/vitorpamplona/amethyst/napplet/WebAppNetworkRegistry.kt index adbef1d225..185b780e99 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/napplet/WebUrlNetworkRegistry.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/napplet/WebAppNetworkRegistry.kt @@ -48,7 +48,7 @@ private val Context.webUrlNetworkDataStore by preferencesDataStore(name = "webur * Lives only in the **main process** (where the browser chrome runs); an in-memory map is authoritative * for the session with write-through persistence. */ -object WebUrlNetworkRegistry { +object WebAppNetworkRegistry { private const val OPEN_WEB = "OPEN" private const val TOR = "TOR" diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/AppNavigation.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/AppNavigation.kt index 550cf9c3a7..ddd9699b8c 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/AppNavigation.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/AppNavigation.kt @@ -83,7 +83,7 @@ import com.vitorpamplona.amethyst.ui.screen.loggedIn.bookmarkgroups.membershipMa import com.vitorpamplona.amethyst.ui.screen.loggedIn.bookmarkgroups.membershipManagement.PostBookmarkListManagementScreen import com.vitorpamplona.amethyst.ui.screen.loggedIn.bookmarkgroups.old.OldBookmarkListScreen import com.vitorpamplona.amethyst.ui.screen.loggedIn.browser.BrowserScreen -import com.vitorpamplona.amethyst.ui.screen.loggedIn.browser.FavoriteWebAppScreen +import com.vitorpamplona.amethyst.ui.screen.loggedIn.browser.WebAppScreen import com.vitorpamplona.amethyst.ui.screen.loggedIn.calendars.CalendarCollectionsScreen import com.vitorpamplona.amethyst.ui.screen.loggedIn.calendars.CalendarReminderSettingsScreen import com.vitorpamplona.amethyst.ui.screen.loggedIn.calendars.CalendarsScreen @@ -127,7 +127,7 @@ import com.vitorpamplona.amethyst.ui.screen.loggedIn.emojipacks.list.metadata.Em import com.vitorpamplona.amethyst.ui.screen.loggedIn.emojipacks.membershipManagement.EmojiPackSelectionScreen import com.vitorpamplona.amethyst.ui.screen.loggedIn.emojipacks.membershipManagement.MyEmojiListScreen import com.vitorpamplona.amethyst.ui.screen.loggedIn.favorites.FavoriteAppsScreen -import com.vitorpamplona.amethyst.ui.screen.loggedIn.favorites.FavoriteNappletScreen +import com.vitorpamplona.amethyst.ui.screen.loggedIn.favorites.NostrAppScreen import com.vitorpamplona.amethyst.ui.screen.loggedIn.followPacks.feed.FollowPackFeedScreen import com.vitorpamplona.amethyst.ui.screen.loggedIn.followPacks.list.FollowPacksScreen import com.vitorpamplona.amethyst.ui.screen.loggedIn.geohash.GeoHashPostScreen @@ -321,8 +321,8 @@ fun BuildNavigation( composableFromEnd { NsitesScreen(accountViewModel, nav) } composableFromEnd { BrowserScreen(accountViewModel, nav) } composableFromEnd { FavoriteAppsScreen(accountViewModel, nav) } - composableFromEndArgs { FavoriteWebAppScreen(it.url, accountViewModel, nav) } - composableFromEndArgs { FavoriteNappletScreen(it.coordinate, accountViewModel, nav) } + composableFromEndArgs { WebAppScreen(it.url, accountViewModel, nav) } + composableFromEndArgs { NostrAppScreen(it.coordinate, accountViewModel, nav) } composableFromEnd { NappletPermissionsScreen(accountViewModel, nav) } composableFromEndArgs { SoftwareAppDetailScreen(Address(it.kind, it.pubKeyHex, it.dTag), accountViewModel, nav) } composableFromEnd { CalendarsScreen(accountViewModel, nav) } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/bottombars/AppBottomBar.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/bottombars/AppBottomBar.kt index 9cc9ffee7c..d6b853ced4 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/bottombars/AppBottomBar.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/bottombars/AppBottomBar.kt @@ -90,7 +90,7 @@ fun AppBottomBar( } // Favorite entries in the unified list resolve to a live favorite for their icon/label and to an - // embedded-tab route. Both kinds embed in-process (WebUrl → browser surface, NostrApp → napplet + // embedded-tab route. Both kinds embed in-process (WebApp → browser surface, NostrApp → napplet // surface), so such a tab swaps in place rather than launching an activity from the bottom row. val favorites by FavoriteAppsRegistry.favorites.collectAsStateWithLifecycle() @@ -143,12 +143,12 @@ private fun RenderBottomMenu( val fav = favoritesById[entry.favoriteId] ?: return@forEach val destination = when (fav) { - is FavoriteApp.WebUrl -> Route.FavoriteWebApp(fav.url) - is FavoriteApp.NostrApp -> Route.FavoriteNostrApp(fav.coordinate) + is FavoriteApp.WebApp -> Route.WebApp(fav.url) + is FavoriteApp.NostrApp -> Route.NostrApp(fav.coordinate) } val iconModel = remember(fav, iconKeys) { - (fav as? FavoriteApp.WebUrl)?.let { OmniboxInput.hostOf(it.url)?.let(BrowserIconRegistry::iconModelFor) } + (fav as? FavoriteApp.WebApp)?.let { OmniboxInput.hostOf(it.url)?.let(BrowserIconRegistry::iconModelFor) } } FavoriteNavItem(destination == selectedRoute, fav, iconModel, destination, nav) } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/bottombars/BottomBarEntry.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/bottombars/BottomBarEntry.kt index cb81df0878..3911885096 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/bottombars/BottomBarEntry.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/bottombars/BottomBarEntry.kt @@ -33,7 +33,7 @@ import kotlinx.serialization.Serializable * - [Favorite] points at a [FavoriteApp][com.vitorpamplona.amethyst.commons.favorites.FavoriteApp] by * its stable id (which already encodes the route's parameters — the `url` or addressable * `coordinate`); the bar resolves it to a live favorite for its icon/label and to - * `Route.FavoriteWebApp` / `Route.FavoriteNostrApp` for navigation. + * `Route.WebApp` / `Route.NostrApp` for navigation. */ @Serializable sealed interface BottomBarEntry { diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/routes/Routes.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/routes/Routes.kt index c6ce226dd0..dd6691ba9f 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/routes/Routes.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/routes/Routes.kt @@ -95,11 +95,11 @@ sealed class Route { @Serializable object FavoriteApps : Route() - @Serializable data class FavoriteWebApp( + @Serializable data class WebApp( val url: String, ) : Route() - @Serializable data class FavoriteNostrApp( + @Serializable data class NostrApp( val coordinate: String, ) : Route() diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/browser/BrowserScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/browser/BrowserScreen.kt index ca28551678..f201f7a95c 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/browser/BrowserScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/browser/BrowserScreen.kt @@ -130,7 +130,7 @@ private fun BrowserLauncher( val candidates = remember(apps, history) { buildList { - apps.forEach { if (it is FavoriteApp.WebUrl) add(OmniboxSuggestions.Candidate(it.url, it.label, isFavorite = true)) } + apps.forEach { if (it is FavoriteApp.WebApp) add(OmniboxSuggestions.Candidate(it.url, it.label, isFavorite = true)) } history.forEach { add( OmniboxSuggestions.Candidate( @@ -214,7 +214,7 @@ private fun BrowserLauncher( ) } else -> { - val favoriteUrls = remember(apps) { apps.filterIsInstance().mapTo(HashSet()) { it.url } } + val favoriteUrls = remember(apps) { apps.filterIsInstance().mapTo(HashSet()) { it.url } } BrowserHome( apps = apps, history = history, @@ -229,7 +229,7 @@ private fun BrowserLauncher( FavoriteAppsRegistry.remove(id) } else { FavoriteAppsRegistry.add( - FavoriteApp.WebUrl(entry.url, entry.title.ifBlank { entry.host }, System.currentTimeMillis()), + FavoriteApp.WebApp(entry.url, entry.title.ifBlank { entry.host }, System.currentTimeMillis()), ) } }, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/browser/EmbeddedBrowserController.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/browser/EmbeddedWebAppController.kt similarity index 99% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/browser/EmbeddedBrowserController.kt rename to amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/browser/EmbeddedWebAppController.kt index 76d8fe78fb..305ba16e0d 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/browser/EmbeddedBrowserController.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/browser/EmbeddedWebAppController.kt @@ -53,7 +53,7 @@ import java.util.concurrent.atomic.AtomicLong * that drive the trusted, main-process address bar. */ @RequiresApi(Build.VERSION_CODES.R) -class EmbeddedBrowserController( +class EmbeddedWebAppController( private val appContext: Context, private val proxyPort: Int, private val initialUseTor: Boolean, 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/WebAppScreen.kt similarity index 90% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/browser/FavoriteWebAppScreen.kt rename to amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/browser/WebAppScreen.kt index 4020781557..a3491f8831 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/WebAppScreen.kt @@ -50,7 +50,7 @@ import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.commons.favorites.FavoriteApp import com.vitorpamplona.amethyst.favorites.FavoriteAppLauncher import com.vitorpamplona.amethyst.favorites.FavoriteAppsRegistry -import com.vitorpamplona.amethyst.napplet.WebUrlNetworkRegistry +import com.vitorpamplona.amethyst.napplet.WebAppNetworkRegistry import com.vitorpamplona.amethyst.ui.navigation.bottombars.AppBottomBar import com.vitorpamplona.amethyst.ui.navigation.bottombars.favoriteIds import com.vitorpamplona.amethyst.ui.navigation.navs.INav @@ -61,7 +61,8 @@ import com.vitorpamplona.amethyst.ui.screen.loggedIn.embed.EmbeddedTabFactory import com.vitorpamplona.amethyst.ui.screen.loggedIn.embed.EmbeddedTabHost /** - * A pinned web client rendered as an **in-app tab**. The embedded `:napplet` browser surface is drawn + * A **Web app** (a Nostr web client, reached by [url]) rendered as an **in-app tab** — for *any* URL, + * favorited or not; favoriting is just the star toggle in the chrome. 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 — it publishes * the controls as an [EmbeddedTabChrome] and the layer draws a top pull-down sheet over the (z-below) @@ -71,13 +72,13 @@ import com.vitorpamplona.amethyst.ui.screen.loggedIn.embed.EmbeddedTabHost * (restarted) when the screen leaves. Requires API 30+ for the cross-process surface. */ @Composable -fun FavoriteWebAppScreen( +fun WebAppScreen( url: String, accountViewModel: AccountViewModel, nav: INav, ) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { - EmbeddedFavoriteTab(url, accountViewModel, nav) + EmbeddedWebAppTab(url, accountViewModel, nav) } else { Box(Modifier.fillMaxSize(), contentAlignment = Alignment.Center) { Text( @@ -90,13 +91,13 @@ fun FavoriteWebAppScreen( @RequiresApi(Build.VERSION_CODES.R) @Composable -private fun EmbeddedFavoriteTab( +private fun EmbeddedWebAppTab( url: String, accountViewModel: AccountViewModel, nav: INav, ) { val context = LocalContext.current - // Matches FavoriteApp.WebUrl.id, so warm-keep membership lines up with the bottom-bar favorites. + // Matches FavoriteApp.WebApp.id, so warm-keep membership lines up with the bottom-bar favorites. val id = "url:$url" var currentUrl by remember { mutableStateOf(url) } @@ -105,16 +106,16 @@ private fun EmbeddedFavoriteTab( val proxyAvailable = remember { Amethyst.instance.torManager.activePortOrNull.value != null } // Start from this site's remembered Tor choice (some sites' servers reject Tor exits, so the user // can opt one out and it must stick). Only meaningful when Tor is actually available. - var torOn by remember { mutableStateOf(proxyAvailable && WebUrlNetworkRegistry.useTor(url)) } + var torOn by remember { mutableStateOf(proxyAvailable && WebAppNetworkRegistry.useTor(url)) } val apps by FavoriteAppsRegistry.favorites.collectAsStateWithLifecycle() - val isFavorite = remember(apps, currentUrl) { apps.any { it is FavoriteApp.WebUrl && it.url == currentUrl } } + val isFavorite = remember(apps, currentUrl) { apps.any { it is FavoriteApp.WebApp && it.url == currentUrl } } val backgroundColor = MaterialTheme.colorScheme.background.toArgb() val controller = remember(id) { - EmbeddedTabFactory.acquireBrowser(context, url, backgroundColor) + EmbeddedTabFactory.acquireWebApp(context, url, backgroundColor) } // Keep the URL/back callback fresh (cheap, needs the latest closure). @@ -137,7 +138,7 @@ private fun EmbeddedFavoriteTab( onToggleTor = { torOn = !torOn controller.setTor(torOn) - WebUrlNetworkRegistry.set(url, torOn) + WebAppNetworkRegistry.set(url, torOn) }, isFavorite = isFavorite, onFavorite = { @@ -145,7 +146,7 @@ private fun EmbeddedFavoriteTab( if (FavoriteAppsRegistry.isFavorite(favId)) { FavoriteAppsRegistry.remove(favId) } else { - FavoriteAppsRegistry.add(FavoriteApp.WebUrl(currentUrl, hostLabel(currentUrl), System.currentTimeMillis())) + FavoriteAppsRegistry.add(FavoriteApp.WebApp(currentUrl, hostLabel(currentUrl), System.currentTimeMillis())) } }, ) @@ -169,7 +170,7 @@ private fun EmbeddedFavoriteTab( Scaffold( bottomBar = { - AppBottomBar(Route.FavoriteWebApp(url), nav, accountViewModel) { route -> nav.navBottomBar(route) } + AppBottomBar(Route.WebApp(url), nav, accountViewModel) { route -> nav.navBottomBar(route) } }, ) { padding -> // Reserve the full content area; the warm surface, its top sheet, and the loading/error overlay diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/embed/ConsoleBridge.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/embed/ConsoleBridge.kt index b60121c5b6..83875219dc 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/embed/ConsoleBridge.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/embed/ConsoleBridge.kt @@ -25,7 +25,7 @@ import androidx.compose.runtime.snapshots.SnapshotStateList /** * A surface controller that exposes JavaScript console output captured from the embedded WebView. * The [consoleLogs] list is Compose snapshot state so [BottomConsoleSheet] recomposes as messages - * arrive. Implemented by [com.vitorpamplona.amethyst.ui.screen.loggedIn.browser.EmbeddedBrowserController]. + * arrive. Implemented by [com.vitorpamplona.amethyst.ui.screen.loggedIn.browser.EmbeddedWebAppController]. */ interface ConsoleBridge { val consoleLogs: SnapshotStateList diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/embed/EmbeddedSurfaceController.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/embed/EmbeddedSurfaceController.kt index 1256e50bc7..bab6cdd01c 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/embed/EmbeddedSurfaceController.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/embed/EmbeddedSurfaceController.kt @@ -26,8 +26,8 @@ import androidx.privacysandbox.ui.client.view.SandboxedSdkView /** * The minimal surface controls [EmbeddedTabHost] needs to keep a session warm regardless of whether - * it's a browser ([com.vitorpamplona.amethyst.ui.screen.loggedIn.browser.EmbeddedBrowserController]) - * or an nsite/napplet ([com.vitorpamplona.amethyst.ui.screen.loggedIn.favorites.EmbeddedNappletController]). + * it's a browser ([com.vitorpamplona.amethyst.ui.screen.loggedIn.browser.EmbeddedWebAppController]) + * or an nsite/napplet ([com.vitorpamplona.amethyst.ui.screen.loggedIn.favorites.EmbeddedNostrAppController]). * * Warm-keep works by keeping the session's [SandboxedSdkView] **attached** to the window the whole * time (just moved off-screen when not active) — the privacy-sandbox view only closes its session on diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/embed/EmbeddedTabFactory.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/embed/EmbeddedTabFactory.kt index 3f677a1c9c..fab2842f38 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/embed/EmbeddedTabFactory.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/embed/EmbeddedTabFactory.kt @@ -30,10 +30,10 @@ import com.vitorpamplona.amethyst.commons.favorites.FavoriteApp import com.vitorpamplona.amethyst.commons.tor.TorType import com.vitorpamplona.amethyst.favorites.FavoriteAppLauncher import com.vitorpamplona.amethyst.model.ThemeType -import com.vitorpamplona.amethyst.napplet.WebUrlNetworkRegistry +import com.vitorpamplona.amethyst.napplet.WebAppNetworkRegistry import com.vitorpamplona.amethyst.napplethost.NappletHostContract -import com.vitorpamplona.amethyst.ui.screen.loggedIn.browser.EmbeddedBrowserController -import com.vitorpamplona.amethyst.ui.screen.loggedIn.favorites.EmbeddedNappletController +import com.vitorpamplona.amethyst.ui.screen.loggedIn.browser.EmbeddedWebAppController +import com.vitorpamplona.amethyst.ui.screen.loggedIn.favorites.EmbeddedNostrAppController /** * The single place that builds a warm embedded-tab controller, so the favorite screens and the @@ -46,19 +46,19 @@ import com.vitorpamplona.amethyst.ui.screen.loggedIn.favorites.EmbeddedNappletCo */ @RequiresApi(Build.VERSION_CODES.R) object EmbeddedTabFactory { - fun browserId(url: String) = "url:$url" + fun webAppId(url: String) = "url:$url" - fun nappletId(coordinate: String) = "nostr:$coordinate" + fun nostrAppId(coordinate: String) = "nostr:$coordinate" /** Acquires (or returns) the warm browser controller for [url], routing over Tor per the site's choice. */ - fun acquireBrowser( + fun acquireWebApp( context: Context, url: String, backgroundColor: Int, - ): EmbeddedBrowserController = - EmbeddedTabHost.acquire(browserId(url)) { + ): EmbeddedWebAppController = + EmbeddedTabHost.acquire(webAppId(url)) { val proxyPort = Amethyst.instance.torManager.activePortOrNull.value ?: -1 - val initialUseTor = proxyPort > 0 && WebUrlNetworkRegistry.useTor(url) + val initialUseTor = proxyPort > 0 && WebAppNetworkRegistry.useTor(url) val themeType = Amethyst.instance.uiPrefs.value.theme.value val theme = when (themeType) { @@ -69,24 +69,24 @@ object EmbeddedTabFactory { if (nightMask == Configuration.UI_MODE_NIGHT_YES) "DARK" else "LIGHT" } } - EmbeddedBrowserController(context.applicationContext, proxyPort, initialUseTor, backgroundColor, theme).also { it.bind(url) } - } as EmbeddedBrowserController + EmbeddedWebAppController(context.applicationContext, proxyPort, initialUseTor, backgroundColor, theme).also { it.bind(url) } + } as EmbeddedWebAppController /** * Acquires (or returns) the warm nsite/napplet controller for [coordinate] using already-resolved * launch [params] (the caller has the metadata anyway). [backgroundColor] is stamped into the params * so the sandbox WebView doesn't flash white before paint. */ - fun acquireNapplet( + fun acquireNostrApp( context: Context, coordinate: String, params: Bundle, backgroundColor: Int, - ): EmbeddedNappletController { + ): EmbeddedNostrAppController { params.putInt(NappletHostContract.EXTRA_BG_COLOR, backgroundColor) - return EmbeddedTabHost.acquire(nappletId(coordinate)) { - EmbeddedNappletController(context.applicationContext, params).also { it.bind() } - } as EmbeddedNappletController + return EmbeddedTabHost.acquire(nostrAppId(coordinate)) { + EmbeddedNostrAppController(context.applicationContext, params).also { it.bind() } + } as EmbeddedNostrAppController } /** @@ -100,30 +100,30 @@ object EmbeddedTabFactory { backgroundColor: Int, ): Boolean = when (app) { - is FavoriteApp.WebUrl -> { + is FavoriteApp.WebApp -> { // Privacy: never preload a Tor-routed site over clearnet just because Tor hasn't finished // connecting yet. A site only "wants Tor" when Tor is enabled AND not opted out for it; in // that case wait until the proxy port is actually up (the preloader retries). When Tor is // off, or the user opted this site out, clearnet IS the real route — preload immediately. val torEnabled = Amethyst.instance.torPrefs.torType.value != TorType.OFF - val wantsTor = torEnabled && WebUrlNetworkRegistry.useTor(app.url) + val wantsTor = torEnabled && WebAppNetworkRegistry.useTor(app.url) val torReady = Amethyst.instance.torManager.activePortOrNull.value != null if (wantsTor && !torReady) { false } else { - acquireBrowser(context, app.url, backgroundColor) + acquireWebApp(context, app.url, backgroundColor) true } } is FavoriteApp.NostrApp -> { - if (EmbeddedTabHost.isWarm(nappletId(app.coordinate))) { + if (EmbeddedTabHost.isWarm(nostrAppId(app.coordinate))) { true } else { val params = FavoriteAppLauncher.embedParams(context, app.coordinate) if (params == null) { false } else { - acquireNapplet(context, app.coordinate, params, backgroundColor) + acquireNostrApp(context, app.coordinate, params, backgroundColor) true } } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/embed/EmbeddedTabPreloader.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/embed/EmbeddedTabPreloader.kt index 0608a38f52..312f76a25e 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/embed/EmbeddedTabPreloader.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/embed/EmbeddedTabPreloader.kt @@ -35,7 +35,7 @@ import androidx.compose.ui.unit.dp import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.vitorpamplona.amethyst.favorites.FavoriteAppsRegistry import com.vitorpamplona.amethyst.napplet.NappletNetworkRegistry -import com.vitorpamplona.amethyst.napplet.WebUrlNetworkRegistry +import com.vitorpamplona.amethyst.napplet.WebAppNetworkRegistry import com.vitorpamplona.amethyst.ui.navigation.bottombars.favoriteIds import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import kotlinx.coroutines.delay @@ -81,9 +81,9 @@ fun EmbeddedTabPreloader(accountViewModel: AccountViewModel) { // Hydrate the per-site Tor/open-web choices BEFORE the first preload: a cold start otherwise reads // the bare Tor default and would route a site the user pinned to the open web through Tor (or stall // it waiting for Tor), which is exactly what breaks Tor-incompatible servers. - WebUrlNetworkRegistry.init(context) + WebAppNetworkRegistry.init(context) NappletNetworkRegistry.init(context) - WebUrlNetworkRegistry.awaitReady() + WebAppNetworkRegistry.awaitReady() NappletNetworkRegistry.awaitReady() var attempt = 0 while (isActive) { diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/favorites/EmbeddedNappletController.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/favorites/EmbeddedNostrAppController.kt similarity index 99% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/favorites/EmbeddedNappletController.kt rename to amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/favorites/EmbeddedNostrAppController.kt index 507032f81f..53cb90bfe9 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/favorites/EmbeddedNappletController.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/favorites/EmbeddedNostrAppController.kt @@ -52,10 +52,10 @@ import java.util.concurrent.atomic.AtomicLong * * [params] is the bundle minted in the main process by * [NappletLauncher.buildLaunchParams][com.vitorpamplona.amethyst.napplet.NappletLauncher.buildLaunchParams] - * — the verified manifest, identity, and launch token. The mirror of `EmbeddedBrowserController`. + * — the verified manifest, identity, and launch token. The mirror of `EmbeddedWebAppController`. */ @RequiresApi(Build.VERSION_CODES.R) -class EmbeddedNappletController( +class EmbeddedNostrAppController( private val appContext: Context, private val params: Bundle, ) : EmbeddedSurfaceController, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/favorites/FavoriteAppsScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/favorites/FavoriteAppsScreen.kt index f466fa61db..baa33c9861 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/favorites/FavoriteAppsScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/favorites/FavoriteAppsScreen.kt @@ -180,7 +180,7 @@ internal fun FavoriteAppCell( val iconKeys by BrowserIconRegistry.keys.collectAsStateWithLifecycle() val faviconModel = remember(app, iconKeys) { - (app as? FavoriteApp.WebUrl)?.let { OmniboxInput.hostOf(it.url)?.let(BrowserIconRegistry::iconModelFor) } + (app as? FavoriteApp.WebApp)?.let { OmniboxInput.hostOf(it.url)?.let(BrowserIconRegistry::iconModelFor) } } Column( 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/NostrAppScreen.kt similarity index 95% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/favorites/FavoriteNappletScreen.kt rename to amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/favorites/NostrAppScreen.kt index e93588b49e..048dfba92e 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/NostrAppScreen.kt @@ -70,7 +70,8 @@ import com.vitorpamplona.amethyst.ui.screen.loggedIn.embed.EmbeddedTabFactory import com.vitorpamplona.amethyst.ui.screen.loggedIn.embed.EmbeddedTabHost /** - * A favorited nsite/napplet rendered as an **in-app tab**. The verified-blob sandbox surface (hosted in + * A **Nostr app** — an nSite or nApplet, reached by [coordinate] (favorited or not) — rendered as an + * **in-app tab**. The verified-blob sandbox surface (hosted in * the keyless `:napplet` process by `NappletHostService`) is drawn by the persistent [EmbeddedTabHost] * layer, which keeps the session warm across tab swaps — the surface stays attached and just moves over * the area this screen reserves. The **trusted chrome** (sandbox shield, app name, "what it can access") @@ -81,13 +82,13 @@ import com.vitorpamplona.amethyst.ui.screen.loggedIn.embed.EmbeddedTabHost * leaves. Requires API 30+ for the cross-process surface. */ @Composable -fun FavoriteNappletScreen( +fun NostrAppScreen( coordinate: String, accountViewModel: AccountViewModel, nav: INav, ) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { - EmbeddedNappletTab(coordinate, accountViewModel, nav) + EmbeddedNostrAppTab(coordinate, accountViewModel, nav) } else { Box(Modifier.fillMaxSize(), contentAlignment = Alignment.Center) { Text( @@ -101,7 +102,7 @@ fun FavoriteNappletScreen( @RequiresApi(Build.VERSION_CODES.R) @OptIn(ExperimentalMaterial3Api::class) @Composable -private fun EmbeddedNappletTab( +private fun EmbeddedNostrAppTab( coordinate: String, accountViewModel: AccountViewModel, nav: INav, @@ -132,7 +133,7 @@ private fun EmbeddedNappletTab( val controller = remember(id) { - EmbeddedTabFactory.acquireNapplet(context, coordinate, params, backgroundColor) + EmbeddedTabFactory.acquireNostrApp(context, coordinate, params, backgroundColor) } // Keep the controller callbacks fresh (cheap, need the latest closures). @@ -203,7 +204,7 @@ private fun EmbeddedNappletTab( Scaffold( bottomBar = { - AppBottomBar(Route.FavoriteNostrApp(coordinate), nav, accountViewModel) { route -> nav.navBottomBar(route) } + AppBottomBar(Route.NostrApp(coordinate), nav, accountViewModel) { route -> nav.navBottomBar(route) } }, ) { padding -> // Reserve the full content area; the warm surface, its top sheet, and the loading/error overlay @@ -227,7 +228,7 @@ private fun UnavailableTab( Scaffold( topBar = { TopAppBar(title = { Text(stringResource(R.string.favorite_apps)) }) }, bottomBar = { - AppBottomBar(Route.FavoriteNostrApp(coordinate), nav, accountViewModel) { route -> nav.navBottomBar(route) } + AppBottomBar(Route.NostrApp(coordinate), nav, accountViewModel) { route -> nav.navBottomBar(route) } }, ) { padding -> Box( diff --git a/amethyst/src/main/res/values/strings.xml b/amethyst/src/main/res/values/strings.xml index 4c4e0adec7..e1376ea31c 100644 --- a/amethyst/src/main/res/values/strings.xml +++ b/amethyst/src/main/res/values/strings.xml @@ -661,8 +661,8 @@ Circuit EMOM AMRAP - Apps - Apps + App Store + App Store nApplets nSites No nSites found yet. @@ -681,8 +681,8 @@ Favorites Options Remove from history - Favorite apps - No favorite apps yet. Open a web client or nsite and tap the star to pin it here. + Web apps + Nothing here yet. Open a web app, nApplet, or nSite and tap the star to keep it here. Add to favorites Remove from favorites Open in its own window diff --git a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/favorites/FavoriteApp.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/favorites/FavoriteApp.kt index d6edc955ad..ac731b6d52 100644 --- a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/favorites/FavoriteApp.kt +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/favorites/FavoriteApp.kt @@ -29,14 +29,15 @@ import androidx.compose.runtime.Immutable * * Two shapes, because the launch layer has exactly two shapes (see `FavoriteAppLauncher`): * - * - [NostrApp] — a NIP-5A nsite **or** NIP-5D napplet. Both resolve to the same launch path + * - [NostrApp] — a NIP-5A nSite **or** NIP-5D nApplet. Both resolve to the same launch path * (`NappletLauncher` → sandboxed `:napplet` host), so they share one case here. Whether the - * resolved event is an nsite (website mode) or a locked napplet is recomputed from the live event + * resolved event is an nSite (website mode) or a locked nApplet is recomputed from the live event * at launch time, never stored: the addressable [coordinate] survives code/manifest updates, but * the manifest's `requires`/website-mode flags do not. - * - [WebUrl] — an arbitrary `https://` nostr client. No identity, no coordinate; the URL is the key. + * - [WebApp] — a Nostr web client reached by an arbitrary `https://` URL. No identity, no + * coordinate; the URL is the key. * - * A [NostrApp] is only launchable while its event is resolvable in `LocalCache`; a [WebUrl] is always + * A [NostrApp] is only launchable while its event is resolvable in `LocalCache`; a [WebApp] is always * launchable. That is the one robustness difference between the two cases. */ @Immutable @@ -68,14 +69,15 @@ sealed interface FavoriteApp { override val id: String get() = "nostr:$coordinate" } - /** An arbitrary web client reached by URL. */ + /** A Nostr web client reached by URL — an arbitrary `https://` app. */ @Immutable - data class WebUrl( + data class WebApp( val url: String, override val label: String, override val addedAt: Long, override val iconUrl: String? = null, ) : FavoriteApp { + // The "url:" prefix is persisted (favorites de-dup + bottom-bar keys); keep it stable. override val id: String get() = "url:$url" } } From 7d163b89c99e873b66959f12fc2513bd5aa9494e Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 25 Jun 2026 18:19:16 +0000 Subject: [PATCH 2/2] refactor: replace napplet host websiteMode boolean with a HostProfile type MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The "locked nApplet vs open nSite" distinction was a `websiteMode: Boolean` threaded through an Intent extra and re-branched at eight independent sites across the launcher, the content server, both host surfaces, and the chrome. That is one coupled security posture (capabilities, CSP, NIP-07 injection, off-origin policy, network UI) expressed as scattered, drift-prone flags — and boolean-blind, since the "website" is actually the *more* capable mode. Introduce `HostProfile` (NAPPLET | WEBSITE), resolved once in the trusted main process and carried over the Intent/Messenger boundary as its name. Every coupled consequence now reads from one place: - `declaredCapabilities(requires)` — THE broker grant, minted into the token - `appCsp` / `injectsNip07` / `allowsOffOrigin` / `exposesNetwork` Pure mechanical mapping (every branch 1:1, no behavior change). The wire extra `EXTRA_WEBSITE_MODE` boolean becomes `EXTRA_HOST_PROFILE` string — safe, as it is in-app process-to-process IPC, never persisted. Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_01HrxLCMcQADUPnm8Sj9ZJ63 --- .../amethyst/favorites/FavoriteAppLauncher.kt | 13 ++-- .../amethyst/napplet/NappletLauncher.kt | 26 +++---- .../amethyst/ui/note/types/StaticWebsite.kt | 5 +- .../loggedIn/favorites/NostrAppScreen.kt | 9 ++- .../amethyst/napplethost/HostProfile.kt | 73 +++++++++++++++++++ .../napplethost/NappletContentServer.kt | 12 +-- .../napplethost/NappletHostActivity.kt | 14 ++-- .../napplethost/NappletHostContract.kt | 7 +- .../napplethost/NappletHostService.kt | 8 +- 9 files changed, 119 insertions(+), 48 deletions(-) create mode 100644 nappletHost/src/main/kotlin/com/vitorpamplona/amethyst/napplethost/HostProfile.kt diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/favorites/FavoriteAppLauncher.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/favorites/FavoriteAppLauncher.kt index 750bc6647a..f9ab3183e9 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/favorites/FavoriteAppLauncher.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/favorites/FavoriteAppLauncher.kt @@ -33,6 +33,7 @@ import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.model.ThemeType import com.vitorpamplona.amethyst.napplet.NappletLauncher import com.vitorpamplona.amethyst.napplet.WebAppNetworkRegistry +import com.vitorpamplona.amethyst.napplethost.HostProfile import com.vitorpamplona.amethyst.napplethost.NappletBrowserActivity import com.vitorpamplona.quartz.nip01Core.core.Event import com.vitorpamplona.quartz.nip5aStaticWebsites.NamedSiteEvent @@ -117,7 +118,7 @@ object FavoriteAppLauncher { aggregateHash = null, title = event.title() ?: "nsite", requires = emptyList(), - websiteMode = true, + profile = HostProfile.WEBSITE, ) is NamedSiteEvent -> NappletLauncher.launch( @@ -129,7 +130,7 @@ object FavoriteAppLauncher { aggregateHash = null, title = event.title() ?: event.identifier(), requires = emptyList(), - websiteMode = true, + profile = HostProfile.WEBSITE, ) else -> { Log.w("FavoriteAppLauncher", "Favorited app not resolvable yet: $coordinate") @@ -160,7 +161,7 @@ object FavoriteAppLauncher { event.declaredAggregateHash() ?: event.computeAggregateHash(), event.title() ?: "Napplet", event.requires(), - false, + HostProfile.NAPPLET, ) is NamedNappletEvent -> NappletLauncher.buildLaunchParams( @@ -172,7 +173,7 @@ object FavoriteAppLauncher { event.declaredAggregateHash() ?: event.computeAggregateHash(), event.title() ?: event.identifier(), event.requires(), - false, + HostProfile.NAPPLET, ) is RootSiteEvent -> NappletLauncher.buildLaunchParams( @@ -184,7 +185,7 @@ object FavoriteAppLauncher { null, event.title() ?: "nsite", emptyList(), - true, + HostProfile.WEBSITE, ) is NamedSiteEvent -> NappletLauncher.buildLaunchParams( @@ -196,7 +197,7 @@ object FavoriteAppLauncher { null, event.title() ?: event.identifier(), emptyList(), - true, + HostProfile.WEBSITE, ) else -> null } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/napplet/NappletLauncher.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/napplet/NappletLauncher.kt index 439b4929aa..d7e6d15f57 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/napplet/NappletLauncher.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/napplet/NappletLauncher.kt @@ -25,11 +25,10 @@ import android.content.Intent import android.content.res.Configuration import android.os.Bundle import com.vitorpamplona.amethyst.Amethyst -import com.vitorpamplona.amethyst.commons.napplet.NappletCapability import com.vitorpamplona.amethyst.commons.napplet.NappletIdentity -import com.vitorpamplona.amethyst.commons.napplet.resolveRequiredCapabilities import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.model.ThemeType +import com.vitorpamplona.amethyst.napplethost.HostProfile import com.vitorpamplona.amethyst.napplethost.NappletHostActivity import com.vitorpamplona.amethyst.napplethost.NappletHostContract import com.vitorpamplona.quartz.nip01Core.core.HexKey @@ -74,12 +73,12 @@ object NappletLauncher { aggregateHash: HexKey?, title: String, requires: List, - // nSites open in "website mode": a NIP-07 window.nostr provider + normal network. The broker - // then grants the IDENTITY + RELAY capabilities NIP-07 needs (consent-gated), regardless of the - // (empty) manifest `requires`. Napplets pass false and keep their declared-only, locked sandbox. - websiteMode: Boolean = false, + // nSites open as [HostProfile.WEBSITE]: a NIP-07 window.nostr provider + normal network. The + // broker then grants the IDENTITY + RELAY capabilities NIP-07 needs (consent-gated), regardless + // of the (empty) manifest `requires`. Napplets keep the default locked [HostProfile.NAPPLET]. + profile: HostProfile = HostProfile.NAPPLET, ) { - val params = buildLaunchParams(context, paths, servers, authorPubKey, identifier, aggregateHash, title, requires, websiteMode) + val params = buildLaunchParams(context, paths, servers, authorPubKey, identifier, aggregateHash, title, requires, profile) val intent = Intent(context, NappletHostActivity::class.java).apply { putExtras(params) @@ -104,7 +103,7 @@ object NappletLauncher { aggregateHash: HexKey?, title: String, requires: List, - websiteMode: Boolean, + profile: HostProfile, ): Bundle { val proxyPort = Amethyst.instance.torManager.activePortOrNull.value ?: -1 @@ -120,18 +119,13 @@ object NappletLauncher { // Mint the launch token in the (trusted) main process: the broker resolves the sandbox's // requests back to THIS identity + declared set, regardless of anything the sandbox sends. val identity = NappletIdentity(authorPubKey = authorPubKey, identifier = identifier, aggregateHash = aggregateHash) - val declared = - if (websiteMode) { - setOf(NappletCapability.IDENTITY, NappletCapability.RELAY) - } else { - resolveRequiredCapabilities(requires).capabilities.toSet() - } + val declared = profile.declaredCapabilities(requires) val launchToken = NappletLaunchRegistry.register(identity, declared) // Resolve the per-site network choice (Tor default; a site can be opted out to the open web). // Locked napplets always keep Tor for their blob fetches — only nSites expose the toggle. NappletNetworkRegistry.init(context.applicationContext) - val useTor = if (websiteMode) NappletNetworkRegistry.useTor(identity.coordinate) else true + val useTor = if (profile.exposesNetwork) NappletNetworkRegistry.useTor(identity.coordinate) else true // Resolve capability labels here (the app has the resources) so the sandbox module needs none. val capLabels = declared.map { context.getString(it.labelRes()) } @@ -159,7 +153,7 @@ object NappletLauncher { putStringArrayList(NappletHostContract.EXTRA_CAP_LABELS, ArrayList(capLabels)) putString(NappletHostContract.EXTRA_LAUNCH_TOKEN, launchToken) putInt(NappletHostContract.EXTRA_PROXY_PORT, proxyPort) - putBoolean(NappletHostContract.EXTRA_WEBSITE_MODE, websiteMode) + putString(NappletHostContract.EXTRA_HOST_PROFILE, profile.name) putBoolean(NappletHostContract.EXTRA_USE_TOR, useTor) putString(NappletHostContract.EXTRA_THEME, theme) } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/StaticWebsite.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/StaticWebsite.kt index 3616216a88..027fdd9247 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/StaticWebsite.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/StaticWebsite.kt @@ -28,6 +28,7 @@ import com.vitorpamplona.amethyst.commons.ui.note.StaticWebsiteCard import com.vitorpamplona.amethyst.favorites.FavoriteAppLauncher import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.napplet.NappletLauncher +import com.vitorpamplona.amethyst.napplethost.HostProfile import com.vitorpamplona.amethyst.napplethost.NappletBlobPrefetcher import com.vitorpamplona.amethyst.ui.navigation.navs.INav import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel @@ -132,7 +133,7 @@ fun RenderRootSiteEvent( aggregateHash = null, title = event.title() ?: "nsite", requires = emptyList(), - websiteMode = true, + profile = HostProfile.WEBSITE, ) } } else { @@ -173,7 +174,7 @@ fun RenderNamedSiteEvent( aggregateHash = null, title = event.title() ?: event.identifier(), requires = emptyList(), - websiteMode = true, + profile = HostProfile.WEBSITE, ) } } else { diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/favorites/NostrAppScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/favorites/NostrAppScreen.kt index 048dfba92e..bd33602d4f 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/favorites/NostrAppScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/favorites/NostrAppScreen.kt @@ -58,6 +58,7 @@ import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.commons.favorites.FavoriteApp import com.vitorpamplona.amethyst.favorites.FavoriteAppLauncher import com.vitorpamplona.amethyst.favorites.FavoriteAppsRegistry +import com.vitorpamplona.amethyst.napplethost.HostProfile import com.vitorpamplona.amethyst.napplethost.NappletEmbedContract import com.vitorpamplona.amethyst.napplethost.NappletHostContract import com.vitorpamplona.amethyst.ui.navigation.bottombars.AppBottomBar @@ -122,7 +123,7 @@ private fun EmbeddedNostrAppTab( val title = params.getString(NappletHostContract.EXTRA_TITLE).orEmpty() val capLabels = params.getStringArrayList(NappletHostContract.EXTRA_CAP_LABELS).orEmpty() - val websiteMode = params.getBoolean(NappletHostContract.EXTRA_WEBSITE_MODE, false) + val profile = HostProfile.fromName(params.getString(NappletHostContract.EXTRA_HOST_PROFILE)) val useTor = params.getBoolean(NappletHostContract.EXTRA_USE_TOR, true) var canGoBack by remember { mutableStateOf(false) } @@ -199,7 +200,7 @@ private fun EmbeddedNostrAppTab( BackHandler(enabled = canGoBack) { controller.back() } if (showAccess) { - AccessDialog(title, capLabels, websiteMode, useTor) { showAccess = false } + AccessDialog(title, capLabels, profile.exposesNetwork, useTor) { showAccess = false } } Scaffold( @@ -251,7 +252,7 @@ private fun UnavailableTab( private fun AccessDialog( title: String, capLabels: List, - websiteMode: Boolean, + showsNetwork: Boolean, useTor: Boolean, onDismiss: () -> Unit, ) { @@ -262,7 +263,7 @@ private fun AccessDialog( capLabels.joinToString("\n") { "• $it" } } val networkBody = - if (websiteMode) { + if (showsNetwork) { "\n\n" + stringResource(if (useTor) R.string.favorite_app_network_tor else R.string.favorite_app_network_open) } else { "" diff --git a/nappletHost/src/main/kotlin/com/vitorpamplona/amethyst/napplethost/HostProfile.kt b/nappletHost/src/main/kotlin/com/vitorpamplona/amethyst/napplethost/HostProfile.kt new file mode 100644 index 0000000000..eb9e32e67c --- /dev/null +++ b/nappletHost/src/main/kotlin/com/vitorpamplona/amethyst/napplethost/HostProfile.kt @@ -0,0 +1,73 @@ +/* + * 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.napplethost + +import com.vitorpamplona.amethyst.commons.napplet.NappletCapability +import com.vitorpamplona.amethyst.commons.napplet.NappletWebContract +import com.vitorpamplona.amethyst.commons.napplet.resolveRequiredCapabilities + +/** + * The security posture a sandbox host renders under — the single decision that "locked nApplet vs + * open nSite" actually *is*. Replaces the old scattered `websiteMode` boolean: resolved once in the + * trusted main process, carried over the Intent/Messenger boundary as [name], and every coupled + * consequence (capabilities, CSP, NIP-07, off-origin, network UI) reads from here — so they can't + * drift out of sync, and a third posture is one new constant instead of several edited call sites. + */ +enum class HostProfile { + /** NIP-5D napplet: locked sandbox — declared-only capabilities, strict app CSP, no off-origin, pinned to Tor. */ + NAPPLET, + + /** NIP-5A nSite ("website mode"): a normal web app — NIP-07 provider, no app CSP, off-origin + per-site Tor toggle. */ + WEBSITE, + ; + + /** + * What this posture is allowed to ask the broker for — THE security decision, minted into the + * launch token in the trusted main process. A website gets the IDENTITY + RELAY pair NIP-07 needs + * (consent-gated); a locked napplet gets only what its manifest `requires` declares. + */ + fun declaredCapabilities(requires: List): Set = + when (this) { + WEBSITE -> setOf(NappletCapability.IDENTITY, NappletCapability.RELAY) + NAPPLET -> resolveRequiredCapabilities(requires).capabilities.toSet() + } + + /** Strict app CSP for a locked applet (connect-src 'none', etc.); null for a website, which sets its own. */ + val appCsp: String? + get() = if (this == NAPPLET) NappletWebContract.APP_CSP else null + + /** Install the NIP-07 `window.nostr` provider before the shim runs. */ + val injectsNip07: Boolean get() = this == WEBSITE + + /** Let off-origin requests reach the network (a locked applet 404s them — its CSP is connect-src 'none'). */ + val allowsOffOrigin: Boolean get() = this == WEBSITE + + /** + * Apply the WebView SOCKS proxy and expose the Tor/network toggle. A website can re-route over Tor + * or the open web; a locked napplet is pinned to Tor with no toggle. + */ + val exposesNetwork: Boolean get() = this == WEBSITE + + companion object { + /** Reconstruct from the wire [name], defaulting to the locked [NAPPLET] posture on anything unknown. */ + fun fromName(name: String?): HostProfile = entries.firstOrNull { it.name == name } ?: NAPPLET + } +} diff --git a/nappletHost/src/main/kotlin/com/vitorpamplona/amethyst/napplethost/NappletContentServer.kt b/nappletHost/src/main/kotlin/com/vitorpamplona/amethyst/napplethost/NappletContentServer.kt index 94c38bba33..e4fce4c3d6 100644 --- a/nappletHost/src/main/kotlin/com/vitorpamplona/amethyst/napplethost/NappletContentServer.kt +++ b/nappletHost/src/main/kotlin/com/vitorpamplona/amethyst/napplethost/NappletContentServer.kt @@ -57,9 +57,9 @@ class NappletContentServer( // The applet's own per-applet origin (a distinct napplet.local subdomain). The shell is on // NappletWebContract.ORIGIN; app blobs are served here so the applet has a real, isolated origin. private val appOrigin: String, - // nSite "website mode": the applet is a normal web app — it gets a NIP-07 window.nostr provider, - // normal network (no app CSP; off-origin requests defer to the WebView), unlike a locked napplet. - private val websiteMode: Boolean = false, + // The host posture: a WEBSITE nSite is a normal web app — NIP-07 window.nostr provider, normal + // network (no app CSP; off-origin requests defer to the WebView), unlike a locked NAPPLET. + private val profile: HostProfile = HostProfile.NAPPLET, // Embedded surfaces (a windowless Service) can't host the soft keyboard, so the shim installs the // IME proxy agent that relays the focused field to the host's keyboard. The full-screen Activity // host has a native keyboard and leaves this false. @@ -126,7 +126,7 @@ class NappletContentServer( } // Off-origin: a locked napplet 404s (connect-src 'none' means it shouldn't ask). An nSite in // website mode is a normal web app — defer to the WebView so it can load external resources. - return if (websiteMode) null else notFound() + return if (profile.allowsOffOrigin) null else notFound() } private fun serveShell(): WebResourceResponse { @@ -172,7 +172,7 @@ class NappletContentServer( // Locked napplets get the strict app CSP (connect-src 'none', etc.). An nSite in website mode // is a normal web app: no app CSP, so it can talk to relays (wss) and load external resources. - val headers = if (websiteMode) emptyMap() else mapOf("Content-Security-Policy" to NappletWebContract.APP_CSP) + val headers = profile.appCsp?.let { mapOf("Content-Security-Policy" to it) } ?: emptyMap() return WebResourceResponse( mime, charset, @@ -193,7 +193,7 @@ class NappletContentServer( // theme-independently. Allowed by the app CSP's `style-src 'unsafe-inline'`. val style = "" // In website mode, set the NIP-07 flag synchronously *before* the shim so window.nostr installs. - val nip07Flag = if (websiteMode) "" else "" + val nip07Flag = if (profile.injectsNip07) "" else "" // Embedded surface: turn on the IME proxy agent (set before the shim runs). val imeFlag = if (imeProxy) "" else "" val script = "$style$nip07Flag$imeFlag" diff --git a/nappletHost/src/main/kotlin/com/vitorpamplona/amethyst/napplethost/NappletHostActivity.kt b/nappletHost/src/main/kotlin/com/vitorpamplona/amethyst/napplethost/NappletHostActivity.kt index 2d165acbec..d50330a956 100644 --- a/nappletHost/src/main/kotlin/com/vitorpamplona/amethyst/napplethost/NappletHostActivity.kt +++ b/nappletHost/src/main/kotlin/com/vitorpamplona/amethyst/napplethost/NappletHostActivity.kt @@ -110,8 +110,8 @@ class NappletHostActivity : ComponentActivity() { // The sandbox never carries its own coordinate, so a compromised :napplet process can't forge one. private var launchToken: String = "" - // nSite "website mode": a normal web app (NIP-07 window.nostr + normal network), vs a locked napplet. - private var websiteMode: Boolean = false + // The host posture: a WEBSITE nSite (NIP-07 window.nostr + normal network) vs a locked NAPPLET. + private var profile: HostProfile = HostProfile.NAPPLET // Per-site network choice: route this site's traffic through Tor (default) or over the open web. // Applied to both the WebView proxy and the blob-fetch client; toggled from the top-bar onion. @@ -224,7 +224,7 @@ class NappletHostActivity : ComponentActivity() { // "Open web" for a site makes everything direct — both its blob fetches (here) and its live // web traffic (the WebView proxy, below). Tor (the default) routes both through the SOCKS port. val effectiveProxy = if (useTor) proxyPort else -1 - contentServer = NappletContentServer(paths, servers, effectiveProxy, cacheDir, shellHtml, shim, appOrigin, websiteMode) + contentServer = NappletContentServer(paths, servers, effectiveProxy, cacheDir, shellHtml, shim, appOrigin, profile) // Create + warm the WebView NOW so its (slow, first-in-process) Chromium init runs on the main // thread concurrently with the index probe below (which runs on IO) — instead of serially after @@ -237,7 +237,7 @@ class NappletHostActivity : ComponentActivity() { // Route the WebView's own (off-origin) traffic through Tor for an nSite, unless this site was // opted out to the open web. Set process-wide before any page navigation; the shell + blobs are // served from cache via shouldInterceptRequest, so only the site's external requests hit this. - if (websiteMode) applyWebViewProxy(effectiveProxy) + if (profile.exposesNetwork) applyWebViewProxy(effectiveProxy) // Origin-restricted bridge: only the trusted shell page (main frame) can reach native. WebViewCompat.addWebMessageListener( webView, @@ -411,7 +411,7 @@ class NappletHostActivity : ComponentActivity() { servers.addAll(intent.getStringArrayListExtra(NappletHostContract.EXTRA_SERVERS) ?: emptyList()) author = intent.getStringExtra(NappletHostContract.EXTRA_AUTHOR).orEmpty() identifier = intent.getStringExtra(NappletHostContract.EXTRA_IDENTIFIER).orEmpty() - websiteMode = intent.getBooleanExtra(NappletHostContract.EXTRA_WEBSITE_MODE, false) + profile = HostProfile.fromName(intent.getStringExtra(NappletHostContract.EXTRA_HOST_PROFILE)) useTor = intent.getBooleanExtra(NappletHostContract.EXTRA_USE_TOR, true) title = intent.getStringExtra(NappletHostContract.EXTRA_TITLE).orEmpty() proxyPort = intent.getIntExtra(NappletHostContract.EXTRA_PROXY_PORT, -1) @@ -731,8 +731,8 @@ class NappletHostActivity : ComponentActivity() { onReload = { if (this::webView.isInitialized) webView.reload() }, // Website-mode nSites can re-route over Tor; switching rebuilds the session via a confirm // dialog, so the row taps through rather than toggling inline. - torInitiallyOn = if (websiteMode && proxyPort > 0) useTor else null, - onNetworkTap = if (websiteMode && proxyPort > 0) ({ showNetworkDialog() }) else null, + torInitiallyOn = if (profile.exposesNetwork && proxyPort > 0) useTor else null, + onNetworkTap = if (profile.exposesNetwork && proxyPort > 0) ({ showNetworkDialog() }) else null, onInfo = { showAccessDialog() }, ) diff --git a/nappletHost/src/main/kotlin/com/vitorpamplona/amethyst/napplethost/NappletHostContract.kt b/nappletHost/src/main/kotlin/com/vitorpamplona/amethyst/napplethost/NappletHostContract.kt index a6a59b467f..79aed1a630 100644 --- a/nappletHost/src/main/kotlin/com/vitorpamplona/amethyst/napplethost/NappletHostContract.kt +++ b/nappletHost/src/main/kotlin/com/vitorpamplona/amethyst/napplethost/NappletHostContract.kt @@ -61,10 +61,11 @@ object NappletHostContract { const val EXTRA_BG_COLOR = "napplet_bg_color" /** - * nSite "website mode": treat the content as a normal web app — install the NIP-07 `window.nostr` - * provider and allow normal network (no app CSP). Off for locked napplets. + * The host posture this launch renders under — a [HostProfile] name. WEBSITE treats the content as + * a normal web app (NIP-07 `window.nostr` provider, normal network, no app CSP); NAPPLET is the + * locked sandbox. Resolved in the trusted main process; the sandbox derives all coupled policy from it. */ - const val EXTRA_WEBSITE_MODE = "napplet_website_mode" + const val EXTRA_HOST_PROFILE = "napplet_host_profile" /** * Whether this site's traffic routes through Tor (true, the default when Tor is active) or over the diff --git a/nappletHost/src/main/kotlin/com/vitorpamplona/amethyst/napplethost/NappletHostService.kt b/nappletHost/src/main/kotlin/com/vitorpamplona/amethyst/napplethost/NappletHostService.kt index 224c9eeab3..c4b139a513 100644 --- a/nappletHost/src/main/kotlin/com/vitorpamplona/amethyst/napplethost/NappletHostService.kt +++ b/nappletHost/src/main/kotlin/com/vitorpamplona/amethyst/napplethost/NappletHostService.kt @@ -89,7 +89,7 @@ class NappletHostService : Service() { val author: String, val identifier: String, val launchToken: String, - val websiteMode: Boolean, + val profile: HostProfile, val useTor: Boolean, val proxyPort: Int, val bgColor: Int, @@ -218,7 +218,7 @@ class NappletHostService : Service() { author = author, identifier = data.getString(NappletHostContract.EXTRA_IDENTIFIER).orEmpty(), launchToken = launchToken, - websiteMode = data.getBoolean(NappletHostContract.EXTRA_WEBSITE_MODE, false), + profile = HostProfile.fromName(data.getString(NappletHostContract.EXTRA_HOST_PROFILE)), useTor = data.getBoolean(NappletHostContract.EXTRA_USE_TOR, true), proxyPort = data.getInt(NappletHostContract.EXTRA_PROXY_PORT, -1), bgColor = data.getInt(NappletHostContract.EXTRA_BG_COLOR, android.graphics.Color.WHITE), @@ -255,13 +255,13 @@ class NappletHostService : Service() { val wv = WebView(context) val appOrigin = NappletWebContract.appOrigin(deriveAppId(tab.author, tab.identifier)) val effectiveProxy = if (tab.useTor) tab.proxyPort else -1 - tab.contentServer = NappletContentServer(tab.paths, tab.servers, effectiveProxy, cacheDir, shellHtml, shimJs, appOrigin, tab.websiteMode, imeProxy = true) + tab.contentServer = NappletContentServer(tab.paths, tab.servers, effectiveProxy, cacheDir, shellHtml, shimJs, appOrigin, tab.profile, imeProxy = true) hardenWebView(wv, tab) // Theme the pre-load background so the shell/app loading shows Amethyst's background, not white. wv.setBackgroundColor(tab.bgColor) wv.dropSystemBarInsets() - if (tab.websiteMode) applyWebViewProxy(effectiveProxy) + if (tab.profile.exposesNetwork) applyWebViewProxy(effectiveProxy) WebViewCompat.addWebMessageListener(wv, NappletWebContract.BRIDGE_NAME, setOf(NappletWebContract.ORIGIN), ::onShellMessage) tab.webView = wv wv.loadUrl(NappletWebContract.SHELL_URL)