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 45cfd84f05..6274bda285 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/favorites/FavoriteAppLauncher.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/favorites/FavoriteAppLauncher.kt @@ -27,6 +27,7 @@ import android.os.Bundle import android.widget.Toast import com.vitorpamplona.amethyst.Amethyst import com.vitorpamplona.amethyst.commons.favorites.FavoriteApp +import com.vitorpamplona.amethyst.commons.favorites.favoriteCoordinateOf import com.vitorpamplona.amethyst.commons.model.ThemeType import com.vitorpamplona.amethyst.commons.model.cache.LocalCache import com.vitorpamplona.amethyst.napplet.NappletLauncher @@ -212,17 +213,6 @@ object FavoriteAppLauncher { } } - /** - * The addressable coordinate `kind:pubkey:dtag` used to key an nsite/napplet favorite. Stored - * instead of the content hash so the favorite survives routine code/manifest updates. - */ - fun coordinateOf(event: Event): String { - val dTag = - when (event) { - is NamedNappletEvent -> event.identifier() - is NamedSiteEvent -> event.identifier() - else -> "" - } - return "${event.kind}:${event.pubKey}:$dTag" - } + /** See [favoriteCoordinateOf]. */ + fun coordinateOf(event: Event): String = favoriteCoordinateOf(event) } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/MainActivity.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/MainActivity.kt index 33902c8bde..1e0a0202c3 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/MainActivity.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/MainActivity.kt @@ -27,51 +27,19 @@ import androidx.activity.enableEdgeToEdge import androidx.annotation.RequiresApi import androidx.appcompat.app.AppCompatActivity import com.vitorpamplona.amethyst.Amethyst -import com.vitorpamplona.amethyst.commons.actions.ConcordActions -import com.vitorpamplona.amethyst.commons.model.Account -import com.vitorpamplona.amethyst.commons.model.cache.LocalCache -import com.vitorpamplona.amethyst.commons.model.navigation.Route -import com.vitorpamplona.amethyst.commons.model.navigation.routeFor -import com.vitorpamplona.amethyst.commons.richtext.RichTextParser -import com.vitorpamplona.amethyst.commons.ui.navigation.routes.routeFor -import com.vitorpamplona.amethyst.commons.ui.navigation.routes.routeForPointer import com.vitorpamplona.amethyst.commons.ui.note.elements.NowProvider import com.vitorpamplona.amethyst.debugState import com.vitorpamplona.amethyst.service.lang.LanguageTranslatorService import com.vitorpamplona.amethyst.service.notifications.NotificationRelayService -import com.vitorpamplona.amethyst.service.notifications.NotificationRoutes import com.vitorpamplona.amethyst.service.playback.composable.DEFAULT_MUTED_SETTING import com.vitorpamplona.amethyst.service.playback.pip.BackgroundMedia -import com.vitorpamplona.amethyst.ui.navigation.findParameterValue -import com.vitorpamplona.amethyst.ui.navigation.findQueryParameterValue import com.vitorpamplona.amethyst.ui.screen.AccountScreen import com.vitorpamplona.amethyst.ui.theme.AmethystTheme -import com.vitorpamplona.quartz.buzz.invite.BuzzInviteLink -import com.vitorpamplona.quartz.nip01Core.core.AddressableEvent -import com.vitorpamplona.quartz.nip17Dm.base.ChatroomKey -import com.vitorpamplona.quartz.nip19Bech32.Nip19Parser -import com.vitorpamplona.quartz.nip19Bech32.entities.NAddress -import com.vitorpamplona.quartz.nip19Bech32.entities.NEmbed -import com.vitorpamplona.quartz.nip19Bech32.entities.NEvent -import com.vitorpamplona.quartz.nip19Bech32.entities.NNote -import com.vitorpamplona.quartz.nip19Bech32.entities.NProfile -import com.vitorpamplona.quartz.nip19Bech32.entities.NPub -import com.vitorpamplona.quartz.nip29RelayGroups.GroupInviteLink -import com.vitorpamplona.quartz.nip29RelayGroups.GroupNAddrInvite -import com.vitorpamplona.quartz.nip29RelayGroups.metadata.GroupMetadataEvent -import com.vitorpamplona.quartz.nip46RemoteSigner.NostrConnectURI -import com.vitorpamplona.quartz.nip47WalletConnect.Nip47WalletConnect -import com.vitorpamplona.quartz.nip52Calendar.appt.day.CalendarDateSlotEvent -import com.vitorpamplona.quartz.nip52Calendar.appt.time.CalendarTimeSlotEvent -import com.vitorpamplona.quartz.nip73ExternalIds.urls.UrlId import com.vitorpamplona.quartz.utils.Log -import com.vitorpamplona.quartz.utils.UriParser -import kotlinx.coroutines.CancellationException import kotlinx.coroutines.DelicateCoroutinesApi import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.launch -import java.net.URLDecoder class MainActivity : AppCompatActivity() { companion object { @@ -155,339 +123,3 @@ class MainActivity : AppCompatActivity() { super.onDestroy() } } - -private const val NOSTR_URI_PREFIX = "nostr:" - -fun isNotificationRoute(uri: String) = uri.startsWith("notifications", true) || uri.startsWith("nostr:notifications", true) - -fun isHashtagRoute(uri: String) = uri.startsWith("hashtag?id=") || uri.startsWith("nostr:hashtag?id=") - -/** - * A markdown link target that is a bare hashtag fragment, e.g. the `#NostrMultiplayerGames` in - * `[Games](#NostrMultiplayerGames)`. Returns the tag without the leading `#`, or null when the - * uri is anything else (full URLs with anchors don't start with `#`). Reuses the same character - * class that linkifies #hashtags in plain text, so validity matches how tags parse everywhere else. - */ -fun fragmentHashtagOrNull(uri: String): String? { - val match = RichTextParser.hashTagsPattern.matchEntire(uri) ?: return null - if (!match.groups[2]?.value.isNullOrEmpty()) return null - return match.groups[1]?.value -} - -fun isUrlRoute(uri: String) = uri.startsWith("url?id=") || uri.startsWith("nostr:url?id=") - -/** - * A private chatroom, addressed by its participants rather than by a message. - * Posted by DM notifications — see [NotificationRoutes.chatroomUri] for why a DM - * cannot deep-link through its own note. - */ -fun isChatroomRoute(uri: String) = uri.startsWith("chatroom?id=") || uri.startsWith("nostr:chatroom?id=") - -/** - * Note what this deliberately does *not* do: register the room on the account. - * - * `routeToMessage` would, and that is right for the in-app callers, but wrong here twice over. - * `uriToRoute` runs against whichever account is current, *before* `?account=` is read and the - * switch happens (`AppNavigation.NavigateIfIntentRequested`), so a DM notification for account B - * tapped while A is on screen would insert an empty conversation into **A**'s message list. And - * `nostr:` is an exported, browsable scheme, so any web page could post - * `nostr:chatroom?id=&account=…` and inject a room of its choosing. - * - * Nothing needs it: `ChatroomFeedFilter.chatroom()` calls `getOrCreatePrivateChatroom` when the - * screen actually opens, by which point the switch has happened and the room lands on the right - * account. - * - * The ids are still checked here — a room key is a set of pubkeys, so anything that isn't one is - * not a room, and a bad deep link should resolve to nothing rather than to an unopenable screen. - */ -fun chatroomRoute(uri: String): Route? { - val users = - uri - .findQueryParameterValue("id") - ?.split(',') - ?.map { it.trim() } - ?.takeIf { it.isNotEmpty() && it.all(::isPubKeyHex) } - ?.toSet() ?: return null - - return Route.Room(ChatroomKey(users)) -} - -/** A bare 32-byte lowercase-hex pubkey, the only thing a chatroom key is made of. */ -private fun isPubKeyHex(value: String) = value.length == 64 && value.all { it in '0'..'9' || it in 'a'..'f' } - -/** - * A NIP-17 private note or reply, addressed by the rumor's own id because its `nevent` - * names the undeliverable gift wrap instead — see [NotificationRoutes.privateNoteUri]. - */ -fun isPrivateNoteRoute(uri: String) = uri.startsWith("privatenote?id=") || uri.startsWith("nostr:privatenote?id=") - -fun privateNoteRoute(uri: String): Route? { - val id = uri.findQueryParameterValue("id") ?: return null - return Route.EventRedirect(id, isPrivate = true) -} - -fun isConnectedAppRoute(uri: String) = uri.startsWith("connectedapp?coordinate=") || uri.startsWith("nostr:connectedapp?coordinate=") - -/** - * The Connected Apps permission-detail route for an app coordinate (`pubkey:dtag` for a napplet/nsite, - * `browser:` for a web client). Fired by the sandbox host so a running full-screen surface can - * jump straight to its editable permissions; the coordinate is URL-encoded into the [coordinate] param. - */ -fun connectedAppRoute(uri: String): Route.ConnectedAppDetail? { - val coordinate = - runCatching { - val raw = java.net.URI(uri.removePrefix(NOSTR_URI_PREFIX)).findParameterValue("coordinate") ?: return null - URLDecoder.decode(raw, Charsets.UTF_8.name()) - }.getOrNull()?.takeIf { it.isNotBlank() } ?: return null - - return Route.ConnectedAppDetail(coordinate) -} - -fun urlRoute(uri: String): Route.Url? { - val url = - runCatching { - val rawUrl = java.net.URI(uri.removePrefix(NOSTR_URI_PREFIX)).findParameterValue("id") ?: return null - URLDecoder.decode(rawUrl, Charsets.UTF_8.name()) - }.getOrNull() ?: return null - - return UrlId.toScopeOrNull(url)?.let { Route.Url(it) } -} - -fun isWalletConnectRoute(uri: String) = uri.startsWith("dlnwc?value=") || uri.startsWith("amethyst+walletconnect:dlnwc?value=") || uri.startsWith("amethyst+walletconnect://dlnwc?value=") - -fun isMarmotGroupRoute(uri: String) = uri.startsWith("marmot:") - -/** - * Tapping the always-on service notification. That notification's whole subject is the relay - * connections it is holding open, so it lands on the screen that explains them rather than on - * whatever tab happened to be last open. - */ -fun isActiveSubscriptionsRoute(uri: String) = uri.startsWith("activesubs", true) || uri.startsWith("nostr:activesubs", true) - -/** - * The launcher shortcut (res/xml/shortcuts.xml) fires `amethyst:scanqr`, caught by MainActivity's - * existing scheme filter. A pseudo-uri rather than a hardcoded component, because debug and - * benchmark builds carry an applicationId suffix a hardcoded targetPackage would miss, and - * manifest placeholders are not substituted into resource XML. - * - * `nostr:scanqr` is still accepted so an older pinned shortcut keeps working, but the shortcut - * itself uses our own scheme: an implicit `nostr:` VIEW intent can be answered by any Nostr client - * installed alongside us. - */ -fun isScanQrRoute(uri: String) = - uri.equals("scanqr", true) || - uri.equals("amethyst:scanqr", true) || - uri.equals("nostr:scanqr", true) - -private val MARMOT_HEX = Regex("^[0-9a-fA-F]+$") - -fun uriToRoute( - uri: String, - account: Account, -): Route? { - if (isNotificationRoute(uri)) { - val scrollTo = uri.findQueryParameterValue("scrollTo") - return Route.Notification(scrollToEventId = scrollTo) - } - if (isActiveSubscriptionsRoute(uri)) { - return Route.ActiveSubscriptions - } - if (isScanQrRoute(uri)) { - return Route.QRDisplay(account.signer.pubKey, startScanning = true) - } - if (isHashtagRoute(uri)) { - return Route.Hashtag(uri.removePrefix(NOSTR_URI_PREFIX).removePrefix("hashtag?id=").lowercase()) - } - fragmentHashtagOrNull(uri)?.let { - return Route.Hashtag(it.lowercase()) - } - if (isUrlRoute(uri)) { - return urlRoute(uri) - } - if (isChatroomRoute(uri)) { - return chatroomRoute(uri) - } - if (isPrivateNoteRoute(uri)) { - return privateNoteRoute(uri) - } - if (isConnectedAppRoute(uri)) { - return connectedAppRoute(uri) - } - - // A scanned/opened `nostrconnect://` offer is an app asking to connect to our signer: open the - // NIP-46 signer screen and let it run the pairing (it enables the signer as part of connecting). - if (uri.startsWith(NostrConnectURI.NOSTRCONNECT_SCHEME)) { - return Route.Nip46Signer(connectUri = uri) - } - - relayGroupInviteRoute(uri)?.let { return it } - concordInviteRoute(uri)?.let { return it } - buzzInviteRoute(uri)?.let { return it } - - val parsedNip19 = Nip19Parser.uriToRoute(uri) - val nip19 = parsedNip19?.entity - if (nip19 != null) { - LocalCache.consume(nip19) - - val route = - when (nip19) { - is NPub -> { - Route.Profile(nip19.hex) - } - - is NProfile -> { - Route.Profile(nip19.hex) - } - - is NNote -> { - Route.Note(nip19.hex) - } - - is NEvent -> { - val note = LocalCache.getOrCreateNote(nip19.hex) - // Only fall back to the pointer's own kind while the body is missing: once the - // event is cached it may belong somewhere the kind alone can't name (a channel, - // a chatroom), and routeFor knows that. - if (note.event == null) { - routeForPointer(nip19.kind, nip19.hex) ?: Route.EventRedirect(nip19.hex) - } else { - routeFor(note = note, loggedIn = account) ?: Route.EventRedirect(nip19.hex) - } - } - - is NAddress -> { - relayGroupDirectRoute(nip19, parsedNip19.additionalChars) - ?: routeFor( - note = LocalCache.getOrCreateAddressableNote(nip19.address()), - loggedIn = account, - ) ?: calendarDirectRoute(nip19) ?: Route.EventRedirect(nip19.aTag()) - } - - is NEmbed -> { - val noteEvent = nip19.event - if (noteEvent is AddressableEvent) { - routeFor( - note = LocalCache.getOrCreateAddressableNote(noteEvent.address()), - loggedIn = account, - ) ?: Route.EventRedirect(noteEvent.addressTag()) - } else { - routeFor( - note = LocalCache.getOrCreateNote(nip19.event.id), - loggedIn = account, - ) ?: Route.EventRedirect(nip19.event.id) - } - } - - else -> { - null - } - } - - if (route != null) { - return route - } - } - - if (isMarmotGroupRoute(uri)) { - // marmot:?account= - val groupHex = - uri - .removePrefix("marmot:") - .substringBefore("?") - .substringBefore("&") - if (groupHex.matches(MARMOT_HEX)) { - return Route.MarmotGroupChat(groupHex) - } - } - - if (isWalletConnectRoute(uri)) { - try { - val url = UriParser(uri) - val nip47Uri = url.getQueryParameter("value")?.firstOrNull() - if (nip47Uri != null) { - Nip47WalletConnect.parse(nip47Uri) - return Route.WalletAddNwc(nip47Uri) - } - } catch (e: Exception) { - if (e is CancellationException) throw e - } - } - - try { - Nip47WalletConnect.parse(uri) - return Route.WalletAddNwc(uri) - } catch (e: Exception) { - if (e is CancellationException) throw e - } - - return null -} - -/** - * Direct route for an `naddr` whose event hasn't arrived in [LocalCache] yet. When a notification - * is tapped (or a `nostr:naddr…` deep link arrives) for a calendar appointment we know is kind - * 31922/31923, route straight to the dedicated detail screen instead of bouncing through - * [Route.EventRedirect]. The detail screen issues its own per-event subscription, so the user - * sees the calendar-specific loading placeholder while the event arrives, not the generic - * redirect screen. - */ -private fun calendarDirectRoute(nip19: NAddress): Route? = - when (nip19.kind) { - CalendarTimeSlotEvent.KIND, - CalendarDateSlotEvent.KIND, - -> Route.CalendarEventDetail(nip19.kind, nip19.author, nip19.dTag) - else -> null - } - -/** - * Direct route for a NIP-29 group `naddr` (kind 39000) shared as a link. The group - * only exists on its host relay, so the naddr's relay hint is mandatory — the group - * id alone can't be resolved. With a hint we open the group chat straight away - * (cold start included); without one there's nowhere to look, so fall through. - */ -private fun relayGroupDirectRoute( - nip19: NAddress, - additionalChars: String?, -): Route? { - if (nip19.kind != GroupMetadataEvent.KIND) return null - val relay = nip19.relay.firstOrNull() ?: return null - // The spec allows an invite code appended as `naddr1…?invite=`; it arrives - // in the trailing chars after the bech32 body. Carry it so the group auto-joins. - val inviteCode = GroupNAddrInvite.parse(additionalChars) - return Route.RelayGroup(nip19.dTag, relay.url, inviteCode = inviteCode) -} - -/** - * Direct route for a NIP-29 group invite link in the de-facto `'[?code=]` - * form shared by Wisp and 0xchat (e.g. `wss://groups.0xchat.com'abc123`). The link carries its - * own host relay, so we can open the group straight away. Returns null for anything that isn't - * this exact shape, so it's safe to try before the nostr-entity parser. - */ -private fun relayGroupInviteRoute(uri: String): Route? { - val link = GroupInviteLink.parse(uri.removePrefix(NOSTR_URI_PREFIX)) ?: return null - return Route.RelayGroup(link.groupId, link.relayUrl.url, inviteCode = link.code) -} - -/** - * A shared Concord invite URL (`…/invite/#`). Cheap substring gates - * keep the parse off the hot path; the whole URL (fragment included) is carried into - * the route so the redeem flow still has the unlock token. - */ -private fun concordInviteRoute(uri: String): Route? = - if (uri.contains("/invite/") && uri.contains('#') && ConcordActions.parseInviteLink(uri) != null) { - Route.ConcordInvite(uri) - } else { - null - } - -/** - * A Buzz workspace invite (`https:///invite/`) — a plain `/invite/` https URL with - * no fragment, so disjoint from the Concord shape above. Opens the in-app join flow - * ([Route.BuzzInvite]) instead of the external browser so the claim signs with the user's key. - */ -private fun buzzInviteRoute(uri: String): Route? = - if (uri.contains("/invite/") && BuzzInviteLink.parse(uri) != null) { - Route.BuzzInvite(uri) - } else { - null - } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/UriToRoute.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/UriToRoute.kt new file mode 100644 index 0000000000..245498a847 --- /dev/null +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/UriToRoute.kt @@ -0,0 +1,388 @@ +/* + * 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 + +import com.vitorpamplona.amethyst.commons.actions.ConcordActions +import com.vitorpamplona.amethyst.commons.model.Account +import com.vitorpamplona.amethyst.commons.model.cache.LocalCache +import com.vitorpamplona.amethyst.commons.model.navigation.Route +import com.vitorpamplona.amethyst.commons.model.navigation.routeFor +import com.vitorpamplona.amethyst.commons.richtext.RichTextParser +import com.vitorpamplona.amethyst.commons.ui.navigation.routes.routeFor +import com.vitorpamplona.amethyst.commons.ui.navigation.routes.routeForPointer +import com.vitorpamplona.amethyst.ui.navigation.findQueryParameterValue +import com.vitorpamplona.quartz.buzz.invite.BuzzInviteLink +import com.vitorpamplona.quartz.nip01Core.core.AddressableEvent +import com.vitorpamplona.quartz.nip17Dm.base.ChatroomKey +import com.vitorpamplona.quartz.nip19Bech32.Nip19Parser +import com.vitorpamplona.quartz.nip19Bech32.entities.NAddress +import com.vitorpamplona.quartz.nip19Bech32.entities.NEmbed +import com.vitorpamplona.quartz.nip19Bech32.entities.NEvent +import com.vitorpamplona.quartz.nip19Bech32.entities.NNote +import com.vitorpamplona.quartz.nip19Bech32.entities.NProfile +import com.vitorpamplona.quartz.nip19Bech32.entities.NPub +import com.vitorpamplona.quartz.nip29RelayGroups.GroupInviteLink +import com.vitorpamplona.quartz.nip29RelayGroups.GroupNAddrInvite +import com.vitorpamplona.quartz.nip29RelayGroups.metadata.GroupMetadataEvent +import com.vitorpamplona.quartz.nip46RemoteSigner.NostrConnectURI +import com.vitorpamplona.quartz.nip47WalletConnect.Nip47WalletConnect +import com.vitorpamplona.quartz.nip52Calendar.appt.day.CalendarDateSlotEvent +import com.vitorpamplona.quartz.nip52Calendar.appt.time.CalendarTimeSlotEvent +import com.vitorpamplona.quartz.nip73ExternalIds.urls.UrlId +import com.vitorpamplona.quartz.utils.UriParser +import com.vitorpamplona.quartz.utils.UrlEncoder +import kotlinx.coroutines.CancellationException + +private const val NOSTR_URI_PREFIX = "nostr:" + +fun isNotificationRoute(uri: String) = uri.startsWith("notifications", true) || uri.startsWith("nostr:notifications", true) + +fun isHashtagRoute(uri: String) = uri.startsWith("hashtag?id=") || uri.startsWith("nostr:hashtag?id=") + +/** + * A markdown link target that is a bare hashtag fragment, e.g. the `#NostrMultiplayerGames` in + * `[Games](#NostrMultiplayerGames)`. Returns the tag without the leading `#`, or null when the + * uri is anything else (full URLs with anchors don't start with `#`). Reuses the same character + * class that linkifies #hashtags in plain text, so validity matches how tags parse everywhere else. + */ +fun fragmentHashtagOrNull(uri: String): String? { + val match = RichTextParser.hashTagsPattern.matchEntire(uri) ?: return null + if (!match.groups[2]?.value.isNullOrEmpty()) return null + return match.groups[1]?.value +} + +fun isUrlRoute(uri: String) = uri.startsWith("url?id=") || uri.startsWith("nostr:url?id=") + +/** + * A private chatroom, addressed by its participants rather than by a message. + * Posted by DM notifications — see `NotificationRoutes.chatroomUri` for why a DM + * cannot deep-link through its own note. + */ +fun isChatroomRoute(uri: String) = uri.startsWith("chatroom?id=") || uri.startsWith("nostr:chatroom?id=") + +/** + * Note what this deliberately does *not* do: register the room on the account. + * + * `routeToMessage` would, and that is right for the in-app callers, but wrong here twice over. + * `uriToRoute` runs against whichever account is current, *before* `?account=` is read and the + * switch happens (`AppNavigation.NavigateIfIntentRequested`), so a DM notification for account B + * tapped while A is on screen would insert an empty conversation into **A**'s message list. And + * `nostr:` is an exported, browsable scheme, so any web page could post + * `nostr:chatroom?id=&account=…` and inject a room of its choosing. + * + * Nothing needs it: `ChatroomFeedFilter.chatroom()` calls `getOrCreatePrivateChatroom` when the + * screen actually opens, by which point the switch has happened and the room lands on the right + * account. + * + * The ids are still checked here — a room key is a set of pubkeys, so anything that isn't one is + * not a room, and a bad deep link should resolve to nothing rather than to an unopenable screen. + */ +fun chatroomRoute(uri: String): Route? { + val users = + uri + .findQueryParameterValue("id") + ?.split(',') + ?.map { it.trim() } + ?.takeIf { it.isNotEmpty() && it.all(::isPubKeyHex) } + ?.toSet() ?: return null + + return Route.Room(ChatroomKey(users)) +} + +/** A bare 32-byte lowercase-hex pubkey, the only thing a chatroom key is made of. */ +private fun isPubKeyHex(value: String) = value.length == 64 && value.all { it in '0'..'9' || it in 'a'..'f' } + +/** + * A NIP-17 private note or reply, addressed by the rumor's own id because its `nevent` + * names the undeliverable gift wrap instead — see `NotificationRoutes.privateNoteUri`. + */ +fun isPrivateNoteRoute(uri: String) = uri.startsWith("privatenote?id=") || uri.startsWith("nostr:privatenote?id=") + +fun privateNoteRoute(uri: String): Route? { + val id = uri.findQueryParameterValue("id") ?: return null + return Route.EventRedirect(id, isPrivate = true) +} + +fun isConnectedAppRoute(uri: String) = uri.startsWith("connectedapp?coordinate=") || uri.startsWith("nostr:connectedapp?coordinate=") + +/** + * The Connected Apps permission-detail route for an app coordinate (`pubkey:dtag` for a napplet/nsite, + * `browser:` for a web client). Fired by the sandbox host so a running full-screen surface can + * jump straight to its editable permissions; the coordinate is URL-encoded into the [coordinate] param. + */ +fun connectedAppRoute(uri: String): Route.ConnectedAppDetail? { + val coordinate = + runCatching { + val raw = uri.removePrefix(NOSTR_URI_PREFIX).findQueryParameterValue("coordinate") ?: return null + UrlEncoder.decode(raw) + }.getOrNull()?.takeIf { it.isNotBlank() } ?: return null + + return Route.ConnectedAppDetail(coordinate) +} + +fun urlRoute(uri: String): Route.Url? { + val url = + runCatching { + val rawUrl = uri.removePrefix(NOSTR_URI_PREFIX).findQueryParameterValue("id") ?: return null + UrlEncoder.decode(rawUrl) + }.getOrNull() ?: return null + + return UrlId.toScopeOrNull(url)?.let { Route.Url(it) } +} + +fun isWalletConnectRoute(uri: String) = uri.startsWith("dlnwc?value=") || uri.startsWith("amethyst+walletconnect:dlnwc?value=") || uri.startsWith("amethyst+walletconnect://dlnwc?value=") + +fun isMarmotGroupRoute(uri: String) = uri.startsWith("marmot:") + +/** + * Tapping the always-on service notification. That notification's whole subject is the relay + * connections it is holding open, so it lands on the screen that explains them rather than on + * whatever tab happened to be last open. + */ +fun isActiveSubscriptionsRoute(uri: String) = uri.startsWith("activesubs", true) || uri.startsWith("nostr:activesubs", true) + +/** + * The launcher shortcut (res/xml/shortcuts.xml) fires `amethyst:scanqr`, caught by MainActivity's + * existing scheme filter. A pseudo-uri rather than a hardcoded component, because debug and + * benchmark builds carry an applicationId suffix a hardcoded targetPackage would miss, and + * manifest placeholders are not substituted into resource XML. + * + * `nostr:scanqr` is still accepted so an older pinned shortcut keeps working, but the shortcut + * itself uses our own scheme: an implicit `nostr:` VIEW intent can be answered by any Nostr client + * installed alongside us. + */ +fun isScanQrRoute(uri: String) = + uri.equals("scanqr", true) || + uri.equals("amethyst:scanqr", true) || + uri.equals("nostr:scanqr", true) + +private val MARMOT_HEX = Regex("^[0-9a-fA-F]+$") + +fun uriToRoute( + uri: String, + account: Account, +): Route? { + if (isNotificationRoute(uri)) { + val scrollTo = uri.findQueryParameterValue("scrollTo") + return Route.Notification(scrollToEventId = scrollTo) + } + if (isActiveSubscriptionsRoute(uri)) { + return Route.ActiveSubscriptions + } + if (isScanQrRoute(uri)) { + return Route.QRDisplay(account.signer.pubKey, startScanning = true) + } + if (isHashtagRoute(uri)) { + return Route.Hashtag(uri.removePrefix(NOSTR_URI_PREFIX).removePrefix("hashtag?id=").lowercase()) + } + fragmentHashtagOrNull(uri)?.let { + return Route.Hashtag(it.lowercase()) + } + if (isUrlRoute(uri)) { + return urlRoute(uri) + } + if (isChatroomRoute(uri)) { + return chatroomRoute(uri) + } + if (isPrivateNoteRoute(uri)) { + return privateNoteRoute(uri) + } + if (isConnectedAppRoute(uri)) { + return connectedAppRoute(uri) + } + + // A scanned/opened `nostrconnect://` offer is an app asking to connect to our signer: open the + // NIP-46 signer screen and let it run the pairing (it enables the signer as part of connecting). + if (uri.startsWith(NostrConnectURI.NOSTRCONNECT_SCHEME)) { + return Route.Nip46Signer(connectUri = uri) + } + + relayGroupInviteRoute(uri)?.let { return it } + concordInviteRoute(uri)?.let { return it } + buzzInviteRoute(uri)?.let { return it } + + val parsedNip19 = Nip19Parser.uriToRoute(uri) + val nip19 = parsedNip19?.entity + if (nip19 != null) { + LocalCache.consume(nip19) + + val route = + when (nip19) { + is NPub -> { + Route.Profile(nip19.hex) + } + + is NProfile -> { + Route.Profile(nip19.hex) + } + + is NNote -> { + Route.Note(nip19.hex) + } + + is NEvent -> { + val note = LocalCache.getOrCreateNote(nip19.hex) + // Only fall back to the pointer's own kind while the body is missing: once the + // event is cached it may belong somewhere the kind alone can't name (a channel, + // a chatroom), and routeFor knows that. + if (note.event == null) { + routeForPointer(nip19.kind, nip19.hex) ?: Route.EventRedirect(nip19.hex) + } else { + routeFor(note = note, loggedIn = account) ?: Route.EventRedirect(nip19.hex) + } + } + + is NAddress -> { + relayGroupDirectRoute(nip19, parsedNip19.additionalChars) + ?: routeFor( + note = LocalCache.getOrCreateAddressableNote(nip19.address()), + loggedIn = account, + ) ?: calendarDirectRoute(nip19) ?: Route.EventRedirect(nip19.aTag()) + } + + is NEmbed -> { + val noteEvent = nip19.event + if (noteEvent is AddressableEvent) { + routeFor( + note = LocalCache.getOrCreateAddressableNote(noteEvent.address()), + loggedIn = account, + ) ?: Route.EventRedirect(noteEvent.addressTag()) + } else { + routeFor( + note = LocalCache.getOrCreateNote(nip19.event.id), + loggedIn = account, + ) ?: Route.EventRedirect(nip19.event.id) + } + } + + else -> { + null + } + } + + if (route != null) { + return route + } + } + + if (isMarmotGroupRoute(uri)) { + // marmot:?account= + val groupHex = + uri + .removePrefix("marmot:") + .substringBefore("?") + .substringBefore("&") + if (groupHex.matches(MARMOT_HEX)) { + return Route.MarmotGroupChat(groupHex) + } + } + + if (isWalletConnectRoute(uri)) { + try { + val url = UriParser(uri) + val nip47Uri = url.getQueryParameter("value")?.firstOrNull() + if (nip47Uri != null) { + Nip47WalletConnect.parse(nip47Uri) + return Route.WalletAddNwc(nip47Uri) + } + } catch (e: Exception) { + if (e is CancellationException) throw e + } + } + + try { + Nip47WalletConnect.parse(uri) + return Route.WalletAddNwc(uri) + } catch (e: Exception) { + if (e is CancellationException) throw e + } + + return null +} + +/** + * Direct route for an `naddr` whose event hasn't arrived in [LocalCache] yet. When a notification + * is tapped (or a `nostr:naddr…` deep link arrives) for a calendar appointment we know is kind + * 31922/31923, route straight to the dedicated detail screen instead of bouncing through + * [Route.EventRedirect]. The detail screen issues its own per-event subscription, so the user + * sees the calendar-specific loading placeholder while the event arrives, not the generic + * redirect screen. + */ +private fun calendarDirectRoute(nip19: NAddress): Route? = + when (nip19.kind) { + CalendarTimeSlotEvent.KIND, + CalendarDateSlotEvent.KIND, + -> Route.CalendarEventDetail(nip19.kind, nip19.author, nip19.dTag) + else -> null + } + +/** + * Direct route for a NIP-29 group `naddr` (kind 39000) shared as a link. The group + * only exists on its host relay, so the naddr's relay hint is mandatory — the group + * id alone can't be resolved. With a hint we open the group chat straight away + * (cold start included); without one there's nowhere to look, so fall through. + */ +private fun relayGroupDirectRoute( + nip19: NAddress, + additionalChars: String?, +): Route? { + if (nip19.kind != GroupMetadataEvent.KIND) return null + val relay = nip19.relay.firstOrNull() ?: return null + // The spec allows an invite code appended as `naddr1…?invite=`; it arrives + // in the trailing chars after the bech32 body. Carry it so the group auto-joins. + val inviteCode = GroupNAddrInvite.parse(additionalChars) + return Route.RelayGroup(nip19.dTag, relay.url, inviteCode = inviteCode) +} + +/** + * Direct route for a NIP-29 group invite link in the de-facto `'[?code=]` + * form shared by Wisp and 0xchat (e.g. `wss://groups.0xchat.com'abc123`). The link carries its + * own host relay, so we can open the group straight away. Returns null for anything that isn't + * this exact shape, so it's safe to try before the nostr-entity parser. + */ +private fun relayGroupInviteRoute(uri: String): Route? { + val link = GroupInviteLink.parse(uri.removePrefix(NOSTR_URI_PREFIX)) ?: return null + return Route.RelayGroup(link.groupId, link.relayUrl.url, inviteCode = link.code) +} + +/** + * A shared Concord invite URL (`…/invite/#`). Cheap substring gates + * keep the parse off the hot path; the whole URL (fragment included) is carried into + * the route so the redeem flow still has the unlock token. + */ +private fun concordInviteRoute(uri: String): Route? = + if (uri.contains("/invite/") && uri.contains('#') && ConcordActions.parseInviteLink(uri) != null) { + Route.ConcordInvite(uri) + } else { + null + } + +/** + * A Buzz workspace invite (`https:///invite/`) — a plain `/invite/` https URL with + * no fragment, so disjoint from the Concord shape above. Opens the in-app join flow + * ([Route.BuzzInvite]) instead of the external browser so the claim signs with the user's key. + */ +private fun buzzInviteRoute(uri: String): Route? = + if (uri.contains("/invite/") && BuzzInviteLink.parse(uri) != null) { + Route.BuzzInvite(uri) + } else { + null + } 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 2531c35626..df27382ddf 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 @@ -1337,34 +1337,3 @@ fun URI.findParameterValue(parameterName: String): String? = Pair(name, value) }?.firstOrNull { it.first == parameterName } ?.second - -/** - * The value of [parameterName] in this URI's query string, without going through - * [java.net.URI]. - * - * Needed because `java.net.URI` only exposes `rawQuery` for *hierarchical* URIs. A URI - * with a scheme and no `//` is **opaque** — everything after the colon is one - * scheme-specific part — so `URI("marmot:?account=npub1…").rawQuery` is null. That - * is the shape [com.vitorpamplona.amethyst.service.notifications.NotificationRoutes.marmotUri] - * produces, so every Marmot group notification silently lost its `?account=` and opened - * the group under whichever account happened to be current instead of switching first. - * - * Splitting on the first `?` gets the same answer for both shapes, and returns null for a - * bare `nevent1…` with no query at all. - * - * [com.vitorpamplona.quartz.utils.UriParser] reads an opaque query correctly too, and is the - * right tool when a URI is already known to be well-formed. It is not this one: it builds a - * [java.net.URI], which *throws* on anything that is not a legal URI. What arrives here comes - * from an exported, browsable scheme, so it can be any string at all, and every caller on the - * deep-link path treats an unreadable uri as "no route" rather than as a crash. - */ -fun String.findQueryParameterValue(parameterName: String): String? { - val query = substringAfter('?', "") - if (query.isEmpty()) return null - - return query - .split('&') - .firstOrNull { it.substringBefore('=') == parameterName } - ?.substringAfter('=', "") - ?.ifEmpty { null } -} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/QueryParameters.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/QueryParameters.kt new file mode 100644 index 0000000000..d9699b7c70 --- /dev/null +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/QueryParameters.kt @@ -0,0 +1,52 @@ +/* + * 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.navigation + +/** + * The value of [parameterName] in this URI's query string, without going through + * `java.net.URI`. + * + * Needed because `java.net.URI` only exposes `rawQuery` for *hierarchical* URIs. A URI + * with a scheme and no `//` is **opaque** — everything after the colon is one + * scheme-specific part — so `URI("marmot:?account=npub1…").rawQuery` is null. That + * is the shape `NotificationRoutes.marmotUri` + * produces, so every Marmot group notification silently lost its `?account=` and opened + * the group under whichever account happened to be current instead of switching first. + * + * Splitting on the first `?` gets the same answer for both shapes, and returns null for a + * bare `nevent1…` with no query at all. + * + * `UriParser` reads an opaque query correctly too, and is the + * right tool when a URI is already known to be well-formed. It is not this one: it builds a + * `java.net.URI`, which *throws* on anything that is not a legal URI. What arrives here comes + * from an exported, browsable scheme, so it can be any string at all, and every caller on the + * deep-link path treats an unreadable uri as "no route" rather than as a crash. + */ +fun String.findQueryParameterValue(parameterName: String): String? { + val query = substringAfter('?', "") + if (query.isEmpty()) return null + + return query + .split('&') + .firstOrNull { it.substringBefore('=') == parameterName } + ?.substringAfter('=', "") + ?.ifEmpty { null } +} 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 6b17157a0b..bf0cacf966 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 @@ -43,7 +43,6 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp import androidx.lifecycle.compose.collectAsStateWithLifecycle -import com.vitorpamplona.amethyst.Amethyst import com.vitorpamplona.amethyst.commons.browser.OmniboxInput import com.vitorpamplona.amethyst.commons.favorites.FavoriteApp import com.vitorpamplona.amethyst.commons.favorites.FavoriteAppIcon @@ -55,6 +54,8 @@ import com.vitorpamplona.amethyst.commons.ui.insets.keyboardAsState import com.vitorpamplona.amethyst.commons.ui.layouts.LocalScreenLayout import com.vitorpamplona.amethyst.commons.ui.navigation.bottombars.LocalTabReselectCoordinator import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav +import com.vitorpamplona.amethyst.commons.ui.platform.LocalAppServices +import com.vitorpamplona.amethyst.commons.ui.platform.rememberNappletIconModel import com.vitorpamplona.amethyst.commons.ui.stringRes import com.vitorpamplona.amethyst.commons.ui.theme.DividerThickness import com.vitorpamplona.amethyst.commons.ui.theme.HorzPadding @@ -64,7 +65,6 @@ import com.vitorpamplona.amethyst.commons.ui.theme.Size25Modifier import com.vitorpamplona.amethyst.commons.ui.theme.Size27Modifier import com.vitorpamplona.amethyst.commons.ui.theme.onSurface65 import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel -import com.vitorpamplona.amethyst.favorites.rememberNappletIconModel /** Content height of the [AppBottomBar] (the 50.dp Column inside [RenderBottomMenu]), * exclusive of the system navigation-bar inset. Used by FAB callers that want to @@ -114,7 +114,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 (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 Amethyst.instance.favoriteApps.favorites + val favorites by LocalAppServices.current.favoriteApps.favorites .collectAsStateWithLifecycle() val isKeyboardState by keyboardAsState() @@ -134,10 +134,11 @@ internal fun rememberFavoriteIconModel(fav: FavoriteApp): Any? = when (fav) { is FavoriteApp.WebApp -> { // Captured favicons, keyed so the icon appears once the site's capture lands. - val iconKeys by Amethyst.instance.browserIcons.keys + val browserIcons = LocalAppServices.current.browserIcons + val iconKeys by browserIcons.keys .collectAsStateWithLifecycle() remember(fav, iconKeys) { - OmniboxInput.hostOf(fav.url)?.let(Amethyst.instance.browserIcons::iconModelFor) + OmniboxInput.hostOf(fav.url)?.let(browserIcons::iconModelFor) } } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/bottombars/GroupBottomBarEntries.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/bottombars/GroupBottomBarEntries.kt index 570e5518e9..bd252a75eb 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/bottombars/GroupBottomBarEntries.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/bottombars/GroupBottomBarEntries.kt @@ -44,10 +44,10 @@ import com.vitorpamplona.amethyst.commons.relayClient.reqCommand.channel.Channel import com.vitorpamplona.amethyst.commons.resources.Res import com.vitorpamplona.amethyst.commons.resources.concord_home_title import com.vitorpamplona.amethyst.commons.ui.components.RobohashFallbackAsyncImage +import com.vitorpamplona.amethyst.commons.ui.platform.rememberConcordImageModel import com.vitorpamplona.amethyst.commons.ui.screen.LocalDisplaySettings import com.vitorpamplona.amethyst.commons.ui.stringRes import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel -import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.concord.rememberConcordImageModel import com.vitorpamplona.quartz.nip01Core.relay.normalizer.RelayUrlNormalizer import com.vitorpamplona.quartz.nip01Core.relay.normalizer.displayUrl import com.vitorpamplona.quartz.nip29RelayGroups.GroupId diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/topbars/AroundMeLocationLabel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/topbars/AroundMeLocationLabel.kt index 9ebc663f23..dae3a52c12 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/topbars/AroundMeLocationLabel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/topbars/AroundMeLocationLabel.kt @@ -40,10 +40,10 @@ import com.vitorpamplona.amethyst.commons.resources.Res import com.vitorpamplona.amethyst.commons.resources.lack_location_permissions import com.vitorpamplona.amethyst.commons.resources.loading_location import com.vitorpamplona.amethyst.commons.ui.components.LoadingAnimation +import com.vitorpamplona.amethyst.commons.ui.note.platform.LoadCityName import com.vitorpamplona.amethyst.commons.ui.stringRes import com.vitorpamplona.amethyst.commons.ui.theme.Font12SP import com.vitorpamplona.amethyst.commons.ui.theme.StdHorzSpacer -import com.vitorpamplona.amethyst.ui.note.creators.location.LoadCityName /** The "around me" feed's subtitle: asks for location, then shows the city it resolved to. */ @OptIn(ExperimentalPermissionsApi::class) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/nip22Comments/GenericCommentPostScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/nip22Comments/GenericCommentPostScreen.kt index 387ff21249..492cf11b65 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/nip22Comments/GenericCommentPostScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/nip22Comments/GenericCommentPostScreen.kt @@ -95,6 +95,7 @@ import com.vitorpamplona.amethyst.commons.ui.note.creators.zapraiser.AddZapraise import com.vitorpamplona.amethyst.commons.ui.note.creators.zapraiser.ZapRaiserRequest import com.vitorpamplona.amethyst.commons.ui.note.creators.zapsplits.ForwardZapToButton import com.vitorpamplona.amethyst.commons.ui.note.nip22Comments.DisplayExternalId +import com.vitorpamplona.amethyst.commons.ui.note.platform.LoadCityName import com.vitorpamplona.amethyst.commons.ui.note.types.ReplyRenderType import com.vitorpamplona.amethyst.commons.ui.platform.GeohashLocationPickerDialog import com.vitorpamplona.amethyst.commons.ui.stringRes @@ -118,7 +119,6 @@ import com.vitorpamplona.amethyst.ui.note.creators.emojiSuggestions.WatchAndLoad import com.vitorpamplona.amethyst.ui.note.creators.expiration.ExpirationDatePicker import com.vitorpamplona.amethyst.ui.note.creators.invoice.InvoiceRequest import com.vitorpamplona.amethyst.ui.note.creators.location.GeoHashPostSection -import com.vitorpamplona.amethyst.ui.note.creators.location.LoadCityName import com.vitorpamplona.amethyst.ui.note.creators.messagefield.MessageField import com.vitorpamplona.amethyst.ui.note.creators.notify.AudienceFlap import com.vitorpamplona.amethyst.ui.note.creators.notify.AudienceSheet diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/share/ShareNoteAsQrScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/share/ShareNoteAsQrScreen.kt index 9044bcecb9..0932556399 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/share/ShareNoteAsQrScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/share/ShareNoteAsQrScreen.kt @@ -61,10 +61,10 @@ import com.vitorpamplona.amethyst.commons.resources.share_as_qr_mode_web import com.vitorpamplona.amethyst.commons.ui.components.LoadNote import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav import com.vitorpamplona.amethyst.commons.ui.navigation.topbars.TopBarWithBackButton +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.qrcode.KeepScreenBrightAndAwake +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.qrcode.QrCodeDrawer import com.vitorpamplona.amethyst.commons.ui.stringRes import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel -import com.vitorpamplona.amethyst.ui.screen.loggedIn.qrcode.KeepScreenBrightAndAwake -import com.vitorpamplona.amethyst.ui.screen.loggedIn.qrcode.QrCodeDrawer // A cap, not a fixed size: QrCodeDrawer's own quiet zone (QR_QUIET_ZONE_MODULES in QrCodeDrawer.kt) is a // fixed pixel count subtracted from raw size.width, so its share of the tile grows as density 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 f8c8cf0b36..093a411218 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 @@ -24,11 +24,11 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.ui.platform.LocalContext import com.vitorpamplona.amethyst.Amethyst +import com.vitorpamplona.amethyst.commons.favorites.favoriteCoordinateOf import com.vitorpamplona.amethyst.commons.model.Note import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav import com.vitorpamplona.amethyst.commons.ui.note.StaticWebsiteCard import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel -import com.vitorpamplona.amethyst.favorites.FavoriteAppLauncher import com.vitorpamplona.amethyst.napplet.NappletLauncher import com.vitorpamplona.amethyst.napplethost.HostProfile import com.vitorpamplona.amethyst.napplethost.NappletBlobPrefetcher @@ -68,7 +68,7 @@ fun RenderRootNappletEvent( } else { null }, - headerActions = { FavoriteToggleButton(FavoriteAppLauncher.coordinateOf(event), event.title() ?: "Napplet", event.icon()) }, + headerActions = { FavoriteToggleButton(favoriteCoordinateOf(event), event.title() ?: "Napplet", event.icon()) }, ) } @@ -98,7 +98,7 @@ fun RenderNamedNappletEvent( } else { null }, - headerActions = { FavoriteToggleButton(FavoriteAppLauncher.coordinateOf(event), event.title() ?: event.identifier(), event.icon()) }, + headerActions = { FavoriteToggleButton(favoriteCoordinateOf(event), event.title() ?: event.identifier(), event.icon()) }, ) } @@ -139,7 +139,7 @@ fun RenderRootSiteEvent( } else { null }, - headerActions = { FavoriteToggleButton(FavoriteAppLauncher.coordinateOf(event), event.title() ?: "nsite", event.icon()) }, + headerActions = { FavoriteToggleButton(favoriteCoordinateOf(event), event.title() ?: "nsite", event.icon()) }, ) } @@ -180,7 +180,7 @@ fun RenderNamedSiteEvent( } else { null }, - headerActions = { FavoriteToggleButton(FavoriteAppLauncher.coordinateOf(event), event.title() ?: event.identifier(), event.icon()) }, + headerActions = { FavoriteToggleButton(favoriteCoordinateOf(event), event.title() ?: event.identifier(), event.icon()) }, ) } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/platform/AndroidAppPlatform.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/platform/AndroidAppPlatform.kt index 47ad8b75a9..6c46a7b4af 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/platform/AndroidAppPlatform.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/platform/AndroidAppPlatform.kt @@ -20,18 +20,31 @@ */ package com.vitorpamplona.amethyst.ui.platform +import android.content.Context import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalContext +import com.vitorpamplona.amethyst.commons.favorites.FavoriteApp import com.vitorpamplona.amethyst.commons.model.navigation.Route import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav +import com.vitorpamplona.amethyst.commons.ui.platform.AppLauncher import com.vitorpamplona.amethyst.commons.ui.platform.AppPlatform +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.qrcode.ScanOutcome import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel +import com.vitorpamplona.amethyst.favorites.FavoriteAppLauncher import com.vitorpamplona.amethyst.ui.navigation.topbars.AndroidAroundMeLocationLabel +import com.vitorpamplona.quartz.concord.cord02Community.ImagePointer +import com.vitorpamplona.amethyst.favorites.rememberManifestIconModel as AppRememberManifestIconModel +import com.vitorpamplona.amethyst.favorites.rememberNappletIconModel as AppRememberNappletIconModel +import com.vitorpamplona.amethyst.favorites.rememberWebAppIconModel as AppRememberWebAppIconModel import com.vitorpamplona.amethyst.ui.navigation.bottombars.AppBottomBar as AppBottomBarImpl import com.vitorpamplona.amethyst.ui.note.creators.location.GeohashLocationPickerContent as AppGeohashLocationPickerContent import com.vitorpamplona.amethyst.ui.note.creators.location.GeohashLocationPickerDialog as AppGeohashLocationPickerDialog +import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.concord.rememberConcordImageModel as AppRememberConcordImageModel +import com.vitorpamplona.amethyst.ui.screen.loggedIn.qrcode.scanner.QrCodeScannerDialog as AppQrCodeScannerDialog -/** Android's [AppPlatform]: the app's own bottom bar and location label. */ +/** Android's [AppPlatform]: the app's own shell pieces, camera scanner, app launcher and icon caches. */ object AndroidAppPlatform : AppPlatform { @Composable override fun AppBottomBar( @@ -58,4 +71,48 @@ object AndroidAppPlatform : AppPlatform { onConfirm: (String) -> Unit, modifier: Modifier, ) = AppGeohashLocationPickerContent(initialGeohash, confirmLabel, onConfirm, modifier) + + @Composable + override fun QrCodeScannerDialog( + onDismiss: () -> Unit, + onScan: (String) -> ScanOutcome, + ) = AppQrCodeScannerDialog(onDismiss, onScan) + + @Composable + override fun rememberAppLauncher(): AppLauncher { + val context = LocalContext.current + return remember(context) { AndroidAppLauncher(context) } + } + + @Composable + override fun rememberConcordImageModel( + pointer: ImagePointer?, + accountViewModel: AccountViewModel, + ): String? = AppRememberConcordImageModel(pointer, accountViewModel) + + @Composable + override fun rememberNappletIconModel(coordinate: String): String? = AppRememberNappletIconModel(coordinate) + + @Composable + override fun rememberManifestIconModel( + author: String, + identifier: String, + ): String? = AppRememberManifestIconModel(author, identifier) + + @Composable + override fun rememberWebAppIconModel(url: String): String? = AppRememberWebAppIconModel(url) +} + +private class AndroidAppLauncher( + private val context: Context, +) : AppLauncher { + override fun launch( + app: FavoriteApp, + stillLoading: String, + ) = FavoriteAppLauncher.launch(context, app, stillLoading) + + override fun launchUrl( + url: String, + preferTor: Boolean, + ) = FavoriteAppLauncher.launchUrl(context, url, preferTor) } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/platform/AndroidAppServices.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/platform/AndroidAppServices.kt new file mode 100644 index 0000000000..8538e98df4 --- /dev/null +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/platform/AndroidAppServices.kt @@ -0,0 +1,49 @@ +/* + * 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.platform + +import com.vitorpamplona.amethyst.Amethyst +import com.vitorpamplona.amethyst.commons.browser.BrowserHistoryRegistry +import com.vitorpamplona.amethyst.commons.browser.BrowserIconRegistry +import com.vitorpamplona.amethyst.commons.connectedApps.signers.NostrSignerPermissionStore +import com.vitorpamplona.amethyst.commons.favorites.FavoriteAppsRegistry +import com.vitorpamplona.amethyst.commons.napplet.permissions.NappletPermissionLedger +import com.vitorpamplona.amethyst.commons.service.AppServices +import com.vitorpamplona.amethyst.commons.tor.TorSettingsFlow + +/** + * [AppServices] over the main process's app modules. Every member is a getter, so installing + * this object costs nothing and is safe in the `:napplet` process, where `Amethyst.instance` is + * never built, as long as nothing there reads it. + */ +object AndroidAppServices : AppServices { + override val favoriteApps: FavoriteAppsRegistry get() = Amethyst.instance.favoriteApps + + override val browserHistory: BrowserHistoryRegistry get() = Amethyst.instance.browserHistory + + override val browserIcons: BrowserIconRegistry get() = Amethyst.instance.browserIcons + + override val nappletPermissionLedger: NappletPermissionLedger get() = Amethyst.instance.nappletPermissionLedger + + override val signerPermissionStore: NostrSignerPermissionStore get() = Amethyst.instance.signerPermissionStore + + override val torSettings: TorSettingsFlow get() = Amethyst.instance.torPrefs.value +} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/UserFeedViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/UserFeedViewModel.kt index 953e7058cf..7f01e6ba76 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/UserFeedViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/UserFeedViewModel.kt @@ -100,24 +100,24 @@ open class UserFeedViewModel( } init { - Log.d("Init") { "${this.javaClass.simpleName}" } + Log.d("Init") { "${this::class.simpleName}" } viewModelScope.launch(Dispatchers.IO) { LocalCache.live.newEventBundles.collect { newNotes -> - Log.d("Rendering Metrics") { "Update feeds: ${this@UserFeedViewModel.javaClass.simpleName} with ${newNotes.size}" } + Log.d("Rendering Metrics") { "Update feeds: ${this@UserFeedViewModel::class.simpleName} with ${newNotes.size}" } invalidateData() } } viewModelScope.launch(Dispatchers.IO) { LocalCache.live.deletedEventBundles.collect { newNotes -> - Log.d("Rendering Metrics") { "Delete from feeds: ${this@UserFeedViewModel.javaClass.simpleName} with ${newNotes.size}" } + Log.d("Rendering Metrics") { "Delete from feeds: ${this@UserFeedViewModel::class.simpleName} with ${newNotes.size}" } invalidateData() } } } override fun onCleared() { - Log.d("Init") { "OnCleared: ${this.javaClass.simpleName}" } + Log.d("Init") { "OnCleared: ${this::class.simpleName}" } bundler.cancel() } } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/AndroidAccountViewModelHost.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/AndroidAccountViewModelHost.kt index a3f5135e7d..310308d505 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/AndroidAccountViewModelHost.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/AndroidAccountViewModelHost.kt @@ -29,16 +29,21 @@ import com.vitorpamplona.amethyst.commons.service.lnurl.LnurlHttpTransport import com.vitorpamplona.amethyst.commons.service.lnurl.OkHttpLnurlTransport import com.vitorpamplona.amethyst.commons.service.pow.PoWJobFailure import com.vitorpamplona.amethyst.commons.tor.MoneyOpRelayRouting +import com.vitorpamplona.amethyst.commons.tor.TorRelayEvaluation import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModelHost import com.vitorpamplona.amethyst.service.notifications.NotificationUtils.dismissNotificationForEvent import com.vitorpamplona.amethyst.ui.note.payViaIntent import com.vitorpamplona.quartz.nip01Core.core.HexKey import com.vitorpamplona.quartz.nip01Core.core.toHexKey +import com.vitorpamplona.quartz.nip01Core.relay.client.auth.RelayAuthSnapshot import com.vitorpamplona.quartz.nip01Core.relay.client.stats.RelayStats +import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl import com.vitorpamplona.quartz.nip01Core.relay.sockets.WebsocketBuilder import com.vitorpamplona.quartz.nip19Bech32.bech32.bechToBytes +import kotlinx.collections.immutable.PersistentMap import kotlinx.coroutines.CancellationException import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.onStart @@ -60,6 +65,11 @@ class AndroidAccountViewModelHost( override val moneyOpRelays: MoneyOpRelayRouting get() = modules.torEvaluatorFlow + override val torRelayEvaluation: StateFlow get() = modules.torEvaluatorFlow.flow + + override val relayAuthState: StateFlow> + get() = modules.authCoordinator.receiver.authStateFlow + override val savedAccounts: Flow> = LocalPreferences .accountsFlow() diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/backups/BackupConflictEventViews.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/backups/BackupConflictEventViews.kt index a081aa07f3..dda3ca3122 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/backups/BackupConflictEventViews.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/backups/BackupConflictEventViews.kt @@ -142,13 +142,13 @@ import com.vitorpamplona.amethyst.commons.ui.note.NoteCompose import com.vitorpamplona.amethyst.commons.ui.note.UserPicture import com.vitorpamplona.amethyst.commons.ui.note.UsernameDisplay import com.vitorpamplona.amethyst.commons.ui.note.elements.BannerImage +import com.vitorpamplona.amethyst.commons.ui.pluralStringRes import com.vitorpamplona.amethyst.commons.ui.screen.LocalDisplaySettings import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.chats.publicChannels.relayGroup.LoadRelayGroupChannel +import com.vitorpamplona.amethyst.commons.ui.stringRes import com.vitorpamplona.amethyst.commons.ui.theme.placeholderText import com.vitorpamplona.amethyst.commons.util.toShortDisplay import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel -import com.vitorpamplona.amethyst.ui.pluralStringRes -import com.vitorpamplona.amethyst.ui.stringRes import com.vitorpamplona.quartz.nip01Core.core.HexKey import com.vitorpamplona.quartz.nip01Core.diff.ContentChange import com.vitorpamplona.quartz.nip01Core.diff.ValueChange diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/backups/BackupConflictListViews.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/backups/BackupConflictListViews.kt index eba5ffc5f3..a6a6c76d64 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/backups/BackupConflictListViews.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/backups/BackupConflictListViews.kt @@ -117,12 +117,12 @@ import com.vitorpamplona.amethyst.commons.ui.note.LoadAddressableNote import com.vitorpamplona.amethyst.commons.ui.note.LoadPublicChatChannel import com.vitorpamplona.amethyst.commons.ui.note.LoadUser import com.vitorpamplona.amethyst.commons.ui.note.UsernameDisplay +import com.vitorpamplona.amethyst.commons.ui.note.platform.LoadCityName import com.vitorpamplona.amethyst.commons.ui.screen.LocalDisplaySettings import com.vitorpamplona.amethyst.commons.ui.stringRes import com.vitorpamplona.amethyst.commons.ui.theme.placeholderText import com.vitorpamplona.amethyst.commons.util.toShortDisplay import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel -import com.vitorpamplona.amethyst.ui.note.creators.location.LoadCityName import com.vitorpamplona.quartz.concord.cord02Community.ConcordCommunityListDiff import com.vitorpamplona.quartz.experimental.ephemChat.chat.RoomId import com.vitorpamplona.quartz.experimental.ephemChat.list.EphemeralChatListDiff diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/backups/BackupConflictPresentation.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/backups/BackupConflictPresentation.kt index 9980be0f5d..f807dba8d9 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/backups/BackupConflictPresentation.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/backups/BackupConflictPresentation.kt @@ -114,7 +114,7 @@ import com.vitorpamplona.amethyst.commons.resources.backup_type_trust_providers import com.vitorpamplona.amethyst.commons.resources.backup_type_trust_providers_explainer import com.vitorpamplona.amethyst.commons.resources.backup_type_trusted_relays import com.vitorpamplona.amethyst.commons.resources.backup_type_trusted_relays_explainer -import com.vitorpamplona.amethyst.ui.stringRes +import com.vitorpamplona.amethyst.commons.ui.stringRes import com.vitorpamplona.quartz.concord.cord02Community.ConcordCommunityListDiff import com.vitorpamplona.quartz.experimental.ephemChat.chat.RoomId import com.vitorpamplona.quartz.experimental.ephemChat.list.EphemeralChatListDiff 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 725fd312a9..f3219d3d6d 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 @@ -63,7 +63,6 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.Color -import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.text.TextRange import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.input.ImeAction @@ -74,7 +73,6 @@ import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp import androidx.lifecycle.compose.collectAsStateWithLifecycle import coil3.compose.AsyncImage -import com.vitorpamplona.amethyst.Amethyst import com.vitorpamplona.amethyst.commons.browser.BrowserHistoryEntry import com.vitorpamplona.amethyst.commons.browser.DefaultWebClients import com.vitorpamplona.amethyst.commons.browser.OmniboxInput @@ -82,6 +80,7 @@ import com.vitorpamplona.amethyst.commons.browser.OmniboxSuggestions import com.vitorpamplona.amethyst.commons.browser.SuggestedWebApp import com.vitorpamplona.amethyst.commons.favorites.FavoriteApp import com.vitorpamplona.amethyst.commons.favorites.FavoriteAppIcon +import com.vitorpamplona.amethyst.commons.favorites.favoriteCoordinateOf import com.vitorpamplona.amethyst.commons.icons.symbols.Icon import com.vitorpamplona.amethyst.commons.icons.symbols.MaterialSymbols import com.vitorpamplona.amethyst.commons.icons.symbols.rememberMaterialSymbolPainter @@ -89,6 +88,7 @@ import com.vitorpamplona.amethyst.commons.model.Note import com.vitorpamplona.amethyst.commons.model.cache.LocalCache import com.vitorpamplona.amethyst.commons.model.navigation.Route import com.vitorpamplona.amethyst.commons.resources.Res +import com.vitorpamplona.amethyst.commons.resources.browser_address_hint import com.vitorpamplona.amethyst.commons.resources.browser_clear import com.vitorpamplona.amethyst.commons.resources.browser_discover_napplets import com.vitorpamplona.amethyst.commons.resources.browser_discover_nsites @@ -104,21 +104,21 @@ import com.vitorpamplona.amethyst.commons.resources.favorite_app_still_loading import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav import com.vitorpamplona.amethyst.commons.ui.note.ArrowBackIcon import com.vitorpamplona.amethyst.commons.ui.platform.AppBottomBar +import com.vitorpamplona.amethyst.commons.ui.platform.LocalAppServices +import com.vitorpamplona.amethyst.commons.ui.platform.rememberAppLauncher +import com.vitorpamplona.amethyst.commons.ui.platform.rememberNappletIconModel import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.nsites.datasource.NsitesFilterAssemblerSubscription +import com.vitorpamplona.amethyst.commons.ui.stringRes import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel -import com.vitorpamplona.amethyst.favorites.FavoriteAppLauncher import com.vitorpamplona.amethyst.favorites.PreloadFavoriteNostrApps -import com.vitorpamplona.amethyst.favorites.rememberNappletIconModel import com.vitorpamplona.amethyst.ui.screen.loggedIn.favorites.favoriteAppItems import com.vitorpamplona.amethyst.ui.screen.loggedIn.napplets.datasource.NappletsFilterAssemblerSubscription -import com.vitorpamplona.amethyst.ui.stringRes import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter import com.vitorpamplona.quartz.nip5aStaticWebsites.NamedSiteEvent import com.vitorpamplona.quartz.nip5aStaticWebsites.RootSiteEvent import com.vitorpamplona.quartz.nip5dNapplets.NamedNappletEvent import com.vitorpamplona.quartz.nip5dNapplets.RootNappletEvent import org.jetbrains.compose.resources.StringResource -import com.vitorpamplona.amethyst.commons.R as CommonsR /** How many of the most recent history entries the idle browser home surfaces under "Recent". */ private const val RECENTS_LIMIT = 12 @@ -150,13 +150,14 @@ private fun BrowserLauncher( accountViewModel: AccountViewModel, nav: INav, ) { - val context = LocalContext.current + val appServices = LocalAppServices.current + val appLauncher = rememberAppLauncher() val appStillLoadingStr = stringRes(Res.string.favorite_app_still_loading) - val apps by Amethyst.instance.favoriteApps.favorites + val apps by appServices.favoriteApps.favorites .collectAsStateWithLifecycle() - val history by Amethyst.instance.browserHistory.history + val history by appServices.browserHistory.history .collectAsStateWithLifecycle() - val iconKeys by Amethyst.instance.browserIcons.keys + val iconKeys by appServices.browserIcons.keys .collectAsStateWithLifecycle() // Fetch favorited nsite/napplet manifests up front so tapping one launches immediately instead of @@ -227,7 +228,7 @@ private fun BrowserLauncher( fun open(text: String) { val target = OmniboxInput.resolve(text) ?: return - FavoriteAppLauncher.launchUrl(context, target.url, target.forceTor) + appLauncher.launchUrl(target.url, target.forceTor) } // Pin/unpin a plain web URL by its favorite id. Shared by the suggestion list and the Recent rows. @@ -236,10 +237,10 @@ private fun BrowserLauncher( label: String, ) { val id = "url:$url" - if (Amethyst.instance.favoriteApps.isFavorite(id)) { - Amethyst.instance.favoriteApps.remove(id) + if (appServices.favoriteApps.isFavorite(id)) { + appServices.favoriteApps.remove(id) } else { - Amethyst.instance.favoriteApps.add(FavoriteApp.WebApp(url, label.ifBlank { OmniboxInput.hostOf(url) ?: url }, System.currentTimeMillis())) + appServices.favoriteApps.add(FavoriteApp.WebApp(url, label.ifBlank { OmniboxInput.hostOf(url) ?: url }, System.currentTimeMillis())) } } @@ -291,7 +292,7 @@ private fun BrowserLauncher( historyUrls = historyUrls, onOpen = { open(it.url) }, onToggleFavorite = { toggleFavorite(it.url, it.label) }, - onRemoveFromHistory = { Amethyst.instance.browserHistory.remove(it) }, + onRemoveFromHistory = { appServices.browserHistory.remove(it) }, modifier = contentModifier, ) else -> { @@ -306,12 +307,12 @@ private fun BrowserLauncher( suggested = suggested, nsites = followedNsites, napplets = followedNapplets, - onOpenApp = { FavoriteAppLauncher.launch(context, it, appStillLoadingStr) }, - onRemoveApp = { Amethyst.instance.favoriteApps.remove(it.id) }, - onAddApp = { Amethyst.instance.favoriteApps.add(it) }, + onOpenApp = { appLauncher.launch(it, appStillLoadingStr) }, + onRemoveApp = { appServices.favoriteApps.remove(it.id) }, + onAddApp = { appServices.favoriteApps.add(it) }, onOpenUrl = { open(it) }, onToggleRecentFavorite = { entry -> toggleFavorite(entry.url, entry.title.ifBlank { entry.host }) }, - onRemoveRecent = { Amethyst.instance.browserHistory.remove(it) }, + onRemoveRecent = { appServices.browserHistory.remove(it) }, modifier = contentModifier, ) } @@ -348,7 +349,7 @@ private fun OmniBar( onValueChange = onValueChange, modifier = Modifier.weight(1f), singleLine = true, - placeholder = { Text(stringRes(CommonsR.string.browser_address_hint)) }, + placeholder = { Text(stringRes(Res.string.browser_address_hint)) }, keyboardOptions = KeyboardOptions( capitalization = KeyboardCapitalization.None, @@ -572,7 +573,8 @@ private fun SuggestedRow( onClick: () -> Unit, onAddFavorite: () -> Unit, ) { - val iconModel = remember(entry, iconKeys) { OmniboxInput.hostOf(entry.app.url)?.let(Amethyst.instance.browserIcons::iconModelFor) } + val appServices = LocalAppServices.current + val iconModel = remember(entry, iconKeys) { OmniboxInput.hostOf(entry.app.url)?.let(appServices.browserIcons::iconModelFor) } Row( modifier = Modifier @@ -644,7 +646,7 @@ private fun List.toDiscoverApps( /** Resolve a cached nsite/napplet manifest note into a launchable favorite + its description, or null. */ private fun Note.toDiscoverNostrApp(): DiscoverNostrApp? { val event = event ?: return null - val coordinate = FavoriteAppLauncher.coordinateOf(event) + val coordinate = favoriteCoordinateOf(event) val label: String val description: String? when (event) { @@ -803,7 +805,8 @@ private fun SiteIcon( iconKeys: Set, modifier: Modifier = Modifier, ) { - val model = remember(host, iconKeys) { Amethyst.instance.browserIcons.iconModelFor(host) } + val appServices = LocalAppServices.current + val model = remember(host, iconKeys) { appServices.browserIcons.iconModelFor(host) } val symbol = if (isFavorite) MaterialSymbols.Star else MaterialSymbols.Public val tint = MaterialTheme.colorScheme.onSurfaceVariant if (model == null) { diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/buzz/BuzzInviteScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/buzz/BuzzInviteScreen.kt index 2d77460d81..2eb9f1f022 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/buzz/BuzzInviteScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/buzz/BuzzInviteScreen.kt @@ -47,7 +47,6 @@ import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip -import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextOverflow @@ -69,9 +68,10 @@ import com.vitorpamplona.amethyst.commons.resources.buzz_invite_title import com.vitorpamplona.amethyst.commons.resources.buzz_invite_workspace import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav import com.vitorpamplona.amethyst.commons.ui.navigation.topbars.TopBarWithBackButton +import com.vitorpamplona.amethyst.commons.ui.platform.AppLauncher +import com.vitorpamplona.amethyst.commons.ui.platform.rememberAppLauncher import com.vitorpamplona.amethyst.commons.ui.stringRes import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel -import com.vitorpamplona.amethyst.favorites.FavoriteAppLauncher import com.vitorpamplona.quartz.buzz.invite.BuzzInviteLink import com.vitorpamplona.quartz.nip01Core.relay.normalizer.RelayUrlNormalizer import com.vitorpamplona.quartz.utils.TimeUtils @@ -85,7 +85,7 @@ import kotlinx.coroutines.launch * a flow the Buzz web app already implements and drives through `window.nostr`. So rather than * re-implement the legally-sensitive age/privacy consent natively, this confirms the workspace, * marks its relay as a Buzz dialect, and hands the URL to the in-app `window.nostr` browser - * ([FavoriteAppLauncher.launchUrl] → the sandboxed WebView), where the SPA signs the claim with + * ([AppLauncher.launchUrl] → the sandboxed WebView), where the SPA signs the claim with * the user's key. The user returns here (or to the app) once enrolled. */ @Composable @@ -95,7 +95,7 @@ fun BuzzInviteScreen( nav: INav, ) { val invite = remember(link) { BuzzInviteLink.parse(link) } - val context = LocalContext.current + val appLauncher = rememberAppLauncher() val scope = rememberCoroutineScope() var launched by remember { mutableStateOf(false) } @@ -201,7 +201,7 @@ fun BuzzInviteScreen( accountViewModel.account.buzzWorkspaces.join(relay) scope.launch { accountViewModel.account.relayAuthLedger.setDecision(relay.url, RelayAuthDecision.ALLOW) } } - FavoriteAppLauncher.launchUrl(context, link) + appLauncher.launchUrl(link) launched = true }, enabled = !expired, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/buzz/RelayStatusBar.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/buzz/RelayStatusBar.kt index 53371635cb..aa0db1c0a0 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/buzz/RelayStatusBar.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/buzz/RelayStatusBar.kt @@ -55,7 +55,6 @@ import androidx.compose.ui.text.font.FontFamily import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.dp import androidx.lifecycle.compose.collectAsStateWithLifecycle -import com.vitorpamplona.amethyst.Amethyst import com.vitorpamplona.amethyst.commons.icons.symbols.Icon import com.vitorpamplona.amethyst.commons.icons.symbols.MaterialSymbol import com.vitorpamplona.amethyst.commons.icons.symbols.MaterialSymbols @@ -88,7 +87,7 @@ fun RelayStatusBar( .connectedRelaysFlow() .collectAsStateWithLifecycle() val authState = - Amethyst.instance.authCoordinator.receiver.authStateFlow + accountViewModel.host.relayAuthState .collectAsStateWithLifecycle() val buzzState = BuzzRelayDialect.flow.collectAsStateWithLifecycle() val info by loadRelayInfo(relay) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/calendars/detail/CalendarEventDetailScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/calendars/detail/CalendarEventDetailScreen.kt index bce0ec65b3..9915e54662 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/calendars/detail/CalendarEventDetailScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/calendars/detail/CalendarEventDetailScreen.kt @@ -96,13 +96,13 @@ import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav import com.vitorpamplona.amethyst.commons.ui.note.ClickableUserPicture import com.vitorpamplona.amethyst.commons.ui.note.LoadUser import com.vitorpamplona.amethyst.commons.ui.note.UsernameDisplay +import com.vitorpamplona.amethyst.commons.ui.note.platform.ReactionsRow import com.vitorpamplona.amethyst.commons.ui.note.types.CalendarRsvpRow import com.vitorpamplona.amethyst.commons.ui.pluralStringRes import com.vitorpamplona.amethyst.commons.ui.stringRes import com.vitorpamplona.amethyst.commons.ui.theme.Size30dp import com.vitorpamplona.amethyst.commons.ui.theme.Size35dp import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel -import com.vitorpamplona.amethyst.ui.note.ReactionsRow import com.vitorpamplona.amethyst.ui.screen.loggedIn.calendars.addToPhoneCalendar import com.vitorpamplona.amethyst.ui.screen.loggedIn.calendars.datasource.CalendarsFilterAssemblerSubscription import com.vitorpamplona.amethyst.ui.screen.loggedIn.calendars.formatCalendarRange diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/cordnGroup/CordnGroupInfoScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/cordnGroup/CordnGroupInfoScreen.kt index fae0dec2d3..b51e0f970c 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/cordnGroup/CordnGroupInfoScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/cordnGroup/CordnGroupInfoScreen.kt @@ -132,12 +132,12 @@ import com.vitorpamplona.amethyst.commons.ui.note.creators.userSuggestions.ShowU import com.vitorpamplona.amethyst.commons.ui.note.creators.userSuggestions.UserSuggestionState import com.vitorpamplona.amethyst.commons.ui.pluralStringRes import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.chats.feed.types.observeUserNameByHex +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.qrcode.QrCodeDrawer import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.settings.cordn.CoordinatorIdentityRow import com.vitorpamplona.amethyst.commons.ui.stringRes import com.vitorpamplona.amethyst.commons.ui.theme.SuggestionListDefaultHeightChat import com.vitorpamplona.amethyst.commons.util.toShortDisplay import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel -import com.vitorpamplona.amethyst.ui.screen.loggedIn.qrcode.QrCodeDrawer import com.vitorpamplona.amethyst.ui.screen.loggedIn.settings.SectionCollapse import com.vitorpamplona.amethyst.ui.screen.loggedIn.settings.SectionExpand import com.vitorpamplona.quartz.cordn.spec00Coordinator.JoinRequest diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/geohashChat/GeohashChatScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/geohashChat/GeohashChatScreen.kt index b1da5855ba..5b1590c891 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/geohashChat/GeohashChatScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/geohashChat/GeohashChatScreen.kt @@ -73,9 +73,9 @@ import com.vitorpamplona.amethyst.commons.ui.layouts.DisappearingScaffold import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav import com.vitorpamplona.amethyst.commons.ui.navigation.topbars.SearchTopBarAction import com.vitorpamplona.amethyst.commons.ui.navigation.topbars.TopBarExtensibleWithBackButton +import com.vitorpamplona.amethyst.commons.ui.note.platform.LoadCityName import com.vitorpamplona.amethyst.commons.ui.screen.LocalDisplaySettings import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel -import com.vitorpamplona.amethyst.ui.note.creators.location.LoadCityName import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.feed.LocalChatActingIdentities import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.feed.LocalChatDisplayNameResolver import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.feed.LocalChatReactOverride diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/geohashChat/GeohashChatsScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/geohashChat/GeohashChatsScreen.kt index 3e53546877..e6652bca50 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/geohashChat/GeohashChatsScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/geohashChat/GeohashChatsScreen.kt @@ -69,10 +69,10 @@ import com.vitorpamplona.amethyst.commons.ui.layouts.DisappearingScaffold import com.vitorpamplona.amethyst.commons.ui.navigation.bottombars.FabBottomBarPadded import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav import com.vitorpamplona.amethyst.commons.ui.navigation.topbars.UserDrawerSearchTopBar +import com.vitorpamplona.amethyst.commons.ui.note.platform.LoadCityName import com.vitorpamplona.amethyst.commons.ui.platform.AppBottomBar import com.vitorpamplona.amethyst.commons.ui.stringRes import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel -import com.vitorpamplona.amethyst.ui.note.creators.location.LoadCityName import com.vitorpamplona.quartz.experimental.bitchat.geohash.GeohashChannelLevel import com.vitorpamplona.amethyst.commons.icons.symbols.Icon as SymbolIcon diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/geohashChat/NewGeohashChatScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/geohashChat/NewGeohashChatScreen.kt index eb54b6aac2..7e8c436be2 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/geohashChat/NewGeohashChatScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/geohashChat/NewGeohashChatScreen.kt @@ -67,8 +67,8 @@ import com.vitorpamplona.amethyst.commons.model.navigation.Route import com.vitorpamplona.amethyst.commons.ui.insets.imePaddingSafe import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav import com.vitorpamplona.amethyst.commons.ui.navigation.topbars.TopBarExtensibleWithBackButton +import com.vitorpamplona.amethyst.commons.ui.note.platform.LoadCityName import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel -import com.vitorpamplona.amethyst.ui.note.creators.location.LoadCityName import com.vitorpamplona.quartz.experimental.bitchat.geohash.GeohashChannelLevel import com.vitorpamplona.quartz.nip01Core.tags.geohash.GeoHash import com.vitorpamplona.amethyst.commons.icons.symbols.Icon as SymbolIcon diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/concord/ConcordChannelListScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/concord/ConcordChannelListScreen.kt index fcb92a4a6a..e76d675bbf 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/concord/ConcordChannelListScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/concord/ConcordChannelListScreen.kt @@ -110,9 +110,9 @@ import com.vitorpamplona.amethyst.commons.ui.note.timeAgo import com.vitorpamplona.amethyst.commons.ui.platform.AppBottomBar import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.chats.publicChannels.concord.datasource.ConcordChannelPreviewLoader import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.chats.publicChannels.concord.datasource.ConcordChannelSubscription +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.qrcode.QrCodeDrawer import com.vitorpamplona.amethyst.commons.ui.stringRes import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel -import com.vitorpamplona.amethyst.ui.screen.loggedIn.qrcode.QrCodeDrawer import com.vitorpamplona.quartz.concord.cord03Channels.ConcordChannelId import com.vitorpamplona.quartz.concord.cord04Roles.ConcordPermissions import com.vitorpamplona.quartz.nip01Core.core.HexKey diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/concord/ConcordHomeScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/concord/ConcordHomeScreen.kt index 01c517f2b3..64895fab11 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/concord/ConcordHomeScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/concord/ConcordHomeScreen.kt @@ -77,6 +77,7 @@ import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav import com.vitorpamplona.amethyst.commons.ui.navigation.topbars.ShorterTopAppBar import com.vitorpamplona.amethyst.commons.ui.note.timeAgo import com.vitorpamplona.amethyst.commons.ui.platform.AppBottomBar +import com.vitorpamplona.amethyst.commons.ui.platform.rememberConcordImageModel import com.vitorpamplona.amethyst.commons.ui.pluralStringRes import com.vitorpamplona.amethyst.commons.ui.screen.LocalDisplaySettings import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.chats.publicChannels.concord.datasource.ConcordChannelSubscription diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/concord/ConcordMetadataForm.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/concord/ConcordMetadataForm.kt index d715b3089e..970e331ae9 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/concord/ConcordMetadataForm.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/concord/ConcordMetadataForm.kt @@ -80,6 +80,7 @@ import com.vitorpamplona.amethyst.commons.ui.components.util.setText import com.vitorpamplona.amethyst.commons.ui.loadStringRes import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav import com.vitorpamplona.amethyst.commons.ui.note.RenderRelayIcon +import com.vitorpamplona.amethyst.commons.ui.platform.rememberConcordImageModel import com.vitorpamplona.amethyst.commons.ui.screen.LocalDisplaySettings import com.vitorpamplona.amethyst.commons.ui.stringRes import com.vitorpamplona.amethyst.commons.ui.theme.MediumRelayIconModifier diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip53LiveActivities/header/LiveStreamGoalHeader.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip53LiveActivities/header/LiveStreamGoalHeader.kt index 615928c14a..ae08a351ba 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip53LiveActivities/header/LiveStreamGoalHeader.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip53LiveActivities/header/LiveStreamGoalHeader.kt @@ -41,13 +41,13 @@ import com.vitorpamplona.amethyst.commons.model.Note import com.vitorpamplona.amethyst.commons.model.nip53LiveActivities.LiveActivitiesChannel import com.vitorpamplona.amethyst.commons.ui.components.LoadNote import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav +import com.vitorpamplona.amethyst.commons.ui.note.platform.ZapReaction import com.vitorpamplona.amethyst.commons.ui.note.types.GoalProgressBar import com.vitorpamplona.amethyst.commons.ui.theme.Size20Modifier import com.vitorpamplona.amethyst.commons.ui.theme.Size20dp import com.vitorpamplona.amethyst.commons.ui.theme.StdHorzSpacer import com.vitorpamplona.amethyst.commons.ui.theme.placeholderText import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel -import com.vitorpamplona.amethyst.ui.note.ZapReaction import com.vitorpamplona.quartz.nip75ZapGoals.ZapGoalEvent /** diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/relayGroup/RelayGroupMetadataScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/relayGroup/RelayGroupMetadataScreen.kt index 55720bc405..aff716d859 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/relayGroup/RelayGroupMetadataScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/relayGroup/RelayGroupMetadataScreen.kt @@ -108,13 +108,13 @@ import com.vitorpamplona.amethyst.commons.ui.insets.imePaddingSafe import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav import com.vitorpamplona.amethyst.commons.ui.navigation.topbars.CreatingTopBar import com.vitorpamplona.amethyst.commons.ui.navigation.topbars.SavingTopBar +import com.vitorpamplona.amethyst.commons.ui.note.platform.LoadCityName import com.vitorpamplona.amethyst.commons.ui.platform.GeohashLocationPickerDialog import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.chats.publicChannels.relayGroup.LoadRelayGroupChannel import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.chats.publicChannels.relayGroup.datasource.RelayGroupCardWarmupSubscription import com.vitorpamplona.amethyst.commons.ui.stringRes import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel import com.vitorpamplona.amethyst.ui.actions.uploads.GallerySelectSingle -import com.vitorpamplona.amethyst.ui.note.creators.location.LoadCityName import com.vitorpamplona.amethyst.ui.note.creators.location.LocationPreviewMap import com.vitorpamplona.quartz.nip01Core.relay.normalizer.RelayUrlNormalizer import com.vitorpamplona.quartz.nip01Core.tags.geohash.GeoHash diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/rooms/ChatroomHeaderCompose.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/rooms/ChatroomHeaderCompose.kt index bf4d44624d..90e207fcef 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/rooms/ChatroomHeaderCompose.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/rooms/ChatroomHeaderCompose.kt @@ -132,6 +132,7 @@ import com.vitorpamplona.amethyst.commons.ui.note.NonClickableUserPictures import com.vitorpamplona.amethyst.commons.ui.note.ObserveDraftEvent import com.vitorpamplona.amethyst.commons.ui.note.elements.TimeAgoStyle import com.vitorpamplona.amethyst.commons.ui.note.elements.ToggleableTimeAgoText +import com.vitorpamplona.amethyst.commons.ui.platform.rememberConcordImageModel import com.vitorpamplona.amethyst.commons.ui.screen.LocalDisplaySettings import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.chats.feed.types.observeUserNameByHex import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.chats.privateDM.header.RoomNameDisplay @@ -154,7 +155,6 @@ import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.marmotGroup.marmotOth import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.marmotGroup.rememberMarmotGroupAvatarUrl import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.privateDM.header.reportWarningContentDescription import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.concord.ConcordLeaveDialog -import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.concord.rememberConcordImageModel import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.ephemChat.LoadEphemeralChatChannel import com.vitorpamplona.quartz.buzz.notifications.MemberAddedNotificationEvent import com.vitorpamplona.quartz.cordn.appEncryptedMedia.CordnMediaTag diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip28Chats/RenderPublicChatChannelThumb.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip28Chats/RenderPublicChatChannelThumb.kt index c4314574a4..c4e82b4bc5 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip28Chats/RenderPublicChatChannelThumb.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip28Chats/RenderPublicChatChannelThumb.kt @@ -59,6 +59,8 @@ import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav import com.vitorpamplona.amethyst.commons.ui.note.DisplayAuthorBanner import com.vitorpamplona.amethyst.commons.ui.note.Gallery import com.vitorpamplona.amethyst.commons.ui.note.LoadPublicChatChannel +import com.vitorpamplona.amethyst.commons.ui.note.platform.LikeReaction +import com.vitorpamplona.amethyst.commons.ui.note.platform.ZapReaction import com.vitorpamplona.amethyst.commons.ui.stringRes import com.vitorpamplona.amethyst.commons.ui.theme.HalfTopPadding import com.vitorpamplona.amethyst.commons.ui.theme.QuoteBorder @@ -67,8 +69,6 @@ import com.vitorpamplona.amethyst.commons.ui.theme.StdHorzSpacer import com.vitorpamplona.amethyst.commons.ui.theme.grayText import com.vitorpamplona.amethyst.commons.util.equalImmutableLists import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel -import com.vitorpamplona.amethyst.ui.note.LikeReaction -import com.vitorpamplona.amethyst.ui.note.ZapReaction import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.persistentListOf import kotlinx.collections.immutable.toImmutableList diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip72Communities/CommunityCard.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip72Communities/CommunityCard.kt index bfc457075a..6bca972c92 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip72Communities/CommunityCard.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip72Communities/CommunityCard.kt @@ -60,6 +60,8 @@ import com.vitorpamplona.amethyst.commons.ui.layouts.LeftPictureLayout import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav import com.vitorpamplona.amethyst.commons.ui.note.DisplayAuthorBanner import com.vitorpamplona.amethyst.commons.ui.note.Gallery +import com.vitorpamplona.amethyst.commons.ui.note.platform.LikeReaction +import com.vitorpamplona.amethyst.commons.ui.note.platform.ZapReaction import com.vitorpamplona.amethyst.commons.ui.stringRes import com.vitorpamplona.amethyst.commons.ui.theme.HalfTopPadding import com.vitorpamplona.amethyst.commons.ui.theme.QuoteBorder @@ -68,8 +70,6 @@ import com.vitorpamplona.amethyst.commons.ui.theme.StdHorzSpacer import com.vitorpamplona.amethyst.commons.ui.theme.grayText import com.vitorpamplona.amethyst.commons.util.equalImmutableLists import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel -import com.vitorpamplona.amethyst.ui.note.LikeReaction -import com.vitorpamplona.amethyst.ui.note.ZapReaction import com.vitorpamplona.quartz.nip01Core.core.HexKey import com.vitorpamplona.quartz.nip72ModCommunities.definition.CommunityDefinitionEvent import kotlinx.collections.immutable.ImmutableList diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip90DVMs/DVMCard.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip90DVMs/DVMCard.kt index 14ff0c3b40..421ee5f46b 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip90DVMs/DVMCard.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip90DVMs/DVMCard.kt @@ -46,6 +46,8 @@ import com.vitorpamplona.amethyst.commons.ui.components.MyAsyncImage import com.vitorpamplona.amethyst.commons.ui.layouts.LeftPictureLayout import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav import com.vitorpamplona.amethyst.commons.ui.note.elements.BannerImage +import com.vitorpamplona.amethyst.commons.ui.note.platform.LikeReaction +import com.vitorpamplona.amethyst.commons.ui.note.platform.ZapReaction import com.vitorpamplona.amethyst.commons.ui.theme.HalfTopPadding import com.vitorpamplona.amethyst.commons.ui.theme.RowColSpacing5dp import com.vitorpamplona.amethyst.commons.ui.theme.SimpleImageBorder @@ -54,8 +56,6 @@ import com.vitorpamplona.amethyst.commons.ui.theme.bitcoinColor import com.vitorpamplona.amethyst.commons.ui.theme.grayText import com.vitorpamplona.amethyst.commons.ui.theme.nip05 import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel -import com.vitorpamplona.amethyst.ui.note.LikeReaction -import com.vitorpamplona.amethyst.ui.note.ZapReaction import com.vitorpamplona.amethyst.ui.screen.loggedIn.dvms.FavoriteAlgoFeedToggle import com.vitorpamplona.amethyst.ui.screen.loggedIn.dvms.observeAppDefinition diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip99Classifieds/ClassifiedsThumb.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip99Classifieds/ClassifiedsThumb.kt index cd518375cc..d39c806415 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip99Classifieds/ClassifiedsThumb.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip99Classifieds/ClassifiedsThumb.kt @@ -54,6 +54,7 @@ import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel import com.vitorpamplona.amethyst.commons.viewmodels.mockAccountViewModel import com.vitorpamplona.quartz.nip99Classifieds.ClassifiedsEvent import com.vitorpamplona.quartz.nip99Classifieds.tags.PriceTag +import com.vitorpamplona.quartz.utils.parseBigDecimalOrNull @Immutable data class ClassifiedsThumb( @@ -141,7 +142,7 @@ fun InnerRenderClassifiedsThumb( card.price?.let { val priceTag = remember(card) { - val newAmount = it.amount.toBigDecimalOrNull()?.let { showAmountInteger(it) } ?: it.amount + val newAmount = parseBigDecimalOrNull(it.amount)?.let { showAmountInteger(it) } ?: it.amount if (it.frequency != null && it.currency != null) { "$newAmount ${it.currency}/${it.frequency}" 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 a4c51199df..374957bf64 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 @@ -54,12 +54,10 @@ import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip -import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp import androidx.lifecycle.compose.collectAsStateWithLifecycle -import com.vitorpamplona.amethyst.Amethyst import com.vitorpamplona.amethyst.commons.browser.OmniboxInput import com.vitorpamplona.amethyst.commons.favorites.FavoriteApp import com.vitorpamplona.amethyst.commons.favorites.FavoriteAppIcon @@ -73,11 +71,12 @@ import com.vitorpamplona.amethyst.commons.resources.favorite_apps import com.vitorpamplona.amethyst.commons.resources.favorite_apps_empty import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav import com.vitorpamplona.amethyst.commons.ui.platform.AppBottomBar +import com.vitorpamplona.amethyst.commons.ui.platform.LocalAppServices +import com.vitorpamplona.amethyst.commons.ui.platform.rememberAppLauncher +import com.vitorpamplona.amethyst.commons.ui.platform.rememberNappletIconModel import com.vitorpamplona.amethyst.commons.ui.stringRes import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel -import com.vitorpamplona.amethyst.favorites.FavoriteAppLauncher import com.vitorpamplona.amethyst.favorites.PreloadFavoriteNostrApps -import com.vitorpamplona.amethyst.favorites.rememberNappletIconModel /** * The Favorite Apps grid: the user's pinned web clients / nsites / napplets as big launch buttons. @@ -91,9 +90,10 @@ fun FavoriteAppsScreen( accountViewModel: AccountViewModel, nav: INav, ) { + val appServices = LocalAppServices.current val appStillLoadingStr = stringRes(Res.string.favorite_app_still_loading) - val context = LocalContext.current - val apps by Amethyst.instance.favoriteApps.favorites + val appLauncher = rememberAppLauncher() + val apps by appServices.favoriteApps.favorites .collectAsStateWithLifecycle() // Fetch favorited nsite/napplet manifests up front so a tap launches immediately instead of showing @@ -125,8 +125,8 @@ fun FavoriteAppsScreen( } else { FavoriteAppsGrid( apps = apps, - onOpen = { FavoriteAppLauncher.launch(context, it, appStillLoadingStr) }, - onRemove = { Amethyst.instance.favoriteApps.remove(it.id) }, + onOpen = { appLauncher.launch(it, appStillLoadingStr) }, + onRemove = { appServices.favoriteApps.remove(it.id) }, modifier = Modifier .fillMaxSize() @@ -184,18 +184,19 @@ internal fun FavoriteAppCell( onOpen: () -> Unit, onRemove: () -> Unit, ) { + val appServices = LocalAppServices.current var menuOpen by remember { mutableStateOf(false) } // For a plain web favorite, prefer the favicon captured when its site was opened; an nsite/napplet uses // the verified icon blob bundled in its own content. Observing the key set recomputes the model as a // captured favicon arrives. - val iconKeys by Amethyst.instance.browserIcons.keys + val iconKeys by appServices.browserIcons.keys .collectAsStateWithLifecycle() val faviconModel = when (app) { is FavoriteApp.WebApp -> remember(app, iconKeys) { - OmniboxInput.hostOf(app.url)?.let(Amethyst.instance.browserIcons::iconModelFor) + OmniboxInput.hostOf(app.url)?.let(appServices.browserIcons::iconModelFor) } is FavoriteApp.NostrApp -> rememberNappletIconModel(app.coordinate) } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/followPacks/feed/FollowPackFeedScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/followPacks/feed/FollowPackFeedScreen.kt index bc55ce3516..7329448b51 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/followPacks/feed/FollowPackFeedScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/followPacks/feed/FollowPackFeedScreen.kt @@ -69,6 +69,8 @@ import com.vitorpamplona.amethyst.commons.ui.navigation.topbars.ShorterTopAppBar import com.vitorpamplona.amethyst.commons.ui.navigation.topbars.TitleIconModifier import com.vitorpamplona.amethyst.commons.ui.navigation.topbars.TopBarSize import com.vitorpamplona.amethyst.commons.ui.note.LoadAddressableNote +import com.vitorpamplona.amethyst.commons.ui.note.platform.LikeReaction +import com.vitorpamplona.amethyst.commons.ui.note.platform.ZapReaction import com.vitorpamplona.amethyst.commons.ui.screen.RefresheableFeedView import com.vitorpamplona.amethyst.commons.ui.stringRes import com.vitorpamplona.amethyst.commons.ui.theme.HalfHorzSpacer @@ -76,9 +78,7 @@ import com.vitorpamplona.amethyst.commons.ui.theme.Size18Modifier import com.vitorpamplona.amethyst.commons.ui.theme.SpacedBy2dp import com.vitorpamplona.amethyst.commons.ui.theme.TabRowHeight import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel -import com.vitorpamplona.amethyst.ui.note.LikeReaction import com.vitorpamplona.amethyst.ui.note.ReplyReaction -import com.vitorpamplona.amethyst.ui.note.ZapReaction import com.vitorpamplona.amethyst.ui.screen.UserFeedView import com.vitorpamplona.amethyst.ui.screen.loggedIn.followPacks.feed.dal.FollowPackFeedConversationsFeedViewModel import com.vitorpamplona.amethyst.ui.screen.loggedIn.followPacks.feed.dal.FollowPackFeedNewThreadFeedViewModel diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/geocaches/create/NewGeocacheScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/geocaches/create/NewGeocacheScreen.kt index 45f447610e..6e244bc1e9 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/geocaches/create/NewGeocacheScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/geocaches/create/NewGeocacheScreen.kt @@ -106,10 +106,10 @@ import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav import com.vitorpamplona.amethyst.commons.ui.note.geocacheEmoji import com.vitorpamplona.amethyst.commons.ui.note.geocacheLabelRes import com.vitorpamplona.amethyst.commons.ui.platform.GeohashLocationPickerDialog +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.qrcode.QrCodeDrawer import com.vitorpamplona.amethyst.commons.ui.stringRes import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel import com.vitorpamplona.amethyst.ui.note.creators.location.LocationPreviewMap -import com.vitorpamplona.amethyst.ui.screen.loggedIn.qrcode.QrCodeDrawer import com.vitorpamplona.quartz.nip01Core.tags.geohash.toGeoHash import com.vitorpamplona.quartz.nipCCGeocaching.listing.GeocacheGeohash import com.vitorpamplona.quartz.nipCCGeocaching.listing.tags.CacheSize diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/geocaches/detail/GeocacheOwnerActions.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/geocaches/detail/GeocacheOwnerActions.kt index 11d10a89a2..f69a4d5c89 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/geocaches/detail/GeocacheOwnerActions.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/geocaches/detail/GeocacheOwnerActions.kt @@ -60,9 +60,9 @@ import com.vitorpamplona.amethyst.commons.resources.geocache_owner_no_finders import com.vitorpamplona.amethyst.commons.resources.geocache_owner_qr_warning import com.vitorpamplona.amethyst.commons.resources.geocache_owner_show_qr import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.qrcode.QrCodeDrawer import com.vitorpamplona.amethyst.commons.ui.stringRes import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel -import com.vitorpamplona.amethyst.ui.screen.loggedIn.qrcode.QrCodeDrawer import com.vitorpamplona.quartz.nip01Core.core.Address import com.vitorpamplona.quartz.nipCCGeocaching.firstToFind.FirstToFindResolver import com.vitorpamplona.quartz.nipCCGeocaching.listing.GeocacheListingEvent diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/geohash/GeoHashScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/geohash/GeoHashScreen.kt index 1c42247bbd..b9f214640e 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/geohash/GeoHashScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/geohash/GeoHashScreen.kt @@ -45,10 +45,10 @@ import com.vitorpamplona.amethyst.commons.ui.navigation.bottombars.FabBottomBarP import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav import com.vitorpamplona.amethyst.commons.ui.navigation.topbars.SearchTopBarAction import com.vitorpamplona.amethyst.commons.ui.navigation.topbars.TopBarExtensibleWithBackButton +import com.vitorpamplona.amethyst.commons.ui.note.platform.LoadCityName import com.vitorpamplona.amethyst.commons.ui.screen.RefresheableFeedView import com.vitorpamplona.amethyst.commons.ui.stringRes import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel -import com.vitorpamplona.amethyst.ui.note.creators.location.LoadCityName import com.vitorpamplona.amethyst.ui.screen.loggedIn.geohash.dal.GeoHashFeedViewModel import com.vitorpamplona.amethyst.ui.screen.loggedIn.geohash.datasource.GeoHashFilterAssemblerSubscription import com.vitorpamplona.amethyst.commons.icons.symbols.Icon as SymbolIcon diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/gitRepo/GitRepositoryHomeUi.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/gitRepo/GitRepositoryHomeUi.kt index fdb89b6ed8..7952419dc3 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/gitRepo/GitRepositoryHomeUi.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/gitRepo/GitRepositoryHomeUi.kt @@ -70,11 +70,11 @@ import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav import com.vitorpamplona.amethyst.commons.ui.navigation.routes.routeFor import com.vitorpamplona.amethyst.commons.ui.note.ClickableUserPicture import com.vitorpamplona.amethyst.commons.ui.note.elements.TimeAgo +import com.vitorpamplona.amethyst.commons.ui.note.platform.ReactionsRow import com.vitorpamplona.amethyst.commons.ui.stringRes import com.vitorpamplona.amethyst.commons.ui.theme.DividerThickness import com.vitorpamplona.amethyst.commons.ui.theme.grayText import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel -import com.vitorpamplona.amethyst.ui.note.ReactionsRow import com.vitorpamplona.quartz.nip01Core.core.Event import com.vitorpamplona.quartz.nip34Git.git.GitCommit import com.vitorpamplona.quartz.nip34Git.issue.GitIssueEvent diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/live/RenderGeohashBubble.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/live/RenderGeohashBubble.kt index 1d3aa3d9fa..430fa01877 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/live/RenderGeohashBubble.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/live/RenderGeohashBubble.kt @@ -34,9 +34,9 @@ import com.vitorpamplona.amethyst.commons.icons.symbols.MaterialSymbols import com.vitorpamplona.amethyst.commons.model.geohashChat.GeohashChatChannel import com.vitorpamplona.amethyst.commons.model.navigation.Route import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav +import com.vitorpamplona.amethyst.commons.ui.note.platform.LoadCityName import com.vitorpamplona.amethyst.commons.ui.theme.StdHorzSpacer import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel -import com.vitorpamplona.amethyst.ui.note.creators.location.LoadCityName import com.vitorpamplona.amethyst.commons.icons.symbols.Icon as SymbolIcon /** diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/keyBackup/AccountBackupScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/keyBackup/AccountBackupScreen.kt index 804ce0e0ef..eae4d35095 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/keyBackup/AccountBackupScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/keyBackup/AccountBackupScreen.kt @@ -136,6 +136,7 @@ import com.vitorpamplona.amethyst.commons.ui.navigation.navs.EmptyNav import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav import com.vitorpamplona.amethyst.commons.ui.navigation.topbars.TopBarWithBackButton import com.vitorpamplona.amethyst.commons.ui.pluralStringRes +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.qrcode.QrCodeDrawer import com.vitorpamplona.amethyst.commons.ui.stringRes import com.vitorpamplona.amethyst.commons.ui.theme.ThemeComparisonRow import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel @@ -143,7 +144,6 @@ import com.vitorpamplona.amethyst.commons.viewmodels.mockAccountViewModel import com.vitorpamplona.amethyst.ui.note.authenticate import com.vitorpamplona.amethyst.ui.note.rememberAuthPromptLabels import com.vitorpamplona.amethyst.ui.screen.loggedIn.qrcode.BackButton -import com.vitorpamplona.amethyst.ui.screen.loggedIn.qrcode.QrCodeDrawer import com.vitorpamplona.quartz.nip19Bech32.toNsec import com.vitorpamplona.quartz.nip49PrivKeyEnc.Nip49 import kotlinx.coroutines.CoroutineScope diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/napplets/ConnectedAppDetailScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/napplets/ConnectedAppDetailScreen.kt index 6a6b38d0c7..1763fa96a3 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/napplets/ConnectedAppDetailScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/napplets/ConnectedAppDetailScreen.kt @@ -114,9 +114,9 @@ import com.vitorpamplona.amethyst.commons.resources.nip46_signer_reconnecting import com.vitorpamplona.amethyst.commons.resources.nip46_signer_remote_app import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav import com.vitorpamplona.amethyst.commons.ui.navigation.topbars.TopBarWithBackButton +import com.vitorpamplona.amethyst.commons.ui.platform.rememberManifestIconModel +import com.vitorpamplona.amethyst.commons.ui.platform.rememberWebAppIconModel import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel -import com.vitorpamplona.amethyst.favorites.rememberManifestIconModel -import com.vitorpamplona.amethyst.favorites.rememberWebAppIconModel import com.vitorpamplona.amethyst.napplet.NappletBrokerService import com.vitorpamplona.amethyst.napplet.WebSitePermissionRegistry import com.vitorpamplona.amethyst.napplet.counterpartyLabel diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/napplets/ConnectedAppsScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/napplets/ConnectedAppsScreen.kt index 25c7625df0..c7f9661616 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/napplets/ConnectedAppsScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/napplets/ConnectedAppsScreen.kt @@ -52,7 +52,6 @@ import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp import androidx.lifecycle.compose.collectAsStateWithLifecycle -import com.vitorpamplona.amethyst.Amethyst import com.vitorpamplona.amethyst.commons.connectedApps.nip46.Nip46PermissionAuthorizer import com.vitorpamplona.amethyst.commons.connectedApps.signers.AppSignerPolicy import com.vitorpamplona.amethyst.commons.connectedApps.signers.NostrSignerPermissionLedger @@ -69,13 +68,15 @@ import com.vitorpamplona.amethyst.commons.resources.napplet_permissions_title import com.vitorpamplona.amethyst.commons.resources.napplet_policy_full_trust import com.vitorpamplona.amethyst.commons.resources.napplet_policy_paranoid import com.vitorpamplona.amethyst.commons.resources.napplet_policy_reasonable +import com.vitorpamplona.amethyst.commons.resources.napplet_untitled import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav import com.vitorpamplona.amethyst.commons.ui.navigation.topbars.TopBarWithBackButton +import com.vitorpamplona.amethyst.commons.ui.platform.LocalAppServices +import com.vitorpamplona.amethyst.commons.ui.platform.rememberManifestIconModel +import com.vitorpamplona.amethyst.commons.ui.platform.rememberWebAppIconModel +import com.vitorpamplona.amethyst.commons.ui.stringRes import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel -import com.vitorpamplona.amethyst.favorites.rememberManifestIconModel -import com.vitorpamplona.amethyst.favorites.rememberWebAppIconModel import com.vitorpamplona.amethyst.ui.screen.loggedIn.napplets.datasource.ConnectedAppsFilterAssemblerSubscription -import com.vitorpamplona.amethyst.ui.stringRes import com.vitorpamplona.quartz.nip01Core.core.Event import com.vitorpamplona.quartz.nip01Core.core.HexKey import com.vitorpamplona.quartz.nip19Bech32.entities.NPub @@ -86,7 +87,6 @@ import com.vitorpamplona.quartz.nip5dNapplets.NappletManifest import com.vitorpamplona.quartz.nip5dNapplets.RootNappletEvent import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext -import com.vitorpamplona.amethyst.commons.R as CommonsR /** Author placeholder used by the browser permission path — not a real pubkey. */ private const val BROWSER_AUTHOR = "browser" @@ -101,8 +101,9 @@ fun ConnectedAppsScreen( accountViewModel: AccountViewModel, nav: INav, ) { - val capabilityLedger = Amethyst.instance.nappletPermissionLedger - val signerLedger = remember { NostrSignerPermissionLedger(Amethyst.instance.signerPermissionStore) } + val appServices = LocalAppServices.current + val capabilityLedger = appServices.nappletPermissionLedger + val signerLedger = remember { NostrSignerPermissionLedger(appServices.signerPermissionStore) } var items by remember { mutableStateOf?>(null) } var nappletAuthors by remember { mutableStateOf>(emptySet()) } @@ -156,7 +157,7 @@ fun ConnectedAppsScreen( } else -> { - val untitled = stringRes(CommonsR.string.napplet_untitled) + val untitled = stringRes(Res.string.napplet_untitled) LazyColumn( modifier = Modifier.fillMaxSize().padding(padding), contentPadding = PaddingValues(16.dp), diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/nests/NestsFeedLoaded.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/nests/NestsFeedLoaded.kt index 86d189d6b6..b85b8f9380 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/nests/NestsFeedLoaded.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/nests/NestsFeedLoaded.kt @@ -91,6 +91,8 @@ import com.vitorpamplona.amethyst.commons.ui.note.LongPressToQuickAction import com.vitorpamplona.amethyst.commons.ui.note.UserPicture import com.vitorpamplona.amethyst.commons.ui.note.UsernameDisplay import com.vitorpamplona.amethyst.commons.ui.note.elements.MoreOptionsButton +import com.vitorpamplona.amethyst.commons.ui.note.platform.LikeReaction +import com.vitorpamplona.amethyst.commons.ui.note.platform.ZapReaction import com.vitorpamplona.amethyst.commons.ui.note.timeAgoNoDot import com.vitorpamplona.amethyst.commons.ui.stringRes import com.vitorpamplona.amethyst.commons.ui.theme.DividerThickness @@ -105,8 +107,6 @@ import com.vitorpamplona.amethyst.commons.ui.theme.StdHorzSpacer import com.vitorpamplona.amethyst.commons.ui.theme.StdPadding import com.vitorpamplona.amethyst.commons.ui.theme.grayText import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel -import com.vitorpamplona.amethyst.ui.note.LikeReaction -import com.vitorpamplona.amethyst.ui.note.ZapReaction import com.vitorpamplona.amethyst.ui.screen.loggedIn.discover.nip53LiveActivities.LoadParticipants import com.vitorpamplona.amethyst.ui.screen.loggedIn.nests.room.activity.NestActivity import com.vitorpamplona.quartz.nip01Core.core.Address diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/pictures/PictureCardCompose.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/pictures/PictureCardCompose.kt index ccd21a2374..cd3a8ed3d5 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/pictures/PictureCardCompose.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/pictures/PictureCardCompose.kt @@ -49,9 +49,9 @@ import com.vitorpamplona.amethyst.commons.ui.components.ContentWarningGate import com.vitorpamplona.amethyst.commons.ui.components.collectContentWarningReasons import com.vitorpamplona.amethyst.commons.ui.components.mediaSizingModifier import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav +import com.vitorpamplona.amethyst.commons.ui.note.platform.ReactionsRow import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel import com.vitorpamplona.amethyst.ui.components.ZoomableContentView -import com.vitorpamplona.amethyst.ui.note.ReactionsRow import com.vitorpamplona.amethyst.ui.screen.loggedIn.video.UserCardHeader import com.vitorpamplona.quartz.nip36SensitiveContent.isSensitiveOrNSFW import com.vitorpamplona.quartz.nip68Picture.PictureEvent diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/podcasts/PodcastEpisodeListItem.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/podcasts/PodcastEpisodeListItem.kt index 3d74d3d04d..f6a1dd63f6 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/podcasts/PodcastEpisodeListItem.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/podcasts/PodcastEpisodeListItem.kt @@ -44,13 +44,13 @@ import com.vitorpamplona.amethyst.commons.resources.podcast_season import com.vitorpamplona.amethyst.commons.resources.podcast_season_episode import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav import com.vitorpamplona.amethyst.commons.ui.navigation.routes.routeFor +import com.vitorpamplona.amethyst.commons.ui.note.platform.ReactionsRow import com.vitorpamplona.amethyst.commons.ui.note.rememberTimeAgoLabels import com.vitorpamplona.amethyst.commons.ui.note.timeAgoWith import com.vitorpamplona.amethyst.commons.ui.stringRes import com.vitorpamplona.amethyst.commons.ui.theme.Size5dp import com.vitorpamplona.amethyst.commons.ui.theme.grayText import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel -import com.vitorpamplona.amethyst.ui.note.ReactionsRow import com.vitorpamplona.amethyst.ui.note.types.PodcastEpisodeAudioPlayer import com.vitorpamplona.quartz.podcasts.PodcastEpisode diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/podcasts/PodcastHeader.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/podcasts/PodcastHeader.kt index 1d92d618ce..16432a19d7 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/podcasts/PodcastHeader.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/podcasts/PodcastHeader.kt @@ -44,6 +44,7 @@ import com.vitorpamplona.amethyst.commons.resources.Res import com.vitorpamplona.amethyst.commons.resources.podcast_episode_count import com.vitorpamplona.amethyst.commons.ui.components.TranslatableRichTextViewer import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav +import com.vitorpamplona.amethyst.commons.ui.note.platform.ReactionsRow import com.vitorpamplona.amethyst.commons.ui.note.types.PodcastCoverCard import com.vitorpamplona.amethyst.commons.ui.note.types.PodcastPeople import com.vitorpamplona.amethyst.commons.ui.pluralStringRes @@ -51,7 +52,6 @@ import com.vitorpamplona.amethyst.commons.ui.theme.DividerThickness import com.vitorpamplona.amethyst.commons.ui.theme.Size5dp import com.vitorpamplona.amethyst.commons.ui.theme.grayText import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel -import com.vitorpamplona.amethyst.ui.note.ReactionsRow import com.vitorpamplona.quartz.nipF4Podcasts.metadata.PodcastMetadataEvent import com.vitorpamplona.quartz.podcasts.PodcastShow diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/podcasts/authoring/EditPodcastShowScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/podcasts/authoring/EditPodcastShowScreen.kt index 47aefaed52..4007521128 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/podcasts/authoring/EditPodcastShowScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/podcasts/authoring/EditPodcastShowScreen.kt @@ -77,11 +77,11 @@ import com.vitorpamplona.amethyst.commons.service.upload.ui.StrippingFailureDial import com.vitorpamplona.amethyst.commons.ui.insets.imePaddingSafe import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav import com.vitorpamplona.amethyst.commons.ui.navigation.topbars.SendingTopBar +import com.vitorpamplona.amethyst.commons.ui.stringRes import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel import com.vitorpamplona.amethyst.ui.actions.uploads.GallerySelectSingle import com.vitorpamplona.amethyst.ui.screen.loggedIn.music.CoverImagePicker import com.vitorpamplona.amethyst.ui.screen.loggedIn.music.UploadInProgressBanner -import com.vitorpamplona.amethyst.ui.stringRes import kotlinx.collections.immutable.persistentListOf import org.jetbrains.compose.resources.StringResource diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/privacy/PrivacyOptionsScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/privacy/PrivacyOptionsScreen.kt index 5ecce348ac..fde68ca8f8 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/privacy/PrivacyOptionsScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/privacy/PrivacyOptionsScreen.kt @@ -54,7 +54,6 @@ import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.lifecycle.compose.collectAsStateWithLifecycle -import com.vitorpamplona.amethyst.Amethyst import com.vitorpamplona.amethyst.commons.icons.symbols.Icon import com.vitorpamplona.amethyst.commons.icons.symbols.MaterialSymbol import com.vitorpamplona.amethyst.commons.icons.symbols.MaterialSymbols @@ -103,6 +102,8 @@ import com.vitorpamplona.amethyst.commons.ui.components.TitleExplainer import com.vitorpamplona.amethyst.commons.ui.navigation.navs.EmptyNav import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav import com.vitorpamplona.amethyst.commons.ui.navigation.topbars.TopBarWithBackButton +import com.vitorpamplona.amethyst.commons.ui.platform.LocalAppServices +import com.vitorpamplona.amethyst.commons.ui.stringRes import com.vitorpamplona.amethyst.commons.ui.theme.ThemeComparisonRow import com.vitorpamplona.amethyst.commons.ui.theme.placeholderText import com.vitorpamplona.amethyst.ui.screen.loggedIn.settings.SegmentedChoiceTile @@ -111,7 +112,6 @@ import com.vitorpamplona.amethyst.ui.screen.loggedIn.settings.SettingsControlRow import com.vitorpamplona.amethyst.ui.screen.loggedIn.settings.SettingsDivider import com.vitorpamplona.amethyst.ui.screen.loggedIn.settings.SettingsSection import com.vitorpamplona.amethyst.ui.screen.loggedIn.settings.SettingsSwitchTile -import com.vitorpamplona.amethyst.ui.stringRes import kotlinx.collections.immutable.toImmutableList import kotlinx.coroutines.flow.MutableStateFlow import org.jetbrains.compose.resources.StringResource @@ -119,7 +119,7 @@ import org.jetbrains.compose.resources.StringResource @OptIn(ExperimentalMaterial3Api::class) @Composable fun PrivacyOptionsScreen(nav: INav) { - PrivacyOptionsScreen(Amethyst.instance.torPrefs.value, nav) + PrivacyOptionsScreen(LocalAppServices.current.torSettings, nav) } @OptIn(ExperimentalMaterial3Api::class) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/header/PaymentTargetsDialog.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/header/PaymentTargetsDialog.kt index d878498609..bf49de7a0a 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/header/PaymentTargetsDialog.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/header/PaymentTargetsDialog.kt @@ -66,9 +66,9 @@ import com.vitorpamplona.amethyst.commons.ui.components.M3ActionSection import com.vitorpamplona.amethyst.commons.ui.components.util.setText import com.vitorpamplona.amethyst.commons.ui.loadStringRes import com.vitorpamplona.amethyst.commons.ui.note.ErrorMessageDialog +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.qrcode.QrCodeDrawer import com.vitorpamplona.amethyst.commons.ui.stringRes import com.vitorpamplona.amethyst.commons.ui.theme.Size20Modifier -import com.vitorpamplona.amethyst.ui.screen.loggedIn.qrcode.QrCodeDrawer import com.vitorpamplona.quartz.nipA3PaymentTargets.PaymentTarget import kotlinx.coroutines.launch diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/qrcode/QrCodeScanner.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/qrcode/QrCodeScanner.kt index f935fd2575..9ae17da7eb 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/qrcode/QrCodeScanner.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/qrcode/QrCodeScanner.kt @@ -24,9 +24,9 @@ import androidx.compose.runtime.Composable import com.vitorpamplona.amethyst.commons.model.navigation.Route import com.vitorpamplona.amethyst.commons.qrcode.ScannedPayload import com.vitorpamplona.amethyst.commons.qrcode.classifyScannedPayload +import com.vitorpamplona.amethyst.commons.ui.platform.QrCodeScannerDialog +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.qrcode.ScanOutcome import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel -import com.vitorpamplona.amethyst.ui.screen.loggedIn.qrcode.scanner.QrCodeScannerDialog -import com.vitorpamplona.amethyst.ui.screen.loggedIn.qrcode.scanner.ScanOutcome import com.vitorpamplona.amethyst.ui.uriToRoute import com.vitorpamplona.quartz.utils.Log import kotlinx.coroutines.CancellationException diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/qrcode/ShowQRScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/qrcode/ShowQRScreen.kt index cfda159a4b..8a31f94ec1 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/qrcode/ShowQRScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/qrcode/ShowQRScreen.kt @@ -64,6 +64,8 @@ import com.vitorpamplona.amethyst.commons.ui.note.LoadUser import com.vitorpamplona.amethyst.commons.ui.note.ObserveAndDisplayNIP05 import com.vitorpamplona.amethyst.commons.ui.richtext.CreateTextWithEmoji import com.vitorpamplona.amethyst.commons.ui.screen.LocalDisplaySettings +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.qrcode.KeepScreenBrightAndAwake +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.qrcode.QrCodeDrawer import com.vitorpamplona.amethyst.commons.ui.stringRes import com.vitorpamplona.amethyst.commons.ui.theme.Font14SP import com.vitorpamplona.amethyst.commons.ui.theme.Size10dp diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/qrcode/scanner/QrScannerDialog.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/qrcode/scanner/QrScannerDialog.kt index e05e6246cd..d1dbf238eb 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/qrcode/scanner/QrScannerDialog.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/qrcode/scanner/QrScannerDialog.kt @@ -104,6 +104,7 @@ import com.vitorpamplona.amethyst.commons.resources.qr_scanner_grant_camera import com.vitorpamplona.amethyst.commons.resources.qr_scanner_no_code_in_image import com.vitorpamplona.amethyst.commons.resources.qr_scanner_open_settings import com.vitorpamplona.amethyst.commons.resources.qr_scanner_unavailable +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.qrcode.ScanOutcome import com.vitorpamplona.amethyst.commons.ui.stringRes import com.vitorpamplona.amethyst.ui.call.openAppSettings import com.vitorpamplona.amethyst.ui.components.SetDialogToEdgeToEdge @@ -114,15 +115,6 @@ import kotlinx.coroutines.launch import java.util.concurrent.Executors import java.util.concurrent.TimeUnit -/** What the caller did with a decoded payload — and therefore what the scanner does next. */ -enum class ScanOutcome { - /** The caller acted on it. Close the scanner. */ - Handled, - - /** Decoded fine, but this screen has nothing to do with it. Explain, and keep scanning. */ - NotSupported, -} - /** * Full-screen QR scanner. * diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/common/BasicRelaySetupInfo.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/common/BasicRelaySetupInfo.kt index b5fb025c3f..5e42cbd9f6 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/common/BasicRelaySetupInfo.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/common/BasicRelaySetupInfo.kt @@ -21,8 +21,8 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.common import androidx.compose.runtime.Immutable -import com.vitorpamplona.amethyst.Amethyst import com.vitorpamplona.amethyst.commons.model.User +import com.vitorpamplona.amethyst.commons.model.cache.LocalCache import com.vitorpamplona.quartz.nip01Core.relay.client.stats.RelayStat import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl @@ -41,6 +41,6 @@ fun relaySetupInfoBuilder( ): BasicRelaySetupInfo = BasicRelaySetupInfo( relay = normalized, - relayStat = Amethyst.instance.relayStats.get(normalized), + relayStat = LocalCache.appHost.relayStats?.get(normalized) ?: RelayStat(), forcesTor = forcesTor, ) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/common/BasicRelaySetupInfoModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/common/BasicRelaySetupInfoModel.kt index 7d565eef51..b39de5b486 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/common/BasicRelaySetupInfoModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/common/BasicRelaySetupInfoModel.kt @@ -22,7 +22,6 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.common import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope -import com.vitorpamplona.amethyst.Amethyst import com.vitorpamplona.amethyst.commons.model.Account import com.vitorpamplona.amethyst.commons.model.cache.LocalCache import com.vitorpamplona.amethyst.commons.relays.ui.CountFilter @@ -131,7 +130,7 @@ abstract class BasicRelaySetupInfoModel : ViewModel() { relaySetupInfoBuilder( normalized = it, forcesTor = - Amethyst.instance.torEvaluatorFlow.flow.value + accountViewModel.host.torRelayEvaluation.value .useTor(it), ) }.distinctBy { it.relay } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/connected/ConnectedRelayListViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/connected/ConnectedRelayListViewModel.kt index 8e7d24af54..60273e00e4 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/connected/ConnectedRelayListViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/connected/ConnectedRelayListViewModel.kt @@ -21,7 +21,6 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.connected import androidx.compose.runtime.Stable -import com.vitorpamplona.amethyst.Amethyst import com.vitorpamplona.amethyst.commons.model.cache.LocalCache import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.common.BasicRelaySetupInfo import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.common.BasicRelaySetupInfoModel @@ -36,7 +35,7 @@ class ConnectedRelayListViewModel : BasicRelaySetupInfoModel() { return relayList .map { - val reqs = Amethyst.instance.client.activeRequests(it) + val reqs = account.client.activeRequests(it) val users = mutableSetOf() reqs.forEach { _, filters -> @@ -55,9 +54,9 @@ class ConnectedRelayListViewModel : BasicRelaySetupInfoModel() { BasicRelaySetupInfo( relay = it, - relayStat = Amethyst.instance.relayStats.get(it), + relayStat = accountViewModel.host.relayStats.get(it), forcesTor = - Amethyst.instance.torEvaluatorFlow.flow.value + accountViewModel.host.torRelayEvaluation.value .useTor(it), users = users.mapNotNull { hex -> LocalCache.checkGetOrCreateUser(hex) }, ) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/subscriptions/ActiveSubscriptionsScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/subscriptions/ActiveSubscriptionsScreen.kt index ef75453d74..1b7c903bd3 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/subscriptions/ActiveSubscriptionsScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/subscriptions/ActiveSubscriptionsScreen.kt @@ -92,18 +92,18 @@ import com.vitorpamplona.amethyst.commons.resources.marmot_group_fallback_name import com.vitorpamplona.amethyst.commons.ui.components.RobohashFallbackAsyncImage import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav import com.vitorpamplona.amethyst.commons.ui.navigation.topbars.TopBarWithBackButton +import com.vitorpamplona.amethyst.commons.ui.note.platform.LoadCityName +import com.vitorpamplona.amethyst.commons.ui.platform.rememberConcordImageModel +import com.vitorpamplona.amethyst.commons.ui.pluralStringRes import com.vitorpamplona.amethyst.commons.ui.screen.LocalDisplaySettings +import com.vitorpamplona.amethyst.commons.ui.stringRes import com.vitorpamplona.amethyst.commons.ui.theme.RelayIconFilter import com.vitorpamplona.amethyst.commons.ui.theme.allGoodColor import com.vitorpamplona.amethyst.commons.ui.theme.placeholderText import com.vitorpamplona.amethyst.commons.ui.theme.warningColor import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel -import com.vitorpamplona.amethyst.ui.note.creators.location.LoadCityName -import com.vitorpamplona.amethyst.ui.pluralStringRes import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.marmotGroup.loadMarmotRelayIcon import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.marmotGroup.rememberMarmotGroupAvatarUrl -import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.concord.rememberConcordImageModel -import com.vitorpamplona.amethyst.ui.stringRes import com.vitorpamplona.quartz.nip01Core.core.HexKey import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl import com.vitorpamplona.quartz.nip01Core.relay.normalizer.displayUrl diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/AllSettingsScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/AllSettingsScreen.kt index ec9e073d0d..e83fea8d75 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/AllSettingsScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/AllSettingsScreen.kt @@ -71,10 +71,10 @@ import com.vitorpamplona.amethyst.commons.ui.settings.SettingsCategory import com.vitorpamplona.amethyst.commons.ui.settings.SettingsEntry import com.vitorpamplona.amethyst.commons.ui.settings.SettingsIcon import com.vitorpamplona.amethyst.commons.ui.settings.filterSettings +import com.vitorpamplona.amethyst.commons.ui.stringRes import com.vitorpamplona.amethyst.commons.ui.theme.ThemeComparisonColumn import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel import com.vitorpamplona.amethyst.commons.viewmodels.mockAccountViewModel -import com.vitorpamplona.amethyst.ui.stringRes import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch import org.jetbrains.compose.resources.StringResource diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/AppSettingsScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/AppSettingsScreen.kt index f7ff0a81e3..139504d5d2 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/AppSettingsScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/AppSettingsScreen.kt @@ -30,22 +30,17 @@ import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.ExperimentalLayoutApi import androidx.compose.foundation.layout.FlowRow import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.WindowInsets import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.widthIn -import androidx.compose.foundation.layout.windowInsetsPadding import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.verticalScroll import androidx.compose.material3.LocalTextStyle import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Scaffold -import androidx.compose.material3.SegmentedButton -import androidx.compose.material3.SegmentedButtonDefaults -import androidx.compose.material3.SingleChoiceSegmentedButtonRow import androidx.compose.material3.Switch import androidx.compose.material3.Text import androidx.compose.runtime.Composable @@ -61,7 +56,6 @@ import androidx.compose.ui.graphics.Color import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.semantics.contentDescription import androidx.compose.ui.semantics.semantics -import androidx.compose.ui.text.TextStyle import androidx.compose.ui.text.font.FontFamily import androidx.compose.ui.text.intl.Locale import androidx.compose.ui.text.style.TextOverflow @@ -122,7 +116,6 @@ import com.vitorpamplona.amethyst.commons.resources.ui_feature_set_type_simplifi import com.vitorpamplona.amethyst.commons.resources.ui_style import com.vitorpamplona.amethyst.commons.resources.ui_style_description import com.vitorpamplona.amethyst.commons.ui.components.SpinnerSelectionDialog -import com.vitorpamplona.amethyst.commons.ui.components.TextSpinner import com.vitorpamplona.amethyst.commons.ui.components.TitleExplainer import com.vitorpamplona.amethyst.commons.ui.navigation.navs.EmptyNav import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav @@ -134,7 +127,6 @@ import com.vitorpamplona.amethyst.commons.ui.theme.isLight import com.vitorpamplona.amethyst.commons.ui.theme.previewColor import com.vitorpamplona.amethyst.commons.ui.theme.toFontFamily import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel -import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.ImmutableMap import kotlinx.collections.immutable.toImmutableList import kotlinx.collections.immutable.toImmutableMap @@ -219,53 +211,6 @@ fun SettingsScreen( } } -/** - * A [SettingsBlockTile] whose control is a full-width [SingleChoiceSegmentedButtonRow]. - * This is the in-screen replacement for the old dropdown ([TextSpinner]) rows: every - * option is visible and one tap away. Best for 2–4 mutually-exclusive options. - * - * [optionTextStyle] lets each option render its own label preview — e.g. the font tiles - * draw each label in the very typeface / size it selects, so the row demonstrates the - * choices instead of only naming them. - */ -@Composable -internal fun SegmentedChoiceTile( - icon: MaterialSymbol, - title: StringResource, - description: StringResource, - options: List, - labelRes: (T) -> StringResource, - selected: T, - onSelect: (T) -> Unit, - optionTextStyle: (@Composable (T) -> TextStyle)? = null, -) { - SettingsBlockTile( - icon = icon, - title = stringRes(title), - description = stringRes(description), - ) { - SingleChoiceSegmentedButtonRow(modifier = Modifier.fillMaxWidth()) { - options.forEachIndexed { index, option -> - SegmentedButton( - selected = option == selected, - onClick = { onSelect(option) }, - shape = SegmentedButtonDefaults.itemShape(index = index, count = options.size), - // Drop the default check icon: the fill already signals selection, and the icon - // steals ~24dp that the label needs in a 3–4-up row. - icon = {}, - ) { - Text( - text = stringRes(labelRes(option)), - maxLines = 1, - overflow = TextOverflow.Ellipsis, - style = optionTextStyle?.invoke(option) ?: LocalTextStyle.current, - ) - } - } - } - } -} - /** A [SettingsControlRow] with a [Switch] backed by a [BooleanType] flow (ALWAYS ⇔ on). */ @Composable private fun BooleanSwitchTile( @@ -617,91 +562,3 @@ private fun AccentColorSwatch( } } } - -@Composable -fun SettingsRow( - name: StringResource, - description: StringResource, - selectedItems: ImmutableList, - selectedIndex: Int, - onSelect: (Int) -> Unit, -) { - SettingsRow(name, description) { - TextSpinner( - label = "", - placeholder = selectedItems[selectedIndex].title, - options = selectedItems, - onSelect = onSelect, - modifier = Modifier.windowInsetsPadding(WindowInsets(0.dp, 0.dp, 0.dp, 0.dp)), - ) - } -} - -@Composable -fun SettingsRow( - name: StringResource, - description: StringResource, - modifier: Modifier = Modifier.fillMaxWidth(), - content: @Composable () -> Unit, -) { - Row( - verticalAlignment = Alignment.CenterVertically, - modifier = modifier, - horizontalArrangement = Arrangement.spacedBy(5.dp), - ) { - Column( - modifier = Modifier.weight(2.0f), - verticalArrangement = Arrangement.spacedBy(3.dp), - ) { - Text( - text = stringRes(name), - maxLines = 1, - overflow = TextOverflow.Ellipsis, - ) - Text( - text = stringRes(description), - style = MaterialTheme.typography.bodySmall, - color = Color.Gray, - maxLines = 3, - overflow = TextOverflow.Ellipsis, - ) - } - - Column( - Modifier.weight(1f), - horizontalAlignment = Alignment.End, - ) { - content() - } - } -} - -@Composable -fun SettingsRow( - name: StringResource, - description: StringResource, -) { - Row( - verticalAlignment = Alignment.CenterVertically, - modifier = Modifier.fillMaxWidth(), - horizontalArrangement = Arrangement.spacedBy(5.dp), - ) { - Column( - modifier = Modifier.weight(2.0f), - verticalArrangement = Arrangement.spacedBy(3.dp), - ) { - Text( - text = stringRes(name), - maxLines = 1, - overflow = TextOverflow.Ellipsis, - ) - Text( - text = stringRes(description), - style = MaterialTheme.typography.bodySmall, - color = Color.Gray, - maxLines = 3, - overflow = TextOverflow.Ellipsis, - ) - } - } -} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/BottomBarSettingsScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/BottomBarSettingsScreen.kt index 8b09e9d267..b4ca1a28cc 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/BottomBarSettingsScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/BottomBarSettingsScreen.kt @@ -67,7 +67,6 @@ import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.compose.ui.zIndex import androidx.lifecycle.compose.collectAsStateWithLifecycle -import com.vitorpamplona.amethyst.Amethyst import com.vitorpamplona.amethyst.commons.favorites.FavoriteApp import com.vitorpamplona.amethyst.commons.favorites.FavoriteAppIcon import com.vitorpamplona.amethyst.commons.icons.symbols.Icon @@ -91,6 +90,8 @@ import com.vitorpamplona.amethyst.commons.resources.bottom_bar_settings_reorder_ import com.vitorpamplona.amethyst.commons.ui.navigation.navs.EmptyNav import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav import com.vitorpamplona.amethyst.commons.ui.navigation.topbars.TopBarWithBackButton +import com.vitorpamplona.amethyst.commons.ui.platform.LocalAppServices +import com.vitorpamplona.amethyst.commons.ui.stringRes import com.vitorpamplona.amethyst.commons.ui.theme.Size22Modifier import com.vitorpamplona.amethyst.commons.ui.theme.ThemeComparisonRow import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel @@ -102,7 +103,6 @@ import com.vitorpamplona.amethyst.ui.navigation.bottombars.NavBarCatalog import com.vitorpamplona.amethyst.ui.navigation.bottombars.NavBarCategory import com.vitorpamplona.amethyst.ui.navigation.bottombars.rememberFavoriteIconModel import com.vitorpamplona.amethyst.ui.navigation.bottombars.rememberGroupEntryDisplay -import com.vitorpamplona.amethyst.ui.stringRes import com.vitorpamplona.quartz.concord.cord02Community.ConcordCommunityListEntry import com.vitorpamplona.quartz.nip51Lists.simpleGroupList.GroupTag import kotlinx.coroutines.flow.MutableStateFlow @@ -498,7 +498,7 @@ private fun PickerChildren( ) { when (item) { NavBarItem.BROWSER -> { - val favorites by Amethyst.instance.favoriteApps.favorites + val favorites by LocalAppServices.current.favoriteApps.favorites .collectAsStateWithLifecycle() if (favorites.isEmpty()) { EmptyChildHint(Res.string.bottom_bar_settings_no_favorites) @@ -818,7 +818,7 @@ private fun rememberPinnedVisual( PinnedVisual.Glyph(def?.icon ?: MaterialSymbols.Apps, def?.let { stringRes(it.labelRes) } ?: "") } is BottomBarEntry.Favorite -> { - val favorites by Amethyst.instance.favoriteApps.favorites + val favorites by LocalAppServices.current.favoriteApps.favorites .collectAsStateWithLifecycle() val app = favorites.firstOrNull { it.id == entry.favoriteId } if (app != null) PinnedVisual.Favorite(app) else PinnedVisual.Glyph(MaterialSymbols.Public, "") diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/SettingsRows.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/SettingsRows.kt new file mode 100644 index 0000000000..997034fd9d --- /dev/null +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/SettingsRows.kt @@ -0,0 +1,183 @@ +/* + * 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.settings + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.WindowInsets +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.windowInsetsPadding +import androidx.compose.material3.LocalTextStyle +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.SegmentedButton +import androidx.compose.material3.SegmentedButtonDefaults +import androidx.compose.material3.SingleChoiceSegmentedButtonRow +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.unit.dp +import com.vitorpamplona.amethyst.commons.icons.symbols.MaterialSymbol +import com.vitorpamplona.amethyst.commons.ui.components.TextSpinner +import com.vitorpamplona.amethyst.commons.ui.components.TitleExplainer +import com.vitorpamplona.amethyst.commons.ui.stringRes +import kotlinx.collections.immutable.ImmutableList +import org.jetbrains.compose.resources.StringResource + +/** + * A [SettingsBlockTile] whose control is a full-width [SingleChoiceSegmentedButtonRow]. + * This is the in-screen replacement for the old dropdown ([TextSpinner]) rows: every + * option is visible and one tap away. Best for 2–4 mutually-exclusive options. + * + * [optionTextStyle] lets each option render its own label preview — e.g. the font tiles + * draw each label in the very typeface / size it selects, so the row demonstrates the + * choices instead of only naming them. + */ +@Composable +fun SegmentedChoiceTile( + icon: MaterialSymbol, + title: StringResource, + description: StringResource, + options: List, + labelRes: (T) -> StringResource, + selected: T, + onSelect: (T) -> Unit, + optionTextStyle: (@Composable (T) -> TextStyle)? = null, +) { + SettingsBlockTile( + icon = icon, + title = stringRes(title), + description = stringRes(description), + ) { + SingleChoiceSegmentedButtonRow(modifier = Modifier.fillMaxWidth()) { + options.forEachIndexed { index, option -> + SegmentedButton( + selected = option == selected, + onClick = { onSelect(option) }, + shape = SegmentedButtonDefaults.itemShape(index = index, count = options.size), + // Drop the default check icon: the fill already signals selection, and the icon + // steals ~24dp that the label needs in a 3–4-up row. + icon = {}, + ) { + Text( + text = stringRes(labelRes(option)), + maxLines = 1, + overflow = TextOverflow.Ellipsis, + style = optionTextStyle?.invoke(option) ?: LocalTextStyle.current, + ) + } + } + } + } +} + +@Composable +fun SettingsRow( + name: StringResource, + description: StringResource, + selectedItems: ImmutableList, + selectedIndex: Int, + onSelect: (Int) -> Unit, +) { + SettingsRow(name, description) { + TextSpinner( + label = "", + placeholder = selectedItems[selectedIndex].title, + options = selectedItems, + onSelect = onSelect, + modifier = Modifier.windowInsetsPadding(WindowInsets(0.dp, 0.dp, 0.dp, 0.dp)), + ) + } +} + +@Composable +fun SettingsRow( + name: StringResource, + description: StringResource, + modifier: Modifier = Modifier.fillMaxWidth(), + content: @Composable () -> Unit, +) { + Row( + verticalAlignment = Alignment.CenterVertically, + modifier = modifier, + horizontalArrangement = Arrangement.spacedBy(5.dp), + ) { + Column( + modifier = Modifier.weight(2.0f), + verticalArrangement = Arrangement.spacedBy(3.dp), + ) { + Text( + text = stringRes(name), + maxLines = 1, + overflow = TextOverflow.Ellipsis, + ) + Text( + text = stringRes(description), + style = MaterialTheme.typography.bodySmall, + color = Color.Gray, + maxLines = 3, + overflow = TextOverflow.Ellipsis, + ) + } + + Column( + Modifier.weight(1f), + horizontalAlignment = Alignment.End, + ) { + content() + } + } +} + +@Composable +fun SettingsRow( + name: StringResource, + description: StringResource, +) { + Row( + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.spacedBy(5.dp), + ) { + Column( + modifier = Modifier.weight(2.0f), + verticalArrangement = Arrangement.spacedBy(3.dp), + ) { + Text( + text = stringRes(name), + maxLines = 1, + overflow = TextOverflow.Ellipsis, + ) + Text( + text = stringRes(description), + style = MaterialTheme.typography.bodySmall, + color = Color.Gray, + maxLines = 3, + overflow = TextOverflow.Ellipsis, + ) + } + } +} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/cordn/CordnMigrateScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/cordn/CordnMigrateScreen.kt index c2bf67e6fb..1d96ceee8d 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/cordn/CordnMigrateScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/cordn/CordnMigrateScreen.kt @@ -82,10 +82,10 @@ import com.vitorpamplona.amethyst.commons.ui.components.EmptyState import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav import com.vitorpamplona.amethyst.commons.ui.navigation.topbars.TopBarWithBackButton import com.vitorpamplona.amethyst.commons.ui.pluralStringRes +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.qrcode.QrCodeDrawer import com.vitorpamplona.amethyst.commons.ui.stringRes import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel import com.vitorpamplona.amethyst.model.cordn.AndroidCordnBlobStore -import com.vitorpamplona.amethyst.ui.screen.loggedIn.qrcode.QrCodeDrawer import com.vitorpamplona.amethyst.ui.screen.loggedIn.qrcode.SimpleQrCodeScanner import com.vitorpamplona.amethyst.ui.screen.loggedIn.settings.SettingsSection import com.vitorpamplona.quartz.cordn.appMultiDevice.CordnHandoffCode diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/nip46/Nip46SignerScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/nip46/Nip46SignerScreen.kt index 37040337e7..a54b850f24 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/nip46/Nip46SignerScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/nip46/Nip46SignerScreen.kt @@ -125,9 +125,9 @@ import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav import com.vitorpamplona.amethyst.commons.ui.navigation.topbars.TopBarWithBackButton import com.vitorpamplona.amethyst.commons.ui.platform.AppBottomBar import com.vitorpamplona.amethyst.commons.ui.pluralStringRes +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.qrcode.QrCodeDrawer import com.vitorpamplona.amethyst.commons.ui.stringRes import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel -import com.vitorpamplona.amethyst.ui.screen.loggedIn.qrcode.QrCodeDrawer import com.vitorpamplona.amethyst.ui.screen.loggedIn.qrcode.SimpleQrCodeScanner import kotlinx.coroutines.launch diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/shorts/VideoCardCompose.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/shorts/VideoCardCompose.kt index 3dff6d975c..197918f1a6 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/shorts/VideoCardCompose.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/shorts/VideoCardCompose.kt @@ -51,9 +51,9 @@ import com.vitorpamplona.amethyst.commons.ui.components.ContentWarningGate import com.vitorpamplona.amethyst.commons.ui.components.collectContentWarningReasons import com.vitorpamplona.amethyst.commons.ui.components.mediaSizingModifier import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav +import com.vitorpamplona.amethyst.commons.ui.note.platform.ReactionsRow import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel import com.vitorpamplona.amethyst.ui.components.ZoomableContentView -import com.vitorpamplona.amethyst.ui.note.ReactionsRow import com.vitorpamplona.amethyst.ui.screen.loggedIn.video.UserCardHeader import com.vitorpamplona.quartz.nip01Core.core.Event import com.vitorpamplona.quartz.nip31Alts.alt diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/softwareapps/SoftwareAppDetailScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/softwareapps/SoftwareAppDetailScreen.kt index b4a2faa10d..8ea539a211 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/softwareapps/SoftwareAppDetailScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/softwareapps/SoftwareAppDetailScreen.kt @@ -75,6 +75,7 @@ import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav import com.vitorpamplona.amethyst.commons.ui.navigation.topbars.TopBarWithBackButton import com.vitorpamplona.amethyst.commons.ui.note.LoadAddressableNote import com.vitorpamplona.amethyst.commons.ui.note.NoteCompose +import com.vitorpamplona.amethyst.commons.ui.note.platform.ReactionsRow import com.vitorpamplona.amethyst.commons.ui.note.types.AppAuthorLine import com.vitorpamplona.amethyst.commons.ui.note.types.AppIcon import com.vitorpamplona.amethyst.commons.ui.note.types.AppLinksColumn @@ -99,7 +100,6 @@ import com.vitorpamplona.amethyst.commons.ui.theme.placeholderText import com.vitorpamplona.amethyst.commons.ui.theme.subtleBorder import com.vitorpamplona.amethyst.commons.ui.thread.drawReplyLevel import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel -import com.vitorpamplona.amethyst.ui.note.ReactionsRow import com.vitorpamplona.amethyst.ui.screen.loggedIn.threadview.dal.ThreadFeedViewModel import com.vitorpamplona.quartz.experimental.nip82SoftwareApps.application.SoftwareApplicationEvent import com.vitorpamplona.quartz.nip01Core.core.Address diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/threadview/ThreadFeedView.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/threadview/ThreadFeedView.kt index 9f839d5c35..3a463ac70e 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/threadview/ThreadFeedView.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/threadview/ThreadFeedView.kt @@ -144,6 +144,7 @@ import com.vitorpamplona.amethyst.commons.ui.note.elements.TimeAgo import com.vitorpamplona.amethyst.commons.ui.note.elements.TimeAgoStyle import com.vitorpamplona.amethyst.commons.ui.note.nip22Comments.DisplayCommentScope import com.vitorpamplona.amethyst.commons.ui.note.observeEdits +import com.vitorpamplona.amethyst.commons.ui.note.platform.ReactionsRow import com.vitorpamplona.amethyst.commons.ui.note.types.BadgeDisplay import com.vitorpamplona.amethyst.commons.ui.note.types.DisplayBlockedRelayList import com.vitorpamplona.amethyst.commons.ui.note.types.DisplayBroadcastRelayList @@ -262,7 +263,6 @@ import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel import com.vitorpamplona.amethyst.commons.viewmodels.mockAccountViewModel import com.vitorpamplona.amethyst.commons.viewmodels.thread.LevelFeedViewModel import com.vitorpamplona.amethyst.ui.components.ZoomableContentView -import com.vitorpamplona.amethyst.ui.note.ReactionsRow import com.vitorpamplona.amethyst.ui.note.types.AudioHeader import com.vitorpamplona.amethyst.ui.note.types.AudioTrackHeader import com.vitorpamplona.amethyst.ui.note.types.RenderGitIssueEvent diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/video/FileMetadataCardCompose.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/video/FileMetadataCardCompose.kt index c88395ced3..aa5b9947f3 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/video/FileMetadataCardCompose.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/video/FileMetadataCardCompose.kt @@ -45,11 +45,11 @@ import com.vitorpamplona.amethyst.commons.ui.components.ContentWarningGate import com.vitorpamplona.amethyst.commons.ui.components.collectContentWarningReasons import com.vitorpamplona.amethyst.commons.ui.components.mediaSizingModifier import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav +import com.vitorpamplona.amethyst.commons.ui.note.platform.ReactionsRow import com.vitorpamplona.amethyst.commons.ui.note.types.FileMetadataAttachmentCard import com.vitorpamplona.amethyst.commons.ui.note.types.toMediaContent import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel import com.vitorpamplona.amethyst.ui.components.ZoomableContentView -import com.vitorpamplona.amethyst.ui.note.ReactionsRow import com.vitorpamplona.quartz.nip01Core.core.Event import com.vitorpamplona.quartz.nip36SensitiveContent.isSensitiveOrNSFW import com.vitorpamplona.quartz.nip94FileMetadata.FileMetadataEvent diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/wallet/OnchainZapSendDialog.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/wallet/OnchainZapSendDialog.kt index 216a41cca9..7408397c0a 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/wallet/OnchainZapSendDialog.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/wallet/OnchainZapSendDialog.kt @@ -126,6 +126,7 @@ import com.vitorpamplona.amethyst.commons.ui.pluralStringRes import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.wallet.sats import com.vitorpamplona.amethyst.commons.ui.stringRes import com.vitorpamplona.amethyst.commons.ui.theme.bitcoinColor +import com.vitorpamplona.amethyst.commons.util.formatGrouped import com.vitorpamplona.amethyst.commons.util.showAmount import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel import com.vitorpamplona.amethyst.ui.components.namecoin.NamecoinResolutionRow @@ -144,7 +145,6 @@ import kotlinx.coroutines.delay import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import org.jetbrains.compose.resources.StringResource -import java.text.NumberFormat /** Shared with the profile Send Payment screen's on-chain rail. */ internal enum class FeeTier( @@ -706,7 +706,7 @@ private fun AmountSection( if (belowMinimum) { Spacer(Modifier.height(4.dp)) Text( - text = stringRes(Res.string.onchain_send_min_warning, NumberFormat.getNumberInstance().format(MIN_ONCHAIN_ZAP_SATS)), + text = stringRes(Res.string.onchain_send_min_warning, formatGrouped(MIN_ONCHAIN_ZAP_SATS)), style = MaterialTheme.typography.labelSmall, color = MaterialTheme.colorScheme.error, ) @@ -806,7 +806,7 @@ private fun SendButton( modifier = Modifier.size(18.dp), ) Spacer(Modifier.size(8.dp)) - val sats = if (amountSats != null && amountSats > 0) NumberFormat.getNumberInstance().format(amountSats) else null + val sats = if (amountSats != null && amountSats > 0) formatGrouped(amountSats) else null Text( text = when { @@ -866,7 +866,7 @@ private fun SplitsRecipientSection( if (share != null) { val belowDust = share.sats < OnchainZapBuilder.DUST_THRESHOLD_SATS Text( - text = "${NumberFormat.getNumberInstance().format(share.sats)} sats", + text = "${formatGrouped(share.sats)} sats", style = MaterialTheme.typography.bodySmall, color = if (belowDust) { @@ -971,12 +971,12 @@ private fun SuccessBody(result: OnchainZapSendResult.Success) { ResultRow(stringRes(Res.string.onchain_send_result_transaction), result.txid) ResultRow( stringRes(Res.string.onchain_send_result_fee), - stringRes(Res.string.onchain_send_sats_amount, NumberFormat.getNumberInstance().format(result.feeSats)), + stringRes(Res.string.onchain_send_sats_amount, formatGrouped(result.feeSats)), ) if (result.changeSats > 0) { ResultRow( stringRes(Res.string.onchain_send_result_change), - stringRes(Res.string.onchain_send_sats_amount, NumberFormat.getNumberInstance().format(result.changeSats)), + stringRes(Res.string.onchain_send_sats_amount, formatGrouped(result.changeSats)), ) } } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/wallet/WalletReceiveScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/wallet/WalletReceiveScreen.kt index 85f789f232..2b3a8a19f1 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/wallet/WalletReceiveScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/wallet/WalletReceiveScreen.kt @@ -74,9 +74,9 @@ import com.vitorpamplona.amethyst.commons.resources.wallet_description import com.vitorpamplona.amethyst.commons.resources.wallet_receive import com.vitorpamplona.amethyst.commons.resources.wallet_sats import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.qrcode.QrCodeDrawer import com.vitorpamplona.amethyst.commons.ui.stringRes import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel -import com.vitorpamplona.amethyst.ui.screen.loggedIn.qrcode.QrCodeDrawer import java.text.NumberFormat @OptIn(ExperimentalMaterial3Api::class) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/wallet/WalletViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/wallet/WalletViewModel.kt index 317d0b09be..497f0ac69d 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/wallet/WalletViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/wallet/WalletViewModel.kt @@ -78,6 +78,8 @@ import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.launch import kotlinx.coroutines.withTimeoutOrNull import org.jetbrains.compose.resources.StringResource +import kotlin.uuid.ExperimentalUuidApi +import kotlin.uuid.Uuid sealed class SendState { data object Idle : SendState() @@ -338,6 +340,7 @@ class WalletViewModel : ViewModel() { } /** Adds a CLINK debit pointer (`ndebit1…`) as a spend-only payment source. */ + @OptIn(ExperimentalUuidApi::class) fun addClinkDebitWallet( name: String, ndebit: String, @@ -347,9 +350,7 @@ class WalletViewModel : ViewModel() { val entry = ClinkDebitWalletEntryNorm( id = - java.util.UUID - .randomUUID() - .toString(), + Uuid.random().toString(), name = name.ifBlank { "Debit" }, pointer = pointer, ) @@ -384,6 +385,7 @@ class WalletViewModel : ViewModel() { } } + @OptIn(ExperimentalUuidApi::class) fun addWallet( name: String, uri: Nip47WalletConnect.Nip47URINorm, @@ -392,9 +394,7 @@ class WalletViewModel : ViewModel() { val entry = NwcWalletEntryNorm( id = - java.util.UUID - .randomUUID() - .toString(), + Uuid.random().toString(), name = name.ifBlank { "Wallet" }, uri = uri, ) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/theme/Theme.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/theme/Theme.kt index 2d01d2adab..04a3631842 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/theme/Theme.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/theme/Theme.kt @@ -43,6 +43,7 @@ import com.vitorpamplona.amethyst.commons.ui.components.LocalInlineQuoteRenderer import com.vitorpamplona.amethyst.commons.ui.components.LocalTranslationPlatform import com.vitorpamplona.amethyst.commons.ui.note.platform.LocalNotePlatform import com.vitorpamplona.amethyst.commons.ui.platform.LocalAppPlatform +import com.vitorpamplona.amethyst.commons.ui.platform.LocalAppServices import com.vitorpamplona.amethyst.commons.ui.richtext.LocalRichTextPlatform import com.vitorpamplona.amethyst.commons.ui.screen.DisplaySettings import com.vitorpamplona.amethyst.commons.ui.screen.collectDisplaySettings @@ -57,6 +58,7 @@ import com.vitorpamplona.amethyst.ui.components.DefaultInlineQuoteRenderer import com.vitorpamplona.amethyst.ui.components.FlavorTranslationPlatform import com.vitorpamplona.amethyst.ui.note.platform.AndroidNotePlatform import com.vitorpamplona.amethyst.ui.platform.AndroidAppPlatform +import com.vitorpamplona.amethyst.ui.platform.AndroidAppServices val chartLightColors = VicoTheme( @@ -141,6 +143,7 @@ fun AmethystTheme( LocalTranslationPlatform provides FlavorTranslationPlatform, LocalNotePlatform provides AndroidNotePlatform, LocalAppPlatform provides AndroidAppPlatform, + LocalAppServices provides AndroidAppServices, content = content, ) } diff --git a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/favorites/FavoriteCoordinate.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/favorites/FavoriteCoordinate.kt new file mode 100644 index 0000000000..1bcfb522b9 --- /dev/null +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/favorites/FavoriteCoordinate.kt @@ -0,0 +1,39 @@ +/* + * 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.commons.favorites + +import com.vitorpamplona.quartz.nip01Core.core.Event +import com.vitorpamplona.quartz.nip5aStaticWebsites.NamedSiteEvent +import com.vitorpamplona.quartz.nip5dNapplets.NamedNappletEvent + +/** + * The addressable coordinate `kind:pubkey:dtag` used to key an nsite/napplet favorite. Stored + * instead of the content hash so the favorite survives routine code/manifest updates. + */ +fun favoriteCoordinateOf(event: Event): String { + val dTag = + when (event) { + is NamedNappletEvent -> event.identifier() + is NamedSiteEvent -> event.identifier() + else -> "" + } + return "${event.kind}:${event.pubKey}:$dTag" +} diff --git a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/service/AppServices.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/service/AppServices.kt new file mode 100644 index 0000000000..692b4cb9fe --- /dev/null +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/service/AppServices.kt @@ -0,0 +1,51 @@ +/* + * 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.commons.service + +import com.vitorpamplona.amethyst.commons.browser.BrowserHistoryRegistry +import com.vitorpamplona.amethyst.commons.browser.BrowserIconRegistry +import com.vitorpamplona.amethyst.commons.connectedApps.signers.NostrSignerPermissionStore +import com.vitorpamplona.amethyst.commons.favorites.FavoriteAppsRegistry +import com.vitorpamplona.amethyst.commons.napplet.permissions.NappletPermissionLedger +import com.vitorpamplona.amethyst.commons.tor.TorSettingsFlow + +/** + * App-wide stores that outlive any one account and that screens read directly: the favorites + * row, the browser's history and favicons, what connected apps may do, and the Tor settings. + * Account-scoped services reach screens through the account's ViewModel instead. + * + * A front end implements it over its own modules; shared UI reads it through `LocalAppServices`. + */ +interface AppServices { + val favoriteApps: FavoriteAppsRegistry + + val browserHistory: BrowserHistoryRegistry + + val browserIcons: BrowserIconRegistry + + /** What each napplet/nsite has been granted, per account. */ + val nappletPermissionLedger: NappletPermissionLedger + + /** What each remote-signer client has been granted. */ + val signerPermissionStore: NostrSignerPermissionStore + + val torSettings: TorSettingsFlow +} diff --git a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/util/GroupedNumber.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/util/GroupedNumber.kt new file mode 100644 index 0000000000..89e3f1a75e --- /dev/null +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/util/GroupedNumber.kt @@ -0,0 +1,24 @@ +/* + * 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.commons.util + +/** [value] with the locale's digit grouping: `1,234,567` in English, `1.234.567` in German. */ +expect fun formatGrouped(value: Long): String diff --git a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/viewmodels/AccountViewModelHost.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/viewmodels/AccountViewModelHost.kt index d596783a8e..1ef92d7047 100644 --- a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/viewmodels/AccountViewModelHost.kt +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/viewmodels/AccountViewModelHost.kt @@ -23,10 +23,15 @@ package com.vitorpamplona.amethyst.commons.viewmodels import com.vitorpamplona.amethyst.commons.service.lnurl.LnurlHttpTransport import com.vitorpamplona.amethyst.commons.service.pow.PoWJobFailure import com.vitorpamplona.amethyst.commons.tor.MoneyOpRelayRouting +import com.vitorpamplona.amethyst.commons.tor.TorRelayEvaluation import com.vitorpamplona.quartz.nip01Core.core.HexKey +import com.vitorpamplona.quartz.nip01Core.relay.client.auth.RelayAuthSnapshot import com.vitorpamplona.quartz.nip01Core.relay.client.stats.RelayStats +import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl import com.vitorpamplona.quartz.nip01Core.relay.sockets.WebsocketBuilder +import kotlinx.collections.immutable.PersistentMap import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.StateFlow /** * What the account's ViewModel needs from the process it runs in: app-wide services that outlive @@ -55,6 +60,12 @@ interface AccountViewModelHost { /** Routes a payment's relays under the money-operations Tor preference while it runs. */ val moneyOpRelays: MoneyOpRelayRouting + /** Which relays go through Tor under the user's settings, kept current. */ + val torRelayEvaluation: StateFlow + + /** NIP-42 authentication state per relay, across every logged-in account. */ + val relayAuthState: StateFlow> + /** Public keys of every account saved on this device. */ val savedAccounts: Flow> diff --git a/commons/src/iosMain/kotlin/com/vitorpamplona/amethyst/commons/util/GroupedNumber.ios.kt b/commons/src/iosMain/kotlin/com/vitorpamplona/amethyst/commons/util/GroupedNumber.ios.kt new file mode 100644 index 0000000000..22e852b588 --- /dev/null +++ b/commons/src/iosMain/kotlin/com/vitorpamplona/amethyst/commons/util/GroupedNumber.ios.kt @@ -0,0 +1,31 @@ +/* + * 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.commons.util + +import platform.Foundation.NSNumber +import platform.Foundation.NSNumberFormatter +import platform.Foundation.NSNumberFormatterDecimalStyle +import platform.Foundation.numberWithLongLong + +actual fun formatGrouped(value: Long): String { + val formatter = NSNumberFormatter().apply { numberStyle = NSNumberFormatterDecimalStyle } + return formatter.stringFromNumber(NSNumber.numberWithLongLong(value)) ?: value.toString() +} diff --git a/commons/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons/util/GroupedNumber.jvmAndroid.kt b/commons/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons/util/GroupedNumber.jvmAndroid.kt new file mode 100644 index 0000000000..b2e00ab2b1 --- /dev/null +++ b/commons/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons/util/GroupedNumber.jvmAndroid.kt @@ -0,0 +1,25 @@ +/* + * 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.commons.util + +import java.text.NumberFormat + +actual fun formatGrouped(value: Long): String = NumberFormat.getNumberInstance().format(value) diff --git a/commonsUI/build.gradle.kts b/commonsUI/build.gradle.kts index 5073bc8d30..1ff1db16a9 100644 --- a/commonsUI/build.gradle.kts +++ b/commonsUI/build.gradle.kts @@ -189,6 +189,10 @@ kotlin { // org.jetbrains.androidx.lifecycle variant or accept a // platform-specific ViewModel access pattern on iOS. implementation(libs.androidx.lifecycle.viewmodel.compose) + + // ZXing core encodes the QR codes the shared QrCodeDrawer shows. Pure Java, + // Apache-2.0, and already linked by both apps. + implementation(libs.zxing) } } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/qrcode/KeepScreenBrightAndAwake.kt b/commonsUI/src/androidMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/qrcode/KeepScreenBrightAndAwake.android.kt similarity index 86% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/qrcode/KeepScreenBrightAndAwake.kt rename to commonsUI/src/androidMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/qrcode/KeepScreenBrightAndAwake.android.kt index 0965ab4c75..85cd70104c 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/qrcode/KeepScreenBrightAndAwake.kt +++ b/commonsUI/src/androidMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/qrcode/KeepScreenBrightAndAwake.android.kt @@ -18,23 +18,15 @@ * 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.qrcode +package com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.qrcode import androidx.compose.runtime.Composable import androidx.compose.runtime.DisposableEffect import androidx.compose.ui.platform.LocalView import com.vitorpamplona.amethyst.commons.ui.components.getActivityWindow -/** - * Pins the screen to full brightness, and awake, while a QR code is on it. - * - * Half of "this QR code will not scan" is the phone showing it. A dark-mode OLED at the - * auto-brightness the room asked for can put so little contrast between the black and white - * modules that the other camera's binarizer cannot separate them — and the person holding it has - * no idea that is the problem, because to a human eye the code looks perfectly clear. - */ @Composable -fun KeepScreenBrightAndAwake() { +actual fun KeepScreenBrightAndAwake() { val view = LocalView.current // NOT `(view.context as? Activity)`: under Compose the context is routinely a // ContextThemeWrapper, so that cast silently yields null and brightness never changes — diff --git a/commonsUI/src/commonMain/composeResources/values-cs/strings.xml b/commonsUI/src/commonMain/composeResources/values-cs/strings.xml index da517ef139..dc034ff669 100644 --- a/commonsUI/src/commonMain/composeResources/values-cs/strings.xml +++ b/commonsUI/src/commonMain/composeResources/values-cs/strings.xml @@ -6486,4 +6486,6 @@ Přehrát hlasovou zprávu Nahrát hlasovou zprávu Zastavit a odeslat + Hledat nebo zadat adresu + Nepojmenovaný nApplet diff --git a/commonsUI/src/commonMain/composeResources/values-de/strings.xml b/commonsUI/src/commonMain/composeResources/values-de/strings.xml index a9ecda02cd..4f08431389 100644 --- a/commonsUI/src/commonMain/composeResources/values-de/strings.xml +++ b/commonsUI/src/commonMain/composeResources/values-de/strings.xml @@ -3034,4 +3034,6 @@ Der Lightning-Service des Empfängers unter %1$s ist nicht verfügbar. Er wurde aus der Lightning-Adresse "%2$s" berechnet. Fehler: %3$s. Überprüfen Sie, ob der Server online ist und ob die Lightning-Adresse korrekt ist Kann keine Blitzrechnung von %1$s erstellen: Element pr nicht im resultierenden JSON gefunden. Blitzrechnung konnte nicht erstellt werden. Nachricht von %1$s: %2$s + Suchen oder Adresse eingeben + Unbenanntes nApplet diff --git a/commonsUI/src/commonMain/composeResources/values-eo/strings.xml b/commonsUI/src/commonMain/composeResources/values-eo/strings.xml index 797f4b70f0..46255b0be0 100644 --- a/commonsUI/src/commonMain/composeResources/values-eo/strings.xml +++ b/commonsUI/src/commonMain/composeResources/values-eo/strings.xml @@ -2995,4 +2995,6 @@ La Lightning-servo de la ricevanto ĉe %1$s ne disponeblas. Ĝi estis kalkulita el la Lightning-adreso „%2$s". Eraro: %3$s. Kontrolu ĉu la servilo funkcias kaj ĉu la Lightning-adreso estas ĝusta Ne eblas krei Lightning-fakturon de %1$s: Elemento pr ne trovita en la rezulta JSON. Ne eblas krei Lightning-fakturon. Mesaĝo de %1$s: %2$s + Serĉi aŭ enigi adreson + Sennoma nApplet diff --git a/commonsUI/src/commonMain/composeResources/values-es/strings.xml b/commonsUI/src/commonMain/composeResources/values-es/strings.xml index c3ca94cced..c278f19263 100644 --- a/commonsUI/src/commonMain/composeResources/values-es/strings.xml +++ b/commonsUI/src/commonMain/composeResources/values-es/strings.xml @@ -2995,4 +2995,6 @@ El servicio de Lightning del destinatario en %1$s no está disponible. Se calculó a partir de la dirección de Lightning "%2$s". Error: %3$s. Comprueba si el servidor está activo y si la dirección de Lightning es correcta. No se pudo crear una factura de Lightning desde %1$s: no se encontró el elemento pr en el JSON resultante. No se pudo crear una factura de Lightning. Mensaje de %1$s: %2$s. + Buscar o introducir dirección + NApplet sin título diff --git a/commonsUI/src/commonMain/composeResources/values-fa/strings.xml b/commonsUI/src/commonMain/composeResources/values-fa/strings.xml index 29be72a76e..318f5e5a88 100644 --- a/commonsUI/src/commonMain/composeResources/values-fa/strings.xml +++ b/commonsUI/src/commonMain/composeResources/values-fa/strings.xml @@ -2995,4 +2995,6 @@ خدمات لایتنینگ %1$s گیرنده دردسترس نیست. از آدرس لایتنینگ "%2$s" محاسبه شد. خطا: %3$s. بررسی کنید آیا سرور کار می کند و آیا آدرس لایتنینگ صحیح است. نمی توان صورتحساب لایتنینگ از %1$s ساخت. المنت pr در JSON بدست آمده یافت نشد. صورت حساب لایتنینگ ساخته نشد. پیغام از %1$s: %2$s + جستجو یا وارد کردن آدرس + nApplet بی‌عنوان diff --git a/commonsUI/src/commonMain/composeResources/values-fr/strings.xml b/commonsUI/src/commonMain/composeResources/values-fr/strings.xml index 136defe6e8..81ae75c3a4 100644 --- a/commonsUI/src/commonMain/composeResources/values-fr/strings.xml +++ b/commonsUI/src/commonMain/composeResources/values-fr/strings.xml @@ -2995,4 +2995,6 @@ Le service lightning du récepteur à %1$s n'est pas disponible. Il a été calculé à partir de l'adresse lightning "%2$s". Erreur: %3$s. Vérifiez si le serveur est en ligne et si l'adresse lightning est correcte Impossible de créer une facture lightning avec %1$s. Élément pr introuvable dans le JSON résultant. Impossible de créer une facture lightning. Message de %1$s: %2$s + Rechercher ou saisir une adresse + nApplet sans titre diff --git a/commonsUI/src/commonMain/composeResources/values-in/strings.xml b/commonsUI/src/commonMain/composeResources/values-in/strings.xml index d957d12d1c..4195ff78f8 100644 --- a/commonsUI/src/commonMain/composeResources/values-in/strings.xml +++ b/commonsUI/src/commonMain/composeResources/values-in/strings.xml @@ -2958,4 +2958,6 @@ Layanan lightning penerima pada %1$s tidak tersedia. Itu dihitung dari alamat petir "%2$s". Kesalahan: %3$s. Periksa apakah server aktif dan apakah alamat lightning sudah benar Tidak dapat membuat tagihan Lightning dari %1$s: Elemen pr tidak ditemukan dalam JSON yang dihasilkan. Tidak dapat membuat tagihan Lightning. Pesan dari %1$s: %2$s + Cari atau masukkan alamat + nApplet Tanpa Judul diff --git a/commonsUI/src/commonMain/composeResources/values-ja/strings.xml b/commonsUI/src/commonMain/composeResources/values-ja/strings.xml index 7791621a8a..22a1767278 100644 --- a/commonsUI/src/commonMain/composeResources/values-ja/strings.xml +++ b/commonsUI/src/commonMain/composeResources/values-ja/strings.xml @@ -2958,4 +2958,6 @@ %1$s の受信者の Lightning サービスが利用できません。Lightning アドレス "%2$s" から計算されました。エラー: %3$s。サーバーが稼働しているか、Lightning アドレスが正しいかを確認してください %1$s から Lightning 請求書を作成できませんでした: 結果の JSON に要素 pr が見つかりませんでした。 Lightning 請求書を作成できませんでした。%1$s からのメッセージ: %2$s + 検索またはアドレスを入力 + 無題の nApplet diff --git a/commonsUI/src/commonMain/composeResources/values-nl-rBE/strings.xml b/commonsUI/src/commonMain/composeResources/values-nl-rBE/strings.xml index f6bda663e7..9a4af42668 100644 --- a/commonsUI/src/commonMain/composeResources/values-nl-rBE/strings.xml +++ b/commonsUI/src/commonMain/composeResources/values-nl-rBE/strings.xml @@ -2995,4 +2995,6 @@ De Lightning-service van de ontvanger op %1$s is niet beschikbaar. Dit werd berekend vanuit Lightning-adres "%2$s". Fout: %3$s. Controleer of de server online is en het adres correct is. Kon geen Lightning-invoice aanmaken van %1$s: element "pr" niet gevonden in JSON. Kon geen Lightning-invoice aanmaken. Bericht van %1$s: %2$s + Zoeken of adres invoeren + Naamloze nApplet diff --git a/commonsUI/src/commonMain/composeResources/values-nl/strings.xml b/commonsUI/src/commonMain/composeResources/values-nl/strings.xml index 2720341499..b0948cc7a7 100644 --- a/commonsUI/src/commonMain/composeResources/values-nl/strings.xml +++ b/commonsUI/src/commonMain/composeResources/values-nl/strings.xml @@ -2995,4 +2995,6 @@ De Lightning service van de ontvanger bij %1$s is niet beschikbaar. Het werd berekend op basis van het Lightning Adress "%2$s". Fout: %3$s. Controleer of de server klaar is en of het Lightning Adress juist is Kan geen Lightning invoice aanmaken van %1$s: PR-element niet gevonden in de JSON. Kan geen Lightning invoice aanmaken. Bericht van %1$s: %2$s + Zoeken of adres invoeren + Naamloze nApplet diff --git a/commonsUI/src/commonMain/composeResources/values-ru/strings.xml b/commonsUI/src/commonMain/composeResources/values-ru/strings.xml index 7c8d0ca239..3b9e5a7240 100644 --- a/commonsUI/src/commonMain/composeResources/values-ru/strings.xml +++ b/commonsUI/src/commonMain/composeResources/values-ru/strings.xml @@ -3069,4 +3069,6 @@ Lightning-сервис получателя по адресу %1$s недоступен. Он был вычислен из Lightning-адреса "%2$s". Ошибка: %3$s. Проверьте, работает ли сервер и правильно ли указан Lightning-адрес Не удалось создать Lightning-счёт от %1$s: элемент pr не найден в полученном JSON. Не удалось создать Lightning-счёт. Сообщение от %1$s: %2$s + Поиск или введите адрес + nApplet без названия diff --git a/commonsUI/src/commonMain/composeResources/values-ta/strings.xml b/commonsUI/src/commonMain/composeResources/values-ta/strings.xml index a6fa3f2130..1de7149e7e 100644 --- a/commonsUI/src/commonMain/composeResources/values-ta/strings.xml +++ b/commonsUI/src/commonMain/composeResources/values-ta/strings.xml @@ -2995,4 +2995,6 @@ %1$s-ல் பெறுநரின் lightning சேவை கிடைக்கவில்லை. இது lightning முகவரி "%2$s" இலிருந்து கணக்கிடப்பட்டது. பிழை: %3$s. சர்வர் இயங்குகிறதா மற்றும் lightning முகவரி சரியானதா என சரிபாருங்கள் %1$s-இலிருந்து lightning invoice உருவாக்க முடியவில்லை: முடிவு JSON-ல் pr உறுப்பு கிடைக்கவில்லை. Lightning invoice உருவாக்க முடியவில்லை. %1$s-இலிருந்து செய்தி: %2$s + தேடவும் அல்லது முகவரியை உள்ளிடவும் + தலைப்பற்ற nApplet diff --git a/commonsUI/src/commonMain/composeResources/values-th/strings.xml b/commonsUI/src/commonMain/composeResources/values-th/strings.xml index cdb57bb5be..c7d90434d1 100644 --- a/commonsUI/src/commonMain/composeResources/values-th/strings.xml +++ b/commonsUI/src/commonMain/composeResources/values-th/strings.xml @@ -2958,4 +2958,6 @@ บริการ Lightning ของ %1$s แลจะไม่พร้อมใช้งานจาก "%2$s". Error: %3$s. โปรดตรวจสอบผู้ให้บริการ หรือที่อยู่ Lightning ให้ถูกตต้อง ไม่สามารถสร้างบิลชำระในการส่ง zap จาก %1$s เนื่องจากไม่พบ PR บน JSON ไม่สามารถสร้าง lightning invoice ข้อความจาก %1$s: %2$s + ค้นหาหรือป้อนที่อยู่ + nApplet ไม่มีชื่อ diff --git a/commonsUI/src/commonMain/composeResources/values-tr/strings.xml b/commonsUI/src/commonMain/composeResources/values-tr/strings.xml index a54077a881..152a231b8b 100644 --- a/commonsUI/src/commonMain/composeResources/values-tr/strings.xml +++ b/commonsUI/src/commonMain/composeResources/values-tr/strings.xml @@ -2995,4 +2995,6 @@ %1$s adresindeki alıcının Lightning hizmeti kullanılamıyor. Bu adres "%2$s" Lightning adresinden hesaplandı. Hata: %3$s. Sunucunun çalışıp çalışmadığını ve Lightning adresinin doğru olup olmadığını kontrol edin. %1$s adresinden Lightning faturası oluşturulamadı: Sonuçtaki JSON'da pr öğesi bulunamadı. Lightning faturası oluşturulamadı. %1$s'den gelen mesaj: %2$s + Ara veya adres gir + Başlıksız nApplet diff --git a/commonsUI/src/commonMain/composeResources/values-uk/strings.xml b/commonsUI/src/commonMain/composeResources/values-uk/strings.xml index 8e96de5dd5..28c1111f75 100644 --- a/commonsUI/src/commonMain/composeResources/values-uk/strings.xml +++ b/commonsUI/src/commonMain/composeResources/values-uk/strings.xml @@ -3069,4 +3069,6 @@ Lightning-сервіс отримувача за адресою %1$s недоступний. Він був розрахований із lightning-адреси "%2$s". Помилка: %3$s. Перевірте, чи працює сервер і чи правильна lightning-адреса Неможливо створити lightning-рахунок від %1$s: Елемент pr не знайдено у відповідному JSON. Неможливо створити lightning-рахунок. Повідомлення від %1$s: %2$s + Пошук або введіть адресу + NApplet без назви diff --git a/commonsUI/src/commonMain/composeResources/values-zh/strings.xml b/commonsUI/src/commonMain/composeResources/values-zh/strings.xml index 6d7ae350e0..3ae6e070a5 100644 --- a/commonsUI/src/commonMain/composeResources/values-zh/strings.xml +++ b/commonsUI/src/commonMain/composeResources/values-zh/strings.xml @@ -2999,4 +2999,6 @@ %1$s 的接收者闪电服务不可用。它是根据雷电地址"%2$s"计算的。 错误: %3$s。请检查服务器是否已经上线,闪电地址是否正确 无法从 %1$s 创建闪电发票:在生成的 JSON 中找不到付款要求元素。 无法创建闪电发票。来自 %1$s 的消息:%2$s + 搜索或输入地址 + 未命名nApplet diff --git a/commonsUI/src/commonMain/composeResources/values/strings.xml b/commonsUI/src/commonMain/composeResources/values/strings.xml index e50c6fd0f3..04871363cd 100644 --- a/commonsUI/src/commonMain/composeResources/values/strings.xml +++ b/commonsUI/src/commonMain/composeResources/values/strings.xml @@ -6243,4 +6243,6 @@ Play voice note Record a voice note Stop and send + Search or enter address + Untitled nApplet diff --git a/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/platform/AppPlatform.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/platform/AppPlatform.kt index 4fd7c0991c..fb0e9ad03b 100644 --- a/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/platform/AppPlatform.kt +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/platform/AppPlatform.kt @@ -24,9 +24,12 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.Stable import androidx.compose.runtime.staticCompositionLocalOf import androidx.compose.ui.Modifier +import com.vitorpamplona.amethyst.commons.favorites.FavoriteApp import com.vitorpamplona.amethyst.commons.model.navigation.Route import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.qrcode.ScanOutcome import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel +import com.vitorpamplona.quartz.concord.cord02Community.ImagePointer /** * The app shell's pieces that shared screens embed but only the front end can draw yet: the @@ -64,6 +67,42 @@ interface AppPlatform { modifier: Modifier, ) {} + /** The camera QR scanner, full screen. [onScan] says whether the payload was used. */ + @Composable + fun QrCodeScannerDialog( + onDismiss: () -> Unit, + onScan: (String) -> ScanOutcome, + ) {} + + /** Opens favorite apps and web links in the platform's app surface. */ + @Composable + fun rememberAppLauncher(): AppLauncher = AppLauncher.None + + /** + * The image model for a CORD-02 community picture: the plain URL for a url-only pointer, or a + * local copy of the decrypted blob for an encrypted one (null while it loads or if it fails). + */ + @Composable + fun rememberConcordImageModel( + pointer: ImagePointer?, + accountViewModel: AccountViewModel, + ): String? = pointer?.takeIf { !it.isResolvable() }?.url?.ifBlank { null } + + /** The cached icon of the napplet or nsite at [coordinate], if one was fetched. */ + @Composable + fun rememberNappletIconModel(coordinate: String): String? = null + + /** The icon a napplet manifest by [author] / [identifier] declares, if cached. */ + @Composable + fun rememberManifestIconModel( + author: String, + identifier: String, + ): String? = null + + /** The favicon the browser captured for [url]'s host, if any. */ + @Composable + fun rememberWebAppIconModel(url: String): String? = null + /** Draws nothing: previews, and front ends still wiring their pieces. */ object None : AppPlatform } @@ -96,3 +135,61 @@ fun GeohashLocationPickerContent( onConfirm: (String) -> Unit, modifier: Modifier = Modifier, ) = LocalAppPlatform.current.GeohashLocationPickerContent(initialGeohash, confirmLabel, onConfirm, modifier) + +@Composable +fun QrCodeScannerDialog( + onDismiss: () -> Unit, + onScan: (String) -> ScanOutcome, +) = LocalAppPlatform.current.QrCodeScannerDialog(onDismiss, onScan) + +@Composable +fun rememberAppLauncher(): AppLauncher = LocalAppPlatform.current.rememberAppLauncher() + +@Composable +fun rememberConcordImageModel( + pointer: ImagePointer?, + accountViewModel: AccountViewModel, +): String? = LocalAppPlatform.current.rememberConcordImageModel(pointer, accountViewModel) + +@Composable +fun rememberNappletIconModel(coordinate: String): String? = LocalAppPlatform.current.rememberNappletIconModel(coordinate) + +@Composable +fun rememberManifestIconModel( + author: String, + identifier: String, +): String? = LocalAppPlatform.current.rememberManifestIconModel(author, identifier) + +@Composable +fun rememberWebAppIconModel(url: String): String? = LocalAppPlatform.current.rememberWebAppIconModel(url) + +/** + * Opens a favorite app or a web link in the platform's app surface (on Android, the sandboxed + * full-screen WebView in its own task). + */ +interface AppLauncher { + /** [stillLoading] is shown when a Nostr app's defining event has not arrived yet. */ + fun launch( + app: FavoriteApp, + stillLoading: String, + ) + + /** [preferTor] forces Tor when available, e.g. for `.onion` hosts. */ + fun launchUrl( + url: String, + preferTor: Boolean = false, + ) + + /** Launches nothing: front ends without an app surface yet. */ + object None : AppLauncher { + override fun launch( + app: FavoriteApp, + stillLoading: String, + ) {} + + override fun launchUrl( + url: String, + preferTor: Boolean, + ) {} + } +} diff --git a/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/platform/LocalAppServices.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/platform/LocalAppServices.kt new file mode 100644 index 0000000000..b691c37bf7 --- /dev/null +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/platform/LocalAppServices.kt @@ -0,0 +1,30 @@ +/* + * 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.commons.ui.platform + +import androidx.compose.runtime.staticCompositionLocalOf +import com.vitorpamplona.amethyst.commons.service.AppServices + +/** + * The front end's [AppServices], installed at the composition root. No default: a screen that + * reads it outside an app root is a wiring bug, not something to paper over with empty stores. + */ +val LocalAppServices = staticCompositionLocalOf { error("No AppServices installed at the composition root") } diff --git a/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/qrcode/KeepScreenBrightAndAwake.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/qrcode/KeepScreenBrightAndAwake.kt new file mode 100644 index 0000000000..f87c641378 --- /dev/null +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/qrcode/KeepScreenBrightAndAwake.kt @@ -0,0 +1,34 @@ +/* + * 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.commons.ui.screen.loggedIn.qrcode + +import androidx.compose.runtime.Composable + +/** + * Pins the screen to full brightness, and awake, while a QR code is on it. + * + * Half of "this QR code will not scan" is the phone showing it. A dark-mode OLED at the + * auto-brightness the room asked for can put so little contrast between the black and white + * modules that the other camera's binarizer cannot separate them — and the person holding it has + * no idea that is the problem, because to a human eye the code looks perfectly clear. + */ +@Composable +expect fun KeepScreenBrightAndAwake() diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/qrcode/QrCodeDrawer.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/qrcode/QrCodeDrawer.kt similarity index 78% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/qrcode/QrCodeDrawer.kt rename to commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/qrcode/QrCodeDrawer.kt index faf17efbf8..d9de803a77 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/qrcode/QrCodeDrawer.kt +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/qrcode/QrCodeDrawer.kt @@ -18,7 +18,7 @@ * 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.qrcode +package com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.qrcode import androidx.compose.foundation.Canvas import androidx.compose.foundation.background @@ -42,11 +42,6 @@ import androidx.compose.ui.graphics.PathFillType import androidx.compose.ui.graphics.drawscope.DrawScope import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp -import com.google.zxing.EncodeHintType -import com.google.zxing.qrcode.decoder.ErrorCorrectionLevel -import com.google.zxing.qrcode.encoder.ByteMatrix -import com.google.zxing.qrcode.encoder.Encoder -import com.google.zxing.qrcode.encoder.QRCode import com.vitorpamplona.amethyst.commons.ui.theme.QuoteBorder import kotlin.math.min @@ -79,7 +74,7 @@ fun QrCodeDrawer( contents: String, modifier: Modifier = Modifier, ) { - val qrCode = remember(contents) { createQrCode(contents = contents) } + val matrix = remember(contents) { encodeQrMatrix(contents) } val foregroundColor = Color.Black @@ -91,64 +86,60 @@ fun QrCodeDrawer( .aspectRatio(1f) .background(Color.White), ) { - Canvas(modifier = Modifier.fillMaxSize()) { - // Calculate the height and width of each column/row - // Solve for the module size with the quiet zone measured in modules, so the whole code - // (zone included) is exactly as wide as the canvas. - val rowHeight = size.height / (qrCode.matrix.height + QR_QUIET_ZONE_MODULES * 2f) - val columnWidth = size.width / (qrCode.matrix.width + QR_QUIET_ZONE_MODULES * 2f) - // Scale the rounding with the module size. A fixed 20px radius is a gentle touch on - // a large code and a serious deformation on a small one: the finder patterns are what - // a decoder locates first, and rounding away a third of a module's worth of their - // corners is exactly the kind of damage that makes a code readable on screen and - // unreadable in a photo of that screen. - val radius = CornerRadius(min(columnWidth, rowHeight) * FINDER_CORNER_RADIUS_MODULES) - - // Draw all of the finder patterns required by the QR spec. Calculate the ratio - // of the number of rows/columns to the width and height - drawQrCodeFinders( - quietZonePx = columnWidth * QR_QUIET_ZONE_MODULES, - sideLength = size.width, - finderPatternSize = - Size( - width = columnWidth * FINDER_PATTERN_ROW_COUNT, - height = rowHeight * FINDER_PATTERN_ROW_COUNT, - ), - color = foregroundColor, - cornerRadius = radius, - ) - - // Draw data bits (encoded data part) - drawAllQrCodeDataBits( - quietZonePx = columnWidth * QR_QUIET_ZONE_MODULES, - bytes = qrCode.matrix, - size = - Size( - width = columnWidth, - height = rowHeight, - ), - color = foregroundColor, - ) + if (matrix != null) { + QrCanvas(matrix, foregroundColor) } } } -private typealias Coordinate = Pair +@Composable +private fun QrCanvas( + matrix: QrMatrix, + foregroundColor: Color, +) { + Canvas(modifier = Modifier.fillMaxSize()) { + // Calculate the height and width of each column/row + // Solve for the module size with the quiet zone measured in modules, so the whole code + // (zone included) is exactly as wide as the canvas. + val rowHeight = size.height / (matrix.height + QR_QUIET_ZONE_MODULES * 2f) + val columnWidth = size.width / (matrix.width + QR_QUIET_ZONE_MODULES * 2f) + // Scale the rounding with the module size. A fixed 20px radius is a gentle touch on + // a large code and a serious deformation on a small one: the finder patterns are what + // a decoder locates first, and rounding away a third of a module's worth of their + // corners is exactly the kind of damage that makes a code readable on screen and + // unreadable in a photo of that screen. + val radius = CornerRadius(min(columnWidth, rowHeight) * FINDER_CORNER_RADIUS_MODULES) -private fun createQrCode(contents: String): QRCode { - require(contents.isNotEmpty()) + // Draw all of the finder patterns required by the QR spec. Calculate the ratio + // of the number of rows/columns to the width and height + drawQrCodeFinders( + quietZonePx = columnWidth * QR_QUIET_ZONE_MODULES, + sideLength = size.width, + finderPatternSize = + Size( + width = columnWidth * FINDER_PATTERN_ROW_COUNT, + height = rowHeight * FINDER_PATTERN_ROW_COUNT, + ), + color = foregroundColor, + cornerRadius = radius, + ) - return Encoder.encode( - contents, - ErrorCorrectionLevel.Q, - mapOf( - EncodeHintType.CHARACTER_SET to "UTF-8", - EncodeHintType.MARGIN to QR_QUIET_ZONE_MODULES, - EncodeHintType.ERROR_CORRECTION to ErrorCorrectionLevel.Q, - ), - ) + // Draw data bits (encoded data part) + drawAllQrCodeDataBits( + quietZonePx = columnWidth * QR_QUIET_ZONE_MODULES, + bytes = matrix, + size = + Size( + width = columnWidth, + height = rowHeight, + ), + color = foregroundColor, + ) + } } +private typealias Coordinate = Pair + fun newPath(withPath: Path.() -> Unit) = Path().apply { fillType = PathFillType.EvenOdd @@ -157,7 +148,7 @@ fun newPath(withPath: Path.() -> Unit) = fun DrawScope.drawAllQrCodeDataBits( quietZonePx: Float, - bytes: ByteMatrix, + bytes: QrMatrix, size: Size, color: Color, ) { @@ -197,7 +188,7 @@ fun DrawScope.drawAllQrCodeDataBits( val newSize = Size(size.width + 0.5f, size.height + 0.5f) for (y in section.first.second until section.second.second) { for (x in section.first.first until section.second.first) { - if (bytes[x, y] == 1.toByte()) { + if (bytes[x, y]) { drawPath( color = color, path = diff --git a/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/qrcode/QrMatrix.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/qrcode/QrMatrix.kt new file mode 100644 index 0000000000..359f1d16c1 --- /dev/null +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/qrcode/QrMatrix.kt @@ -0,0 +1,39 @@ +/* + * 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.commons.ui.screen.loggedIn.qrcode + +/** The dark/light modules of an encoded QR code, row-major, without its quiet zone. */ +class QrMatrix( + val width: Int, + val height: Int, + private val modules: BooleanArray, +) { + operator fun get( + x: Int, + y: Int, + ): Boolean = modules[y * width + x] +} + +/** + * Encodes [contents] (UTF-8, error correction Q) into its module grid, or null when this + * platform has no encoder yet or the payload does not fit a QR code. + */ +expect fun encodeQrMatrix(contents: String): QrMatrix? diff --git a/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/qrcode/ScanOutcome.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/qrcode/ScanOutcome.kt new file mode 100644 index 0000000000..e3fcb38532 --- /dev/null +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/qrcode/ScanOutcome.kt @@ -0,0 +1,30 @@ +/* + * 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.commons.ui.screen.loggedIn.qrcode + +/** What the caller did with a decoded payload — and therefore what the scanner does next. */ +enum class ScanOutcome { + /** The caller acted on it. Close the scanner. */ + Handled, + + /** Decoded fine, but this screen has nothing to do with it. Explain, and keep scanning. */ + NotSupported, +} diff --git a/commonsUI/src/iosMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/qrcode/KeepScreenBrightAndAwake.ios.kt b/commonsUI/src/iosMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/qrcode/KeepScreenBrightAndAwake.ios.kt new file mode 100644 index 0000000000..a144adff19 --- /dev/null +++ b/commonsUI/src/iosMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/qrcode/KeepScreenBrightAndAwake.ios.kt @@ -0,0 +1,27 @@ +/* + * 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.commons.ui.screen.loggedIn.qrcode + +import androidx.compose.runtime.Composable + +/** Not wired on iOS yet. */ +@Composable +actual fun KeepScreenBrightAndAwake() {} diff --git a/commonsUI/src/iosMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/qrcode/QrMatrix.ios.kt b/commonsUI/src/iosMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/qrcode/QrMatrix.ios.kt new file mode 100644 index 0000000000..a7052a9868 --- /dev/null +++ b/commonsUI/src/iosMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/qrcode/QrMatrix.ios.kt @@ -0,0 +1,24 @@ +/* + * 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.commons.ui.screen.loggedIn.qrcode + +/** No encoder on iOS yet: the drawer shows its blank frame. */ +actual fun encodeQrMatrix(contents: String): QrMatrix? = null diff --git a/commonsUI/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/qrcode/QrMatrix.jvmAndroid.kt b/commonsUI/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/qrcode/QrMatrix.jvmAndroid.kt new file mode 100644 index 0000000000..2c60a855c0 --- /dev/null +++ b/commonsUI/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/qrcode/QrMatrix.jvmAndroid.kt @@ -0,0 +1,52 @@ +/* + * 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.commons.ui.screen.loggedIn.qrcode + +import com.google.zxing.EncodeHintType +import com.google.zxing.WriterException +import com.google.zxing.qrcode.decoder.ErrorCorrectionLevel +import com.google.zxing.qrcode.encoder.Encoder + +actual fun encodeQrMatrix(contents: String): QrMatrix? { + if (contents.isEmpty()) return null + + val code = + try { + Encoder.encode( + contents, + ErrorCorrectionLevel.Q, + mapOf( + EncodeHintType.CHARACTER_SET to "UTF-8", + EncodeHintType.MARGIN to QR_QUIET_ZONE_MODULES, + EncodeHintType.ERROR_CORRECTION to ErrorCorrectionLevel.Q, + ), + ) + } catch (e: WriterException) { + return null + } + + val matrix = code.matrix + return QrMatrix( + width = matrix.width, + height = matrix.height, + modules = BooleanArray(matrix.width * matrix.height) { matrix[it % matrix.width, it / matrix.width] == 1.toByte() }, + ) +} diff --git a/commonsUI/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons/viewmodels/AccountViewModelPreviews.jvmAndroid.kt b/commonsUI/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons/viewmodels/AccountViewModelPreviews.jvmAndroid.kt index 4c9a3d0b40..a7b91243e5 100644 --- a/commonsUI/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons/viewmodels/AccountViewModelPreviews.jvmAndroid.kt +++ b/commonsUI/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons/viewmodels/AccountViewModelPreviews.jvmAndroid.kt @@ -39,6 +39,8 @@ import com.vitorpamplona.amethyst.commons.service.lnurl.LnurlHttpTransport import com.vitorpamplona.amethyst.commons.service.pow.PoWJobFailure import com.vitorpamplona.amethyst.commons.state.UiSettingsState import com.vitorpamplona.amethyst.commons.tor.MoneyOpRelayRouting +import com.vitorpamplona.amethyst.commons.tor.TorRelayEvaluation +import com.vitorpamplona.amethyst.commons.tor.TorRelaySettings import com.vitorpamplona.amethyst.commons.tor.TorSettingsFlow import com.vitorpamplona.amethyst.commons.tor.TorType import com.vitorpamplona.quartz.nip01Core.core.HexKey @@ -46,15 +48,20 @@ import com.vitorpamplona.quartz.nip01Core.crypto.KeyPair import com.vitorpamplona.quartz.nip01Core.relay.client.EmptyNostrClient import com.vitorpamplona.quartz.nip01Core.relay.client.accessories.RelayOfflineTracker import com.vitorpamplona.quartz.nip01Core.relay.client.auth.EmptyIAuthStatus +import com.vitorpamplona.quartz.nip01Core.relay.client.auth.RelayAuthSnapshot import com.vitorpamplona.quartz.nip01Core.relay.client.stats.RelayStats +import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl import com.vitorpamplona.quartz.nip01Core.relay.sockets.okhttp.BasicOkHttpWebSocket import com.vitorpamplona.quartz.nip01Core.signers.NostrSignerInternal import com.vitorpamplona.quartz.nip03Timestamp.EmptyOtsResolverBuilder import com.vitorpamplona.quartz.nip05DnsIdentifiers.EmptyNip05Client import com.vitorpamplona.quartz.nip60Cashu.mintApi.OkHttpMintTransport import com.vitorpamplona.quartz.utils.Hex +import kotlinx.collections.immutable.PersistentMap +import kotlinx.collections.immutable.persistentMapOf import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.emptyFlow import kotlinx.coroutines.flow.flowOf import okhttp3.OkHttpClient @@ -189,6 +196,8 @@ private object PreviewAccountViewModelHost : AccountViewModelHost { override val localBlossomCacheAvailable: Flow = flowOf(false) override val powPublishFailures: Flow = emptyFlow() override val relayStats = RelayStats(EmptyNostrClient()) + override val torRelayEvaluation: StateFlow = MutableStateFlow(TorRelayEvaluation(TorRelaySettings())) + override val relayAuthState: StateFlow> = MutableStateFlow(persistentMapOf()) override val websocketBuilder = BasicOkHttpWebSocket.Builder { OkHttpClient() } override val lnurlTransport = LnurlHttpTransport { throw IllegalStateException("Previews do not reach the network") } override val moneyOpRelays = MoneyOpRelayRouting.None diff --git a/commonsUI/src/jvmMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/qrcode/KeepScreenBrightAndAwake.jvm.kt b/commonsUI/src/jvmMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/qrcode/KeepScreenBrightAndAwake.jvm.kt new file mode 100644 index 0000000000..3412c7355b --- /dev/null +++ b/commonsUI/src/jvmMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/qrcode/KeepScreenBrightAndAwake.jvm.kt @@ -0,0 +1,27 @@ +/* + * 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.commons.ui.screen.loggedIn.qrcode + +import androidx.compose.runtime.Composable + +/** Desktop monitors do not dim for a QR code the way a phone does; nothing to pin. */ +@Composable +actual fun KeepScreenBrightAndAwake() {} diff --git a/tools/strings-migrate/__pycache__/fix_escapes.cpython-311.pyc b/tools/strings-migrate/__pycache__/fix_escapes.cpython-311.pyc index c700f97938..8e90cd5b8b 100644 Binary files a/tools/strings-migrate/__pycache__/fix_escapes.cpython-311.pyc and b/tools/strings-migrate/__pycache__/fix_escapes.cpython-311.pyc differ diff --git a/tools/strings-migrate/__pycache__/migrate.cpython-311.pyc b/tools/strings-migrate/__pycache__/migrate.cpython-311.pyc index 5e750571e7..7d06e126e4 100644 Binary files a/tools/strings-migrate/__pycache__/migrate.cpython-311.pyc and b/tools/strings-migrate/__pycache__/migrate.cpython-311.pyc differ