mirror of
https://github.com/vitorpamplona/amethyst.git
synced 2026-10-05 19:28:25 +00:00
refactor: cut shared-screen blockers (step 6 wave 3 seams)
- String helpers: files that only read Compose resources import the commons stringRes, not the app's Android-resource overloads. browser_address_hint and napplet_untitled get Compose copies (every locale) so the browser and connected-apps screens stop reading commons' Android R. - ReactionsRow/LikeReaction/ZapReaction and LoadCityName are imported through the note-platform shims instead of the app implementations. - QR: the drawer moves to commonsUI with the encoding behind encodeQrMatrix (zxing core on JVM/Android, nothing yet on iOS); KeepScreenBrightAndAwake becomes expect/actual; ScanOutcome is shared and the camera scanner is an AppPlatform slot. - AppPlatform gains the app launcher and the Concord/napplet/manifest/favicon image models. favoriteCoordinateOf moves to commons. - New AppServices port (favorites, browser history and icons, napplet and signer permissions, Tor settings) installed as LocalAppServices; AccountViewModelHost gains the Tor relay evaluation and NIP-42 auth state. Screens read these instead of Amethyst.instance. - uriToRoute and its helpers leave MainActivity for UriToRoute.kt and decode with the KMP UrlEncoder; findQueryParameterValue leaves AppNavigation. - Settings rows split out of AppSettingsScreen; formatGrouped replaces NumberFormat; Uuid replaces java.util.UUID; javaClass and toBigDecimalOrNull swapped for KMP forms. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01S7FuNBSKiyVecARSoE4B9P
This commit is contained in:
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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=<hex>&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:<origin>` 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:<groupHex>?account=<npub>
|
||||
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=<code>`; 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 `<relay>'<groupId>[?code=<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/<naddr>#<fragment>`). 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://<host>/invite/<token>`) — 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
|
||||
}
|
||||
|
||||
@@ -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=<hex>&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:<origin>` 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:<groupHex>?account=<npub>
|
||||
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=<code>`; 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 `<relay>'<groupId>[?code=<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/<naddr>#<fragment>`). 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://<host>/invite/<token>`) — 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
|
||||
}
|
||||
@@ -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:<hex>?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 }
|
||||
}
|
||||
|
||||
@@ -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:<hex>?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 }
|
||||
}
|
||||
+6
-5
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+1
-1
@@ -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
|
||||
|
||||
+1
-1
@@ -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)
|
||||
|
||||
+1
-1
@@ -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
|
||||
|
||||
+2
-2
@@ -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
|
||||
|
||||
@@ -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()) },
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
+58
-1
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
|
||||
+10
@@ -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<TorRelayEvaluation> get() = modules.torEvaluatorFlow.flow
|
||||
|
||||
override val relayAuthState: StateFlow<PersistentMap<NormalizedRelayUrl, RelayAuthSnapshot>>
|
||||
get() = modules.authCoordinator.receiver.authStateFlow
|
||||
|
||||
override val savedAccounts: Flow<Set<HexKey>> =
|
||||
LocalPreferences
|
||||
.accountsFlow()
|
||||
|
||||
+2
-2
@@ -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
|
||||
|
||||
+1
-1
@@ -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
|
||||
|
||||
+1
-1
@@ -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
|
||||
|
||||
+26
-23
@@ -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<Note>.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<String>,
|
||||
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) {
|
||||
|
||||
+5
-5
@@ -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,
|
||||
|
||||
+1
-2
@@ -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)
|
||||
|
||||
+1
-1
@@ -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
|
||||
|
||||
+1
-1
@@ -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
|
||||
|
||||
+1
-1
@@ -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
|
||||
|
||||
+1
-1
@@ -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
|
||||
|
||||
|
||||
+1
-1
@@ -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
|
||||
|
||||
+1
-1
@@ -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
|
||||
|
||||
+1
@@ -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
|
||||
|
||||
+1
@@ -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
|
||||
|
||||
+1
-1
@@ -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
|
||||
|
||||
/**
|
||||
|
||||
+1
-1
@@ -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
|
||||
|
||||
+1
-1
@@ -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
|
||||
|
||||
+2
-2
@@ -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
|
||||
|
||||
+2
-2
@@ -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
|
||||
|
||||
+2
-2
@@ -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
|
||||
|
||||
|
||||
+2
-1
@@ -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}"
|
||||
|
||||
+11
-10
@@ -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)
|
||||
}
|
||||
|
||||
+2
-2
@@ -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
|
||||
|
||||
+1
-1
@@ -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
|
||||
|
||||
+1
-1
@@ -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
|
||||
|
||||
+1
-1
@@ -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
|
||||
|
||||
+1
-1
@@ -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
|
||||
|
||||
+1
-1
@@ -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
|
||||
|
||||
/**
|
||||
|
||||
+1
-1
@@ -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
|
||||
|
||||
+2
-2
@@ -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
|
||||
|
||||
+9
-8
@@ -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<List<ConnectedAppEntry>?>(null) }
|
||||
var nappletAuthors by remember { mutableStateOf<Set<HexKey>>(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),
|
||||
|
||||
+2
-2
@@ -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
|
||||
|
||||
+1
-1
@@ -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
|
||||
|
||||
+1
-1
@@ -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
|
||||
|
||||
|
||||
+1
-1
@@ -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
|
||||
|
||||
|
||||
+1
-1
@@ -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
|
||||
|
||||
|
||||
+3
-3
@@ -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)
|
||||
|
||||
+1
-1
@@ -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
|
||||
|
||||
|
||||
+2
-2
@@ -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
|
||||
|
||||
+2
@@ -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
|
||||
|
||||
+1
-9
@@ -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.
|
||||
*
|
||||
|
||||
+2
-2
@@ -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,
|
||||
)
|
||||
|
||||
+1
-2
@@ -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 }
|
||||
|
||||
+3
-4
@@ -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<HexKey>()
|
||||
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) },
|
||||
)
|
||||
|
||||
+4
-4
@@ -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
|
||||
|
||||
+1
-1
@@ -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
|
||||
|
||||
-143
@@ -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 <T> SegmentedChoiceTile(
|
||||
icon: MaterialSymbol,
|
||||
title: StringResource,
|
||||
description: StringResource,
|
||||
options: List<T>,
|
||||
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<TitleExplainer>,
|
||||
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,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+4
-4
@@ -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, "")
|
||||
|
||||
+183
@@ -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 <T> SegmentedChoiceTile(
|
||||
icon: MaterialSymbol,
|
||||
title: StringResource,
|
||||
description: StringResource,
|
||||
options: List<T>,
|
||||
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<TitleExplainer>,
|
||||
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,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
+1
-1
@@ -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
|
||||
|
||||
+1
-1
@@ -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
|
||||
|
||||
|
||||
+1
-1
@@ -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
|
||||
|
||||
+1
-1
@@ -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
|
||||
|
||||
+1
-1
@@ -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
|
||||
|
||||
+1
-1
@@ -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
|
||||
|
||||
+6
-6
@@ -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)),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
+1
-1
@@ -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)
|
||||
|
||||
+6
-6
@@ -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,
|
||||
)
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
}
|
||||
|
||||
+39
@@ -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"
|
||||
}
|
||||
+51
@@ -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
|
||||
}
|
||||
+24
@@ -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
|
||||
+11
@@ -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<TorRelayEvaluation>
|
||||
|
||||
/** NIP-42 authentication state per relay, across every logged-in account. */
|
||||
val relayAuthState: StateFlow<PersistentMap<NormalizedRelayUrl, RelayAuthSnapshot>>
|
||||
|
||||
/** Public keys of every account saved on this device. */
|
||||
val savedAccounts: Flow<Set<HexKey>>
|
||||
|
||||
|
||||
+31
@@ -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()
|
||||
}
|
||||
+25
@@ -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)
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+2
-10
@@ -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 —
|
||||
@@ -6486,4 +6486,6 @@
|
||||
<string name="cordn_voice_play">Přehrát hlasovou zprávu</string>
|
||||
<string name="cordn_voice_record">Nahrát hlasovou zprávu</string>
|
||||
<string name="cordn_voice_stop">Zastavit a odeslat</string>
|
||||
<string name="browser_address_hint">Hledat nebo zadat adresu</string>
|
||||
<string name="napplet_untitled">Nepojmenovaný nApplet</string>
|
||||
</resources>
|
||||
|
||||
@@ -3034,4 +3034,6 @@
|
||||
<string name="the_receiver_s_lightning_service_at_is_not_available_it_was_calculated_from_the_lightning_address_error_check_if_the_server_is_up_and_if_the_lightning_address_is_correct">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</string>
|
||||
<string name="unable_to_create_a_lightning_invoice_before_sending_the_zap_element_pr_not_found_in_the_resulting_json_with_user">Kann keine Blitzrechnung von %1$s erstellen: Element pr nicht im resultierenden JSON gefunden.</string>
|
||||
<string name="unable_to_create_a_lightning_invoice_before_sending_the_zap_the_receiver_s_lightning_wallet_sent_the_following_error_with_user">Blitzrechnung konnte nicht erstellt werden. Nachricht von %1$s: %2$s</string>
|
||||
<string name="browser_address_hint">Suchen oder Adresse eingeben</string>
|
||||
<string name="napplet_untitled">Unbenanntes nApplet</string>
|
||||
</resources>
|
||||
|
||||
@@ -2995,4 +2995,6 @@
|
||||
<string name="the_receiver_s_lightning_service_at_is_not_available_it_was_calculated_from_the_lightning_address_error_check_if_the_server_is_up_and_if_the_lightning_address_is_correct">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</string>
|
||||
<string name="unable_to_create_a_lightning_invoice_before_sending_the_zap_element_pr_not_found_in_the_resulting_json_with_user">Ne eblas krei Lightning-fakturon de %1$s: Elemento pr ne trovita en la rezulta JSON.</string>
|
||||
<string name="unable_to_create_a_lightning_invoice_before_sending_the_zap_the_receiver_s_lightning_wallet_sent_the_following_error_with_user">Ne eblas krei Lightning-fakturon. Mesaĝo de %1$s: %2$s</string>
|
||||
<string name="browser_address_hint">Serĉi aŭ enigi adreson</string>
|
||||
<string name="napplet_untitled">Sennoma nApplet</string>
|
||||
</resources>
|
||||
|
||||
@@ -2995,4 +2995,6 @@
|
||||
<string name="the_receiver_s_lightning_service_at_is_not_available_it_was_calculated_from_the_lightning_address_error_check_if_the_server_is_up_and_if_the_lightning_address_is_correct">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.</string>
|
||||
<string name="unable_to_create_a_lightning_invoice_before_sending_the_zap_element_pr_not_found_in_the_resulting_json_with_user">No se pudo crear una factura de Lightning desde %1$s: no se encontró el elemento pr en el JSON resultante.</string>
|
||||
<string name="unable_to_create_a_lightning_invoice_before_sending_the_zap_the_receiver_s_lightning_wallet_sent_the_following_error_with_user">No se pudo crear una factura de Lightning. Mensaje de %1$s: %2$s.</string>
|
||||
<string name="browser_address_hint">Buscar o introducir dirección</string>
|
||||
<string name="napplet_untitled">NApplet sin título</string>
|
||||
</resources>
|
||||
|
||||
@@ -2995,4 +2995,6 @@
|
||||
<string name="the_receiver_s_lightning_service_at_is_not_available_it_was_calculated_from_the_lightning_address_error_check_if_the_server_is_up_and_if_the_lightning_address_is_correct">خدمات لایتنینگ %1$s گیرنده دردسترس نیست. از آدرس لایتنینگ "%2$s" محاسبه شد. خطا: %3$s. بررسی کنید آیا سرور کار می کند و آیا آدرس لایتنینگ صحیح است.</string>
|
||||
<string name="unable_to_create_a_lightning_invoice_before_sending_the_zap_element_pr_not_found_in_the_resulting_json_with_user">نمی توان صورتحساب لایتنینگ از %1$s ساخت. المنت pr در JSON بدست آمده یافت نشد.</string>
|
||||
<string name="unable_to_create_a_lightning_invoice_before_sending_the_zap_the_receiver_s_lightning_wallet_sent_the_following_error_with_user">صورت حساب لایتنینگ ساخته نشد. پیغام از %1$s: %2$s</string>
|
||||
<string name="browser_address_hint">جستجو یا وارد کردن آدرس</string>
|
||||
<string name="napplet_untitled">nApplet بیعنوان</string>
|
||||
</resources>
|
||||
|
||||
@@ -2995,4 +2995,6 @@
|
||||
<string name="the_receiver_s_lightning_service_at_is_not_available_it_was_calculated_from_the_lightning_address_error_check_if_the_server_is_up_and_if_the_lightning_address_is_correct">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</string>
|
||||
<string name="unable_to_create_a_lightning_invoice_before_sending_the_zap_element_pr_not_found_in_the_resulting_json_with_user">Impossible de créer une facture lightning avec %1$s. Élément pr introuvable dans le JSON résultant.</string>
|
||||
<string name="unable_to_create_a_lightning_invoice_before_sending_the_zap_the_receiver_s_lightning_wallet_sent_the_following_error_with_user">Impossible de créer une facture lightning. Message de %1$s: %2$s</string>
|
||||
<string name="browser_address_hint">Rechercher ou saisir une adresse</string>
|
||||
<string name="napplet_untitled">nApplet sans titre</string>
|
||||
</resources>
|
||||
|
||||
@@ -2958,4 +2958,6 @@
|
||||
<string name="the_receiver_s_lightning_service_at_is_not_available_it_was_calculated_from_the_lightning_address_error_check_if_the_server_is_up_and_if_the_lightning_address_is_correct">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</string>
|
||||
<string name="unable_to_create_a_lightning_invoice_before_sending_the_zap_element_pr_not_found_in_the_resulting_json_with_user">Tidak dapat membuat tagihan Lightning dari %1$s: Elemen pr tidak ditemukan dalam JSON yang dihasilkan.</string>
|
||||
<string name="unable_to_create_a_lightning_invoice_before_sending_the_zap_the_receiver_s_lightning_wallet_sent_the_following_error_with_user">Tidak dapat membuat tagihan Lightning. Pesan dari %1$s: %2$s</string>
|
||||
<string name="browser_address_hint">Cari atau masukkan alamat</string>
|
||||
<string name="napplet_untitled">nApplet Tanpa Judul</string>
|
||||
</resources>
|
||||
|
||||
@@ -2958,4 +2958,6 @@
|
||||
<string name="the_receiver_s_lightning_service_at_is_not_available_it_was_calculated_from_the_lightning_address_error_check_if_the_server_is_up_and_if_the_lightning_address_is_correct">%1$s の受信者の Lightning サービスが利用できません。Lightning アドレス "%2$s" から計算されました。エラー: %3$s。サーバーが稼働しているか、Lightning アドレスが正しいかを確認してください</string>
|
||||
<string name="unable_to_create_a_lightning_invoice_before_sending_the_zap_element_pr_not_found_in_the_resulting_json_with_user">%1$s から Lightning 請求書を作成できませんでした: 結果の JSON に要素 pr が見つかりませんでした。</string>
|
||||
<string name="unable_to_create_a_lightning_invoice_before_sending_the_zap_the_receiver_s_lightning_wallet_sent_the_following_error_with_user">Lightning 請求書を作成できませんでした。%1$s からのメッセージ: %2$s</string>
|
||||
<string name="browser_address_hint">検索またはアドレスを入力</string>
|
||||
<string name="napplet_untitled">無題の nApplet</string>
|
||||
</resources>
|
||||
|
||||
@@ -2995,4 +2995,6 @@
|
||||
<string name="the_receiver_s_lightning_service_at_is_not_available_it_was_calculated_from_the_lightning_address_error_check_if_the_server_is_up_and_if_the_lightning_address_is_correct">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.</string>
|
||||
<string name="unable_to_create_a_lightning_invoice_before_sending_the_zap_element_pr_not_found_in_the_resulting_json_with_user">Kon geen Lightning-invoice aanmaken van %1$s: element "pr" niet gevonden in JSON.</string>
|
||||
<string name="unable_to_create_a_lightning_invoice_before_sending_the_zap_the_receiver_s_lightning_wallet_sent_the_following_error_with_user">Kon geen Lightning-invoice aanmaken. Bericht van %1$s: %2$s</string>
|
||||
<string name="browser_address_hint">Zoeken of adres invoeren</string>
|
||||
<string name="napplet_untitled">Naamloze nApplet</string>
|
||||
</resources>
|
||||
|
||||
@@ -2995,4 +2995,6 @@
|
||||
<string name="the_receiver_s_lightning_service_at_is_not_available_it_was_calculated_from_the_lightning_address_error_check_if_the_server_is_up_and_if_the_lightning_address_is_correct">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</string>
|
||||
<string name="unable_to_create_a_lightning_invoice_before_sending_the_zap_element_pr_not_found_in_the_resulting_json_with_user">Kan geen Lightning invoice aanmaken van %1$s: PR-element niet gevonden in de JSON.</string>
|
||||
<string name="unable_to_create_a_lightning_invoice_before_sending_the_zap_the_receiver_s_lightning_wallet_sent_the_following_error_with_user">Kan geen Lightning invoice aanmaken. Bericht van %1$s: %2$s</string>
|
||||
<string name="browser_address_hint">Zoeken of adres invoeren</string>
|
||||
<string name="napplet_untitled">Naamloze nApplet</string>
|
||||
</resources>
|
||||
|
||||
@@ -3069,4 +3069,6 @@
|
||||
<string name="the_receiver_s_lightning_service_at_is_not_available_it_was_calculated_from_the_lightning_address_error_check_if_the_server_is_up_and_if_the_lightning_address_is_correct">Lightning-сервис получателя по адресу %1$s недоступен. Он был вычислен из Lightning-адреса "%2$s". Ошибка: %3$s. Проверьте, работает ли сервер и правильно ли указан Lightning-адрес</string>
|
||||
<string name="unable_to_create_a_lightning_invoice_before_sending_the_zap_element_pr_not_found_in_the_resulting_json_with_user">Не удалось создать Lightning-счёт от %1$s: элемент pr не найден в полученном JSON.</string>
|
||||
<string name="unable_to_create_a_lightning_invoice_before_sending_the_zap_the_receiver_s_lightning_wallet_sent_the_following_error_with_user">Не удалось создать Lightning-счёт. Сообщение от %1$s: %2$s</string>
|
||||
<string name="browser_address_hint">Поиск или введите адрес</string>
|
||||
<string name="napplet_untitled">nApplet без названия</string>
|
||||
</resources>
|
||||
|
||||
@@ -2995,4 +2995,6 @@
|
||||
<string name="the_receiver_s_lightning_service_at_is_not_available_it_was_calculated_from_the_lightning_address_error_check_if_the_server_is_up_and_if_the_lightning_address_is_correct">%1$s-ல் பெறுநரின் lightning சேவை கிடைக்கவில்லை. இது lightning முகவரி "%2$s" இலிருந்து கணக்கிடப்பட்டது. பிழை: %3$s. சர்வர் இயங்குகிறதா மற்றும் lightning முகவரி சரியானதா என சரிபாருங்கள்</string>
|
||||
<string name="unable_to_create_a_lightning_invoice_before_sending_the_zap_element_pr_not_found_in_the_resulting_json_with_user">%1$s-இலிருந்து lightning invoice உருவாக்க முடியவில்லை: முடிவு JSON-ல் pr உறுப்பு கிடைக்கவில்லை.</string>
|
||||
<string name="unable_to_create_a_lightning_invoice_before_sending_the_zap_the_receiver_s_lightning_wallet_sent_the_following_error_with_user">Lightning invoice உருவாக்க முடியவில்லை. %1$s-இலிருந்து செய்தி: %2$s</string>
|
||||
<string name="browser_address_hint">தேடவும் அல்லது முகவரியை உள்ளிடவும்</string>
|
||||
<string name="napplet_untitled">தலைப்பற்ற nApplet</string>
|
||||
</resources>
|
||||
|
||||
@@ -2958,4 +2958,6 @@
|
||||
<string name="the_receiver_s_lightning_service_at_is_not_available_it_was_calculated_from_the_lightning_address_error_check_if_the_server_is_up_and_if_the_lightning_address_is_correct">บริการ Lightning ของ %1$s แลจะไม่พร้อมใช้งานจาก "%2$s". Error: %3$s. โปรดตรวจสอบผู้ให้บริการ หรือที่อยู่ Lightning ให้ถูกตต้อง</string>
|
||||
<string name="unable_to_create_a_lightning_invoice_before_sending_the_zap_element_pr_not_found_in_the_resulting_json_with_user">ไม่สามารถสร้างบิลชำระในการส่ง zap จาก %1$s เนื่องจากไม่พบ PR บน JSON</string>
|
||||
<string name="unable_to_create_a_lightning_invoice_before_sending_the_zap_the_receiver_s_lightning_wallet_sent_the_following_error_with_user">ไม่สามารถสร้าง lightning invoice ข้อความจาก %1$s: %2$s</string>
|
||||
<string name="browser_address_hint">ค้นหาหรือป้อนที่อยู่</string>
|
||||
<string name="napplet_untitled">nApplet ไม่มีชื่อ</string>
|
||||
</resources>
|
||||
|
||||
@@ -2995,4 +2995,6 @@
|
||||
<string name="the_receiver_s_lightning_service_at_is_not_available_it_was_calculated_from_the_lightning_address_error_check_if_the_server_is_up_and_if_the_lightning_address_is_correct">%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.</string>
|
||||
<string name="unable_to_create_a_lightning_invoice_before_sending_the_zap_element_pr_not_found_in_the_resulting_json_with_user">%1$s adresinden Lightning faturası oluşturulamadı: Sonuçtaki JSON'da pr öğesi bulunamadı.</string>
|
||||
<string name="unable_to_create_a_lightning_invoice_before_sending_the_zap_the_receiver_s_lightning_wallet_sent_the_following_error_with_user">Lightning faturası oluşturulamadı. %1$s'den gelen mesaj: %2$s</string>
|
||||
<string name="browser_address_hint">Ara veya adres gir</string>
|
||||
<string name="napplet_untitled">Başlıksız nApplet</string>
|
||||
</resources>
|
||||
|
||||
@@ -3069,4 +3069,6 @@
|
||||
<string name="the_receiver_s_lightning_service_at_is_not_available_it_was_calculated_from_the_lightning_address_error_check_if_the_server_is_up_and_if_the_lightning_address_is_correct">Lightning-сервіс отримувача за адресою %1$s недоступний. Він був розрахований із lightning-адреси "%2$s". Помилка: %3$s. Перевірте, чи працює сервер і чи правильна lightning-адреса</string>
|
||||
<string name="unable_to_create_a_lightning_invoice_before_sending_the_zap_element_pr_not_found_in_the_resulting_json_with_user">Неможливо створити lightning-рахунок від %1$s: Елемент pr не знайдено у відповідному JSON.</string>
|
||||
<string name="unable_to_create_a_lightning_invoice_before_sending_the_zap_the_receiver_s_lightning_wallet_sent_the_following_error_with_user">Неможливо створити lightning-рахунок. Повідомлення від %1$s: %2$s</string>
|
||||
<string name="browser_address_hint">Пошук або введіть адресу</string>
|
||||
<string name="napplet_untitled">NApplet без назви</string>
|
||||
</resources>
|
||||
|
||||
@@ -2999,4 +2999,6 @@
|
||||
<string name="the_receiver_s_lightning_service_at_is_not_available_it_was_calculated_from_the_lightning_address_error_check_if_the_server_is_up_and_if_the_lightning_address_is_correct">%1$s 的接收者闪电服务不可用。它是根据雷电地址"%2$s"计算的。 错误: %3$s。请检查服务器是否已经上线,闪电地址是否正确</string>
|
||||
<string name="unable_to_create_a_lightning_invoice_before_sending_the_zap_element_pr_not_found_in_the_resulting_json_with_user">无法从 %1$s 创建闪电发票:在生成的 JSON 中找不到付款要求元素。</string>
|
||||
<string name="unable_to_create_a_lightning_invoice_before_sending_the_zap_the_receiver_s_lightning_wallet_sent_the_following_error_with_user">无法创建闪电发票。来自 %1$s 的消息:%2$s</string>
|
||||
<string name="browser_address_hint">搜索或输入地址</string>
|
||||
<string name="napplet_untitled">未命名nApplet</string>
|
||||
</resources>
|
||||
|
||||
@@ -6243,4 +6243,6 @@
|
||||
<string name="cordn_voice_play">Play voice note</string>
|
||||
<string name="cordn_voice_record">Record a voice note</string>
|
||||
<string name="cordn_voice_stop">Stop and send</string>
|
||||
<string name="browser_address_hint">Search or enter address</string>
|
||||
<string name="napplet_untitled">Untitled nApplet</string>
|
||||
</resources>
|
||||
|
||||
+97
@@ -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,
|
||||
) {}
|
||||
}
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user