refactor: cut more shared-screen blockers (step 6 wave 5 seams)

- PlatformBackHandler (commonsUI): AndroidX BackHandler on Android, Compose Multiplatform's
  ui-backhandler on desktop and iOS. The app's 21 BackHandler users switch to it.
  commonsUI adds androidx.activity:activity-compose (androidMain) and
  org.jetbrains.compose.ui:ui-backhandler (skikoMain); both Apache-2.0, both already shipped.
- rememberLongNotice joins rememberShortNotice, so Toast.LENGTH_LONG messages keep their
  duration; four screens drop their Toasts for the shared notices.
- The wallet screens use formatGrouped instead of NumberFormat and the shared Clipboard
  getText/setText extensions instead of ClipboardManager.
- formatMonthDayTime (commonsUI) builds "May 28 14:32" from DateSkeletonFormatter("MMMd") and the
  system-clock time-of-day formatter; the two transaction lists use it.
- UsageKeys uses quartz's ConcurrentMap. LocalCacheHost gains a UsageCounter (no-op by default,
  the Android host feeds its resource-usage ledger), so reconnectPoolAfterJoin no longer reaches
  for Amethyst.instance.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01S7FuNBSKiyVecARSoE4B9P
This commit is contained in:
Claude
2026-09-29 06:57:24 +00:00
parent fc212b79f8
commit 3a391c835f
47 changed files with 301 additions and 125 deletions
@@ -24,6 +24,7 @@ import com.vitorpamplona.amethyst.commons.model.cache.FileSystemNip95BlobStore
import com.vitorpamplona.amethyst.commons.model.cache.LocalCache
import com.vitorpamplona.amethyst.commons.model.cache.LocalCacheHost
import com.vitorpamplona.amethyst.commons.model.cache.Nip95BlobStore
import com.vitorpamplona.amethyst.commons.service.UsageCounter
import com.vitorpamplona.quartz.nip01Core.core.HexKey
import com.vitorpamplona.quartz.nip01Core.relay.client.stats.RelayStats
import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl
@@ -54,6 +55,8 @@ class AmethystLocalCacheHost(
override val relayStats: RelayStats get() = modules.relayStats
override val usageCounter: UsageCounter = UsageCounter { key, amount -> modules.resourceUsage.add(key, amount) }
override fun relaySelfPubKey(relay: NormalizedRelayUrl): HexKey? = modules.nip11Cache.getFromCache(relay).self
override fun relayInfo(relay: NormalizedRelayUrl): Nip11RelayInformation = modules.nip11Cache.getFromCache(relay)
@@ -30,7 +30,7 @@ import com.vitorpamplona.quartz.nip01Core.relay.commands.toClient.MachineReadabl
import com.vitorpamplona.quartz.nip01Core.relay.commands.toClient.MachineReadablePrefix.RESTRICTED
import com.vitorpamplona.quartz.nip01Core.relay.commands.toClient.MachineReadablePrefix.UNSUPPORTED
import com.vitorpamplona.quartz.nip66RelayMonitor.reachability.RelayObserver
import java.util.concurrent.ConcurrentHashMap
import com.vitorpamplona.quartz.utils.concurrent.ConcurrentMap
/**
* Counter-key grammar for the resource-usage ledger. Keys are flat strings so
@@ -489,8 +489,8 @@ object UsageKeys {
private const val UP = "up"
private const val DOWN = "down"
private val VERB_UP_KEYS = ConcurrentHashMap<String, Array<String>>()
private val VERB_DOWN_KEYS = ConcurrentHashMap<String, Array<String>>()
private val VERB_UP_KEYS = ConcurrentMap<String, Array<String>>()
private val VERB_DOWN_KEYS = ConcurrentMap<String, Array<String>>()
/**
* `relay.purpose.home_feed.sent` / `.bytes` — REQ frames and REQ bytes by the
@@ -562,7 +562,7 @@ object UsageKeys {
private val PURPOSE_SUFFIXES = arrayOf("sent", "bytes", "down", "downn", "dupbytes")
private val PURPOSE_KEYS = ConcurrentHashMap<String, Array<String>>()
private val PURPOSE_KEYS = ConcurrentMap<String, Array<String>>()
/** The five `relay.purpose.<purpose>.*` keys, indexed by the `P_` constants above. */
private fun purposeKeys(purpose: String): Array<String> = PURPOSE_KEYS.getOrPut(purpose) { Array(PURPOSE_SUFFIXES.size) { "relay.purpose.$purpose.${PURPOSE_SUFFIXES[it]}" } }
@@ -20,7 +20,6 @@
*/
package com.vitorpamplona.amethyst.ui.actions.mediaServers
import android.widget.Toast
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
@@ -53,7 +52,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.font.FontWeight
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.Dp
@@ -83,6 +81,7 @@ import com.vitorpamplona.amethyst.commons.resources.blossom_import_sources_secti
import com.vitorpamplona.amethyst.commons.resources.blossom_import_start_button
import com.vitorpamplona.amethyst.commons.resources.blossom_import_title
import com.vitorpamplona.amethyst.commons.resources.delete_media_server
import com.vitorpamplona.amethyst.commons.ui.components.rememberLongNotice
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
@@ -123,7 +122,7 @@ fun BlossomImportScreen(
// kind-10063 list arrives from a relay just after the screen opens (common on cold start).
val targetServers by accountViewModel.account.blossomServers.flow
.collectAsStateWithLifecycle()
val context = LocalContext.current
val longNotice = rememberLongNotice()
Scaffold(
topBar = {
@@ -254,9 +253,7 @@ fun BlossomImportScreen(
when (vm.importSelected()) {
is ImportStart.Started -> nav.popBack()
ImportStart.Busy ->
Toast
.makeText(context, blossomImportBusyStr, Toast.LENGTH_LONG)
.show()
longNotice.show(blossomImportBusyStr)
ImportStart.Empty -> {}
}
},
@@ -22,7 +22,6 @@ package com.vitorpamplona.amethyst.ui.call
import android.app.Activity
import android.media.projection.MediaProjectionManager
import androidx.activity.compose.BackHandler
import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.result.contract.ActivityResultContracts
import androidx.compose.foundation.background
@@ -71,6 +70,7 @@ import com.vitorpamplona.amethyst.commons.resources.call_incoming
import com.vitorpamplona.amethyst.commons.resources.call_incoming_video
import com.vitorpamplona.amethyst.commons.resources.call_incoming_voice
import com.vitorpamplona.amethyst.commons.resources.call_reject
import com.vitorpamplona.amethyst.commons.ui.components.PlatformBackHandler
import com.vitorpamplona.amethyst.commons.ui.stringRes
import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel
import com.vitorpamplona.amethyst.ui.call.session.CallSession
@@ -98,7 +98,7 @@ fun CallScreen(
val emptyStringFlow = remember { kotlinx.coroutines.flow.MutableStateFlow<String?>(null) }
val errorMessage by (callSession?.errorMessage ?: emptyStringFlow).collectAsState()
BackHandler(enabled = callState !is CallState.Idle && callState !is CallState.Ended) {
PlatformBackHandler(enabled = callState !is CallState.Idle && callState !is CallState.Ended) {
scope.launch { callManager.hangup() }
}
@@ -22,7 +22,6 @@ package com.vitorpamplona.amethyst.ui.note.nip22Comments
import android.content.Intent
import android.net.Uri
import androidx.activity.compose.BackHandler
import androidx.compose.foundation.clickable
import androidx.compose.foundation.horizontalScroll
import androidx.compose.foundation.layout.Box
@@ -79,6 +78,7 @@ import com.vitorpamplona.amethyst.commons.resources.post_anonymously
import com.vitorpamplona.amethyst.commons.resources.what_s_on_your_mind
import com.vitorpamplona.amethyst.commons.resources.zapraiser
import com.vitorpamplona.amethyst.commons.service.upload.ui.StrippingFailureDialog
import com.vitorpamplona.amethyst.commons.ui.components.PlatformBackHandler
import com.vitorpamplona.amethyst.commons.ui.insets.imePaddingSafe
import com.vitorpamplona.amethyst.commons.ui.navigation.topbars.PostingTopBar
import com.vitorpamplona.amethyst.commons.ui.note.BaseUserPicture
@@ -227,7 +227,7 @@ fun GenericCommentPostScreen(
StrippingFailureDialog(postViewModel.strippingFailureConfirmation)
BackHandler {
PlatformBackHandler {
accountViewModel.launchSigner {
postViewModel.sendDraftSync()
onUiThread { postViewModel.cancel() }
@@ -21,7 +21,6 @@
package com.vitorpamplona.amethyst.ui.screen.loggedIn
import android.content.res.Configuration
import androidx.activity.compose.BackHandler
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxHeight
@@ -48,6 +47,7 @@ import androidx.compose.ui.platform.LocalConfiguration
import androidx.navigation.NavDestination.Companion.hasRoute
import androidx.navigation.compose.currentBackStackEntryAsState
import com.vitorpamplona.amethyst.commons.model.navigation.Route
import com.vitorpamplona.amethyst.commons.ui.components.PlatformBackHandler
import com.vitorpamplona.amethyst.commons.ui.layouts.LocalScreenLayout
import com.vitorpamplona.amethyst.commons.ui.layouts.NavigationStyle
import com.vitorpamplona.amethyst.commons.ui.theme.DividerThickness
@@ -182,7 +182,7 @@ private fun ModalDrawerShell(
gesturesEnabled = drawerGesturesEnabled,
drawerContent = {
DrawerContent(nav, openSheet, accountViewModel)
BackHandler(enabled = nav.drawerState.isOpen, nav::closeDrawer)
PlatformBackHandler(enabled = nav.drawerState.isOpen, nav::closeDrawer)
},
content = {
if (showRail) {
@@ -20,7 +20,6 @@
*/
package com.vitorpamplona.amethyst.ui.screen.loggedIn.badges.award
import androidx.activity.compose.BackHandler
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
@@ -56,6 +55,7 @@ import com.vitorpamplona.amethyst.commons.resources.award_badge_remove_recipient
import com.vitorpamplona.amethyst.commons.resources.award_badge_search_label
import com.vitorpamplona.amethyst.commons.resources.award_badge_search_placeholder
import com.vitorpamplona.amethyst.commons.ui.components.Nip05OrPubkeyLine
import com.vitorpamplona.amethyst.commons.ui.components.PlatformBackHandler
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.SavingTopBar
@@ -93,7 +93,7 @@ fun AwardBadgeScreen(
onDispose { userSuggestions.reset() }
}
BackHandler {
PlatformBackHandler {
nav.popBack()
}
@@ -20,7 +20,6 @@
*/
package com.vitorpamplona.amethyst.ui.screen.loggedIn.bookmarkgroups.old
import android.widget.Toast
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
@@ -44,7 +43,6 @@ import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.lifecycle.viewmodel.compose.viewModel
import com.vitorpamplona.amethyst.commons.icons.symbols.Icon
@@ -58,6 +56,7 @@ import com.vitorpamplona.amethyst.commons.resources.old_bookmarks_title
import com.vitorpamplona.amethyst.commons.resources.private_bookmarks
import com.vitorpamplona.amethyst.commons.resources.public_bookmarks
import com.vitorpamplona.amethyst.commons.ui.components.DeletedItemsBanner
import com.vitorpamplona.amethyst.commons.ui.components.rememberShortNotice
import com.vitorpamplona.amethyst.commons.ui.layouts.DisappearingScaffold
import com.vitorpamplona.amethyst.commons.ui.loadStringRes
import com.vitorpamplona.amethyst.commons.ui.navigation.bottombars.FabBottomBarPadded
@@ -114,7 +113,7 @@ private fun RenderOldBookmarkScreen(
) {
val pagerState = rememberPagerState { 2 }
val coroutineScope = rememberCoroutineScope()
val context = LocalContext.current
val shortNotice = rememberShortNotice()
val cache = accountViewModel.account.cache
val deletedEventIds = remember(bookmarkState) { mutableSetOf<String>() }
@@ -177,12 +176,7 @@ private fun RenderOldBookmarkScreen(
accountViewModel.launchSigner {
accountViewModel.account.migrateOldBookmarksToNew()
coroutineScope.launch {
Toast
.makeText(
context,
loadStringRes(Res.string.migrate_bookmarks_success),
Toast.LENGTH_SHORT,
).show()
shortNotice.show(loadStringRes(Res.string.migrate_bookmarks_success))
}
}
},
@@ -24,7 +24,6 @@ import android.Manifest
import android.content.pm.PackageManager
import android.os.Build
import android.widget.Toast
import androidx.activity.compose.BackHandler
import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.result.contract.ActivityResultContracts
import androidx.annotation.RequiresApi
@@ -74,6 +73,7 @@ import com.vitorpamplona.amethyst.commons.model.navigation.Route
import com.vitorpamplona.amethyst.commons.model.navigation.favoriteIds
import com.vitorpamplona.amethyst.commons.resources.Res
import com.vitorpamplona.amethyst.commons.resources.browser_unsupported
import com.vitorpamplona.amethyst.commons.ui.components.PlatformBackHandler
import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav
import com.vitorpamplona.amethyst.commons.ui.platform.AppBottomBar
import com.vitorpamplona.amethyst.commons.ui.stringRes
@@ -299,9 +299,9 @@ private fun EmbeddedWebAppTab(
}
val isFullscreen by controller.isFullscreen
BackHandler(enabled = canGoBack && !isFullscreen) { controller.back() }
PlatformBackHandler(enabled = canGoBack && !isFullscreen) { controller.back() }
// A fullscreen video inside the tab: back leaves fullscreen first, as in Chrome.
BackHandler(enabled = isFullscreen) { controller.exitFullscreen() }
PlatformBackHandler(enabled = isFullscreen) { controller.exitFullscreen() }
EmbeddedPageUi(controller, chrome.ui, showPageInfo, onPageInfoDismiss = { showPageInfo = false })
@@ -20,7 +20,6 @@
*/
package com.vitorpamplona.amethyst.ui.screen.loggedIn.buzz
import androidx.activity.compose.BackHandler
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
@@ -63,6 +62,7 @@ import com.vitorpamplona.amethyst.commons.resources.buzz_canvas_empty
import com.vitorpamplona.amethyst.commons.resources.buzz_canvas_save
import com.vitorpamplona.amethyst.commons.resources.buzz_canvas_title
import com.vitorpamplona.amethyst.commons.resources.cancel
import com.vitorpamplona.amethyst.commons.ui.components.PlatformBackHandler
import com.vitorpamplona.amethyst.commons.ui.components.TranslatableRichTextViewer
import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav
import com.vitorpamplona.amethyst.commons.ui.navigation.topbars.TopBarExtensibleWithBackButton
@@ -222,7 +222,7 @@ private fun CanvasEditor(
val scope = rememberCoroutineScope()
// Back cancels the edit and returns to the rendered canvas rather than leaving the screen.
BackHandler(enabled = !saving, onBack = onClose)
PlatformBackHandler(enabled = !saving, onBack = onClose)
Scaffold(
topBar = {
@@ -20,7 +20,7 @@
*/
package com.vitorpamplona.amethyst.ui.screen.loggedIn.buzz
import com.vitorpamplona.amethyst.Amethyst
import com.vitorpamplona.amethyst.commons.model.cache.LocalCache
import com.vitorpamplona.amethyst.service.resourceusage.UsageKeys
import com.vitorpamplona.quartz.nip01Core.relay.client.INostrClient
@@ -45,9 +45,6 @@ import com.vitorpamplona.quartz.nip01Core.relay.client.INostrClient
* subscription — so `relay.trigger.buzz` undercounts joins, not re-challenges.
*/
internal fun reconnectPoolAfterJoin(client: INostrClient) {
// Guarded like every other ledger write that reaches the application singleton
// (MediaPlayTimeTracker, the workers): a diagnostics counter must never break a
// user-visible join, and `Amethyst.instance` is lateinit.
runCatching { Amethyst.instance.resourceUsage.add(UsageKeys.relayTrigger(UsageKeys.TRIGGER_BUZZ), 1) }
LocalCache.appHost.usageCounter.add(UsageKeys.relayTrigger(UsageKeys.TRIGGER_BUZZ), 1)
client.reconnect(onlyIfChanged = false, ignoreRetryDelays = true)
}
@@ -21,7 +21,6 @@
package com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.privateDM.send
import android.net.Uri
import androidx.activity.compose.BackHandler
import androidx.compose.foundation.horizontalScroll
import androidx.compose.foundation.layout.Arrangement.Absolute.spacedBy
import androidx.compose.foundation.layout.Box
@@ -88,6 +87,7 @@ import com.vitorpamplona.amethyst.commons.richtext.EncryptedMediaUrlVideo
import com.vitorpamplona.amethyst.commons.richtext.MediaUrlImage
import com.vitorpamplona.amethyst.commons.richtext.MediaUrlVideo
import com.vitorpamplona.amethyst.commons.richtext.RichTextParser
import com.vitorpamplona.amethyst.commons.ui.components.PlatformBackHandler
import com.vitorpamplona.amethyst.commons.ui.insets.imePaddingSafe
import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav
import com.vitorpamplona.amethyst.commons.ui.navigation.routes.routeToMessage
@@ -181,7 +181,7 @@ fun NewGroupDMScreen(
WatchAndLoadMyEmojiList(accountViewModel)
BackHandler {
PlatformBackHandler {
accountViewModel.launchSigner {
postViewModel.sendDraftSync()
onUiThread { postViewModel.cancel() }
@@ -20,7 +20,6 @@
*/
package com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.privateDM.send
import androidx.activity.compose.BackHandler
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
@@ -63,6 +62,7 @@ import com.vitorpamplona.amethyst.commons.resources.retry_without_encryption
import com.vitorpamplona.amethyst.commons.resources.this_message_will_disappear_in
import com.vitorpamplona.amethyst.commons.resources.upload_without_encryption_warning
import com.vitorpamplona.amethyst.commons.service.upload.ui.StrippingFailureDialog
import com.vitorpamplona.amethyst.commons.ui.components.PlatformBackHandler
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.routes.routeFor
@@ -118,7 +118,7 @@ fun PrivateMessageEditFieldRow(
onSendNewMessage: () -> Unit,
nav: INav,
) {
BackHandler {
PlatformBackHandler {
if (channelScreenModel.message.text.isNotBlank()) {
accountViewModel.launchSigner {
channelScreenModel.sendDraftSync()
@@ -20,7 +20,6 @@
*/
package com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.send
import androidx.activity.compose.BackHandler
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth
@@ -54,6 +53,7 @@ import com.vitorpamplona.amethyst.commons.resources.buzz_editing_banner
import com.vitorpamplona.amethyst.commons.resources.cancel
import com.vitorpamplona.amethyst.commons.resources.reply_here
import com.vitorpamplona.amethyst.commons.service.upload.ui.StrippingFailureDialog
import com.vitorpamplona.amethyst.commons.ui.components.PlatformBackHandler
import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav
import com.vitorpamplona.amethyst.commons.ui.note.creators.userSuggestions.ShowUserSuggestionList
import com.vitorpamplona.amethyst.commons.ui.stringRes
@@ -82,7 +82,7 @@ fun EditFieldRow(
onSendNewMessage: suspend () -> Unit,
nav: INav,
) {
BackHandler {
PlatformBackHandler {
accountViewModel.launchSigner {
channelScreenModel.sendDraftSync()
onUiThread { channelScreenModel.cancel() }
@@ -20,7 +20,6 @@
*/
package com.vitorpamplona.amethyst.ui.screen.loggedIn.discover.nip23LongForm
import androidx.activity.compose.BackHandler
import androidx.compose.foundation.BorderStroke
import androidx.compose.foundation.border
import androidx.compose.foundation.clickable
@@ -103,6 +102,7 @@ import com.vitorpamplona.amethyst.commons.resources.your_article_title
import com.vitorpamplona.amethyst.commons.resources.zapraiser
import com.vitorpamplona.amethyst.commons.service.upload.ui.StrippingFailureDialog
import com.vitorpamplona.amethyst.commons.ui.components.MyAsyncImage
import com.vitorpamplona.amethyst.commons.ui.components.PlatformBackHandler
import com.vitorpamplona.amethyst.commons.ui.insets.imePaddingSafe
import com.vitorpamplona.amethyst.commons.ui.navigation.topbars.PostingTopBar
import com.vitorpamplona.amethyst.commons.ui.note.creators.contentWarning.ContentSensitivityExplainer
@@ -169,7 +169,7 @@ fun LongFormPostScreen(
StrippingFailureDialog(postViewModel.strippingFailureConfirmation)
BackHandler {
PlatformBackHandler {
accountViewModel.launchSigner {
postViewModel.sendDraftSync()
onUiThread { postViewModel.cancel() }
@@ -20,7 +20,6 @@
*/
package com.vitorpamplona.amethyst.ui.screen.loggedIn.discover.nip99Classifieds
import androidx.activity.compose.BackHandler
import androidx.compose.foundation.horizontalScroll
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
@@ -53,6 +52,7 @@ import com.vitorpamplona.amethyst.commons.resources.lightning_invoice
import com.vitorpamplona.amethyst.commons.resources.new_product
import com.vitorpamplona.amethyst.commons.resources.zapraiser
import com.vitorpamplona.amethyst.commons.service.upload.ui.StrippingFailureDialog
import com.vitorpamplona.amethyst.commons.ui.components.PlatformBackHandler
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.PostingTopBar
@@ -148,7 +148,7 @@ fun NewProductScreen(
StrippingFailureDialog(postViewModel.strippingFailureConfirmation)
BackHandler {
PlatformBackHandler {
accountViewModel.launchSigner {
postViewModel.sendDraftSync()
onUiThread { postViewModel.cancel() }
@@ -29,7 +29,6 @@ import android.graphics.BitmapFactory
import android.os.Build
import android.os.SystemClock
import android.view.ViewGroup
import androidx.activity.compose.BackHandler
import androidx.annotation.RequiresApi
import androidx.compose.foundation.BorderStroke
import androidx.compose.foundation.Canvas
@@ -90,6 +89,7 @@ import com.vitorpamplona.amethyst.commons.browser.ui.pill.BrowserPillEvent
import com.vitorpamplona.amethyst.commons.browser.ui.pill.ConsoleLine
import com.vitorpamplona.amethyst.commons.browser.ui.pill.ConsoleSheet
import com.vitorpamplona.amethyst.commons.browser.ui.pill.FindInPagePill
import com.vitorpamplona.amethyst.commons.ui.components.PlatformBackHandler
import com.vitorpamplona.amethyst.napplethost.BrowserWebTools
import kotlinx.coroutines.delay
import kotlinx.serialization.json.buildJsonObject
@@ -306,7 +306,7 @@ fun EmbeddedTabLayer(barFavoriteIds: List<String>) {
}
if (pillExpanded) {
BackHandler { pillExpanded = false }
PlatformBackHandler { pillExpanded = false }
Box(
Modifier
.fillMaxSize()
@@ -361,7 +361,7 @@ fun EmbeddedTabLayer(barFavoriteIds: List<String>) {
// Find in page: opened from the pill's Find tile.
if (findShowing && findBridge != null) {
BackHandler { closeFind() }
PlatformBackHandler { closeFind() }
val result by findBridge.findResult
FindInPagePill(
query = findQuery,
@@ -22,7 +22,6 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn.favorites
import android.os.Build
import android.widget.Toast
import androidx.activity.compose.BackHandler
import androidx.annotation.RequiresApi
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxSize
@@ -73,6 +72,7 @@ import com.vitorpamplona.amethyst.commons.resources.favorite_apps
import com.vitorpamplona.amethyst.commons.resources.favorite_notice_paid
import com.vitorpamplona.amethyst.commons.resources.favorite_notice_published
import com.vitorpamplona.amethyst.commons.resources.favorite_notice_uploaded
import com.vitorpamplona.amethyst.commons.ui.components.PlatformBackHandler
import com.vitorpamplona.amethyst.commons.ui.loadStringRes
import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav
import com.vitorpamplona.amethyst.commons.ui.platform.AppBottomBar
@@ -262,7 +262,7 @@ private fun EmbeddedNostrAppTab(
onDispose { lifecycleOwner.lifecycle.removeObserver(observer) }
}
BackHandler(enabled = canGoBack) { controller.back() }
PlatformBackHandler(enabled = canGoBack) { controller.back() }
if (showAccess) {
Dialog(onDismissRequest = { showAccess = false }, properties = DialogProperties(usePlatformDefaultWidth = false)) {
@@ -20,7 +20,6 @@
*/
package com.vitorpamplona.amethyst.ui.screen.loggedIn.gitRepo.code
import androidx.activity.compose.BackHandler
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.horizontalScroll
@@ -73,6 +72,7 @@ import com.vitorpamplona.amethyst.commons.resources.git_repo_no_clone_url
import com.vitorpamplona.amethyst.commons.resources.git_repo_no_search_results
import com.vitorpamplona.amethyst.commons.resources.git_repo_root
import com.vitorpamplona.amethyst.commons.resources.git_repo_search_files
import com.vitorpamplona.amethyst.commons.ui.components.PlatformBackHandler
import com.vitorpamplona.amethyst.commons.ui.layouts.LocalDisappearingBarState
import com.vitorpamplona.amethyst.commons.ui.layouts.LocalDisappearingScaffoldPadding
import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav
@@ -141,7 +141,7 @@ private fun CodeBrowser(
if (openPath != null) {
val entry = remember(openFilePath) { snapshot.entryAt(openPath) }
BackHandler { openFilePath = null }
PlatformBackHandler { openFilePath = null }
Column(Modifier.fillMaxSize().padding(scaffoldPaddingTop)) {
FileHeader(name = openPath.lastOrNull() ?: "", onBack = { openFilePath = null })
HorizontalDivider(thickness = 0.5.dp)
@@ -164,7 +164,7 @@ private fun CodeBrowser(
var query by rememberSaveable(snapshot.headCommit) { mutableStateOf("") }
val searching = query.isNotBlank()
BackHandler(enabled = searching || path.isNotEmpty()) {
PlatformBackHandler(enabled = searching || path.isNotEmpty()) {
if (searching) query = "" else pathString = path.dropLast(1).joinToString("/")
}
@@ -21,7 +21,6 @@
package com.vitorpamplona.amethyst.ui.screen.loggedIn.gitRepo.code
import android.text.format.DateUtils
import androidx.activity.compose.BackHandler
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
@@ -66,6 +65,7 @@ import com.vitorpamplona.amethyst.commons.resources.git_repo_code_loading
import com.vitorpamplona.amethyst.commons.resources.git_repo_commits
import com.vitorpamplona.amethyst.commons.resources.git_repo_file_load_error
import com.vitorpamplona.amethyst.commons.resources.git_repo_no_commits
import com.vitorpamplona.amethyst.commons.ui.components.PlatformBackHandler
import com.vitorpamplona.amethyst.commons.ui.note.ArrowBackIcon
import com.vitorpamplona.amethyst.commons.ui.note.GitDiffView
import com.vitorpamplona.amethyst.commons.ui.stringRes
@@ -103,7 +103,7 @@ fun GitCommitLog(
if (openCommit != null) {
val commit = remember(openCommit, commits) { commits?.firstOrNull { it.oid == openCommit } }
BackHandler { openCommit = null }
PlatformBackHandler { openCommit = null }
Column(Modifier.fillMaxSize()) {
LogHeader(title = commit?.shortOid ?: "", onBack = { openCommit = null })
HorizontalDivider(thickness = 0.5.dp)
@@ -116,7 +116,7 @@ fun GitCommitLog(
return
}
BackHandler { onBack() }
PlatformBackHandler { onBack() }
Column(Modifier.fillMaxSize()) {
LogHeader(title = stringRes(Res.string.git_repo_commits), onBack = onBack)
HorizontalDivider(thickness = 0.5.dp)
@@ -22,7 +22,6 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn.home
import android.content.Intent
import android.net.Uri
import androidx.activity.compose.BackHandler
import androidx.compose.animation.animateColorAsState
import androidx.compose.animation.core.tween
import androidx.compose.foundation.clickable
@@ -110,6 +109,7 @@ import com.vitorpamplona.amethyst.commons.resources.schedule_post_always_on_prom
import com.vitorpamplona.amethyst.commons.resources.what_s_on_your_mind
import com.vitorpamplona.amethyst.commons.resources.zapraiser
import com.vitorpamplona.amethyst.commons.service.upload.ui.StrippingFailureDialog
import com.vitorpamplona.amethyst.commons.ui.components.PlatformBackHandler
import com.vitorpamplona.amethyst.commons.ui.insets.imePaddingSafe
import com.vitorpamplona.amethyst.commons.ui.navigation.topbars.PostingTopBar
import com.vitorpamplona.amethyst.commons.ui.note.BaseUserPicture
@@ -275,7 +275,7 @@ internal fun NewPostScreenInner(
StrippingFailureDialog(postViewModel.strippingFailureConfirmation)
BackHandler {
PlatformBackHandler {
accountViewModel.launchSigner {
postViewModel.sendDraftSync()
onUiThread { postViewModel.cancel() }
@@ -20,7 +20,6 @@
*/
package com.vitorpamplona.amethyst.ui.screen.loggedIn.home
import androidx.activity.compose.BackHandler
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.consumeWindowInsets
@@ -40,6 +39,7 @@ import androidx.compose.runtime.getValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import androidx.lifecycle.viewmodel.compose.viewModel
import com.vitorpamplona.amethyst.commons.ui.components.PlatformBackHandler
import com.vitorpamplona.amethyst.commons.ui.navigation.topbars.PostingTopBar
import com.vitorpamplona.amethyst.commons.ui.note.NoteCompose
import com.vitorpamplona.amethyst.commons.ui.note.types.ReplyRenderType
@@ -71,7 +71,7 @@ fun VoiceReplyScreen(
viewModel.load(replyToNoteId, recordingFilePath, mimeType, duration, amplitudesJson)
}
BackHandler {
PlatformBackHandler {
viewModel.cancel()
nav.popBack()
}
@@ -20,7 +20,6 @@
*/
package com.vitorpamplona.amethyst.ui.screen.loggedIn.home.nip75Goals
import androidx.activity.compose.BackHandler
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
@@ -59,6 +58,7 @@ import com.vitorpamplona.amethyst.commons.resources.goal_summary_placeholder
import com.vitorpamplona.amethyst.commons.resources.goal_website_label
import com.vitorpamplona.amethyst.commons.resources.goal_website_placeholder
import com.vitorpamplona.amethyst.commons.resources.new_goal
import com.vitorpamplona.amethyst.commons.ui.components.PlatformBackHandler
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.PostingTopBar
@@ -93,7 +93,7 @@ fun NewGoalScreen(
accountViewModel: AccountViewModel,
nav: INav,
) {
BackHandler {
PlatformBackHandler {
goalViewModel.cancel()
nav.popBack()
}
@@ -20,7 +20,6 @@
*/
package com.vitorpamplona.amethyst.ui.screen.loggedIn.nests.room.activity
import androidx.activity.compose.BackHandler
import androidx.activity.compose.LocalActivity
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
@@ -52,6 +51,7 @@ import com.vitorpamplona.amethyst.commons.resources.nest_loading_room
import com.vitorpamplona.amethyst.commons.resources.nest_unjoinable_back
import com.vitorpamplona.amethyst.commons.resources.nest_unjoinable_body
import com.vitorpamplona.amethyst.commons.resources.nest_unjoinable_title
import com.vitorpamplona.amethyst.commons.ui.components.PlatformBackHandler
import com.vitorpamplona.amethyst.commons.ui.note.LoadAddressableNote
import com.vitorpamplona.amethyst.commons.ui.stringRes
import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel
@@ -362,7 +362,7 @@ private fun NestActivityBody(
// Failed) or PIP isn't supported, we let back fall through to the
// default Activity.finish() so the user can cancel out cleanly.
val canMinimize = isPipSupported && !isInPipMode && ui.connection is ConnectionUiState.Connected
BackHandler(enabled = canMinimize) { onMinimize() }
PlatformBackHandler(enabled = canMinimize) { onMinimize() }
if (isInPipMode) {
NestPipScreen(
@@ -20,7 +20,6 @@
*/
package com.vitorpamplona.amethyst.ui.screen.loggedIn.notifications.publicMessages
import androidx.activity.compose.BackHandler
import androidx.compose.foundation.horizontalScroll
import androidx.compose.foundation.layout.Arrangement.Absolute.spacedBy
import androidx.compose.foundation.layout.Column
@@ -61,6 +60,7 @@ import com.vitorpamplona.amethyst.commons.resources.messages_new_message_to_capt
import com.vitorpamplona.amethyst.commons.resources.public_message
import com.vitorpamplona.amethyst.commons.resources.zapraiser
import com.vitorpamplona.amethyst.commons.service.upload.ui.StrippingFailureDialog
import com.vitorpamplona.amethyst.commons.ui.components.PlatformBackHandler
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.PostingTopBar
@@ -142,7 +142,7 @@ fun NewPublicMessageScreen(
StrippingFailureDialog(postViewModel.strippingFailureConfirmation)
BackHandler {
PlatformBackHandler {
accountViewModel.launchSigner {
postViewModel.sendDraftSync()
onUiThread { postViewModel.cancel() }
@@ -20,7 +20,6 @@
*/
package com.vitorpamplona.amethyst.ui.screen.loggedIn.settings
import android.widget.Toast
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
@@ -46,7 +45,6 @@ import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalUriHandler
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.tooling.preview.Preview
@@ -62,6 +60,8 @@ import com.vitorpamplona.amethyst.commons.resources.reset_marmot_confirm_title
import com.vitorpamplona.amethyst.commons.resources.reset_marmot_failure
import com.vitorpamplona.amethyst.commons.resources.reset_marmot_success
import com.vitorpamplona.amethyst.commons.resources.settings_search_no_results
import com.vitorpamplona.amethyst.commons.ui.components.rememberLongNotice
import com.vitorpamplona.amethyst.commons.ui.components.rememberShortNotice
import com.vitorpamplona.amethyst.commons.ui.loadStringRes
import com.vitorpamplona.amethyst.commons.ui.navigation.navs.EmptyNav
import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav
@@ -98,7 +98,8 @@ fun AllSettingsScreen(
accountViewModel: AccountViewModel,
nav: INav,
) {
val context = LocalContext.current
val shortNotice = rememberShortNotice()
val longNotice = rememberLongNotice()
val uriHandler = LocalUriHandler.current
val scope = rememberCoroutineScope()
var showResetMarmotDialog by remember { mutableStateOf(false) }
@@ -186,13 +187,13 @@ fun AllSettingsScreen(
try {
accountViewModel.resetMarmotState()
launch(Dispatchers.Main) {
Toast.makeText(context, successMessage, Toast.LENGTH_SHORT).show()
shortNotice.show(successMessage)
}
} catch (e: Exception) {
val failureMessage =
loadStringRes(Res.string.reset_marmot_failure, e.message ?: "")
launch(Dispatchers.Main) {
Toast.makeText(context, failureMessage, Toast.LENGTH_LONG).show()
longNotice.show(failureMessage)
}
} finally {
isResettingMarmot = false
@@ -80,9 +80,9 @@ import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav
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.viewmodels.AccountViewModel
import kotlinx.coroutines.launch
import java.text.NumberFormat
/**
* "Bitcoin" card on the wallet screen, shown above the lightning NWC wallet
@@ -277,7 +277,7 @@ private fun BalanceBlock(
OnchainBalanceStatus.READY -> {
val formatted =
remember(sats) {
NumberFormat.getIntegerInstance().format(sats ?: 0L)
formatGrouped(sats ?: 0L)
}
Text(
text = formatted,
@@ -51,7 +51,6 @@ import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalUriHandler
import androidx.compose.ui.platform.UriHandler
import androidx.compose.ui.text.font.FontFamily
@@ -81,13 +80,14 @@ import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav
import com.vitorpamplona.amethyst.commons.ui.note.LoadUser
import com.vitorpamplona.amethyst.commons.ui.note.UserPicture
import com.vitorpamplona.amethyst.commons.ui.note.UsernameDisplay
import com.vitorpamplona.amethyst.commons.ui.note.formatMonthDayTime
import com.vitorpamplona.amethyst.commons.ui.note.rememberTimeOfDayFormatter
import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.wallet.TransactionFilter
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.viewmodels.AccountViewModel
import com.vitorpamplona.amethyst.ui.note.formatMonthDayTime
import com.vitorpamplona.amethyst.ui.screen.loggedIn.wallet.datasource.OnchainZapsFilterAssemblerSubscription
import java.text.NumberFormat
import kotlin.math.absoluteValue
@OptIn(ExperimentalMaterial3Api::class)
@@ -309,15 +309,14 @@ private fun OnchainTransactionItem(
val amountSats = view.tx.netValueSats.absoluteValue
val formattedAmount =
remember(view.tx.netValueSats) {
val fmt = NumberFormat.getIntegerInstance()
(if (isIncoming) "+" else "-") + fmt.format(amountSats)
(if (isIncoming) "+" else "-") + formatGrouped(amountSats)
}
val context = LocalContext.current
val timeOfDay = rememberTimeOfDayFormatter()
val dateText =
remember(view.tx.blockTime, view.tx.confirmations, context) {
remember(view.tx.blockTime, view.tx.confirmations, timeOfDay) {
val ts = view.tx.blockTime
if (ts != null) formatMonthDayTime(ts, context) else ""
if (ts != null) formatMonthDayTime(ts, timeOfDay) else ""
}
val counterpartyPubkeyHex = view.counterpartyPubkeyHex()
@@ -65,8 +65,8 @@ import com.vitorpamplona.amethyst.commons.resources.wallet_transactions
import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav
import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.wallet.WalletViewModel
import com.vitorpamplona.amethyst.commons.ui.stringRes
import com.vitorpamplona.amethyst.commons.util.formatGrouped
import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel
import java.text.NumberFormat
@OptIn(ExperimentalMaterial3Api::class)
@Composable
@@ -122,8 +122,7 @@ fun WalletDetailScreen(
} else {
val formattedBalance =
remember(balance) {
val fmt = NumberFormat.getIntegerInstance()
fmt.format(balance ?: 0L)
formatGrouped(balance ?: 0L)
}
Text(
text = formattedBalance,
@@ -20,9 +20,6 @@
*/
package com.vitorpamplona.amethyst.ui.screen.loggedIn.wallet
import android.content.ClipData
import android.content.ClipboardManager
import android.content.Context
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
@@ -52,10 +49,11 @@ import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalClipboard
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.input.KeyboardType
import androidx.compose.ui.text.style.TextAlign
@@ -73,13 +71,15 @@ import com.vitorpamplona.amethyst.commons.resources.wallet_creating_invoice
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.components.util.setText
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.screen.loggedIn.wallet.ReceiveState
import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.wallet.WalletViewModel
import com.vitorpamplona.amethyst.commons.ui.stringRes
import com.vitorpamplona.amethyst.commons.util.formatGrouped
import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel
import java.text.NumberFormat
import kotlinx.coroutines.launch
@OptIn(ExperimentalMaterial3Api::class)
@Composable
@@ -102,7 +102,8 @@ fun WalletReceiveScreen(
val receiveState by walletViewModel.receiveState.collectAsState()
var amountText by remember { mutableStateOf("") }
var descriptionText by remember { mutableStateOf("") }
val context = LocalContext.current
val clipboard = LocalClipboard.current
val clipboardScope = rememberCoroutineScope()
Scaffold(
topBar = {
@@ -194,8 +195,7 @@ fun WalletReceiveScreen(
val formattedAmount =
remember(state.amount) {
val fmt = NumberFormat.getIntegerInstance()
fmt.format(state.amount)
formatGrouped(state.amount)
}
Text(
@@ -233,8 +233,7 @@ fun WalletReceiveScreen(
) {
OutlinedButton(
onClick = {
val clipboard = context.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager
clipboard.setPrimaryClip(ClipData.newPlainText("invoice", state.invoice))
clipboardScope.launch { clipboard.setText(state.invoice) }
},
modifier =
Modifier
@@ -20,7 +20,6 @@
*/
package com.vitorpamplona.amethyst.ui.screen.loggedIn.wallet
import android.widget.Toast
import androidx.compose.foundation.BorderStroke
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
@@ -67,7 +66,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.font.FontWeight
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
@@ -103,6 +101,7 @@ import com.vitorpamplona.amethyst.commons.resources.wallet_rename_title
import com.vitorpamplona.amethyst.commons.resources.wallet_sats
import com.vitorpamplona.amethyst.commons.resources.wallet_save
import com.vitorpamplona.amethyst.commons.resources.wallet_set_default
import com.vitorpamplona.amethyst.commons.ui.components.rememberLongNotice
import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav
import com.vitorpamplona.amethyst.commons.ui.platform.AppBottomBar
import com.vitorpamplona.amethyst.commons.ui.pluralStringRes
@@ -110,10 +109,10 @@ import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.wallet.CashuWalletV
import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.wallet.WalletInfo
import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.wallet.WalletViewModel
import com.vitorpamplona.amethyst.commons.ui.stringRes
import com.vitorpamplona.amethyst.commons.util.formatGrouped
import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel
import com.vitorpamplona.quartz.experimental.clink.debits.DebitFrequency
import kotlinx.coroutines.launch
import java.text.NumberFormat
import androidx.compose.material3.Icon as Material3Icon
@OptIn(ExperimentalMaterial3Api::class)
@@ -248,7 +247,7 @@ private fun MultiWalletHomeContent(
cashuMintCount: Int,
) {
val walletInfoList by walletViewModel.walletInfoList.collectAsState()
val context = LocalContext.current
val longNotice = rememberLongNotice()
val budgetApprovedMsg = stringRes(Res.string.clink_budget_approved)
val debitNoResponseMsg = stringRes(Res.string.clink_debit_no_response)
@@ -298,7 +297,7 @@ private fun MultiWalletHomeContent(
!error.isNullOrBlank() -> error
else -> debitNoResponseMsg
}
Toast.makeText(context, msg, Toast.LENGTH_LONG).show()
longNotice.show(msg)
}
}
} else {
@@ -460,8 +459,7 @@ private fun WalletCard(
Column(horizontalAlignment = Alignment.End) {
val formattedBalance =
remember(walletInfo.balanceSats) {
val fmt = NumberFormat.getIntegerInstance()
fmt.format(walletInfo.balanceSats ?: 0L)
formatGrouped(walletInfo.balanceSats ?: 0L)
}
Text(
text = formattedBalance,
@@ -574,7 +572,7 @@ private fun CashuWalletRow(
onClick: () -> Unit,
) {
val formattedBalance =
remember(balanceSats) { NumberFormat.getIntegerInstance().format(balanceSats) }
remember(balanceSats) { formatGrouped(balanceSats) }
Card(
modifier =
Modifier
@@ -20,8 +20,6 @@
*/
package com.vitorpamplona.amethyst.ui.screen.loggedIn.wallet
import android.content.ClipboardManager
import android.content.Context
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer
@@ -47,10 +45,11 @@ import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalClipboard
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import androidx.lifecycle.viewmodel.compose.viewModel
@@ -63,11 +62,13 @@ import com.vitorpamplona.amethyst.commons.resources.wallet_pay
import com.vitorpamplona.amethyst.commons.resources.wallet_payment_sending
import com.vitorpamplona.amethyst.commons.resources.wallet_payment_success
import com.vitorpamplona.amethyst.commons.resources.wallet_send
import com.vitorpamplona.amethyst.commons.ui.components.util.getText
import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav
import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.wallet.SendState
import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.wallet.WalletViewModel
import com.vitorpamplona.amethyst.commons.ui.stringRes
import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel
import kotlinx.coroutines.launch
@OptIn(ExperimentalMaterial3Api::class)
@Composable
@@ -89,7 +90,8 @@ fun WalletSendScreen(
val sendState by walletViewModel.sendState.collectAsState()
var invoiceText by remember { mutableStateOf("") }
val context = LocalContext.current
val clipboard = LocalClipboard.current
val clipboardScope = rememberCoroutineScope()
Scaffold(
topBar = {
@@ -128,11 +130,7 @@ fun WalletSendScreen(
maxLines = 5,
trailingIcon = {
IconButton(onClick = {
val clipboard = context.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager
val clip = clipboard.primaryClip
if (clip != null && clip.itemCount > 0) {
invoiceText = clip.getItemAt(0).text?.toString() ?: ""
}
clipboardScope.launch { invoiceText = clipboard.getText() ?: "" }
}) {
Icon(
symbol = MaterialSymbols.ContentPaste,
@@ -51,7 +51,6 @@ import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
@@ -78,14 +77,15 @@ import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav
import com.vitorpamplona.amethyst.commons.ui.note.LoadUser
import com.vitorpamplona.amethyst.commons.ui.note.UserPicture
import com.vitorpamplona.amethyst.commons.ui.note.UsernameDisplay
import com.vitorpamplona.amethyst.commons.ui.note.formatMonthDayTime
import com.vitorpamplona.amethyst.commons.ui.note.rememberTimeOfDayFormatter
import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.wallet.TransactionFilter
import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.wallet.WalletViewModel
import com.vitorpamplona.amethyst.commons.ui.stringRes
import com.vitorpamplona.amethyst.commons.util.formatGrouped
import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel
import com.vitorpamplona.amethyst.ui.note.formatMonthDayTime
import com.vitorpamplona.quartz.nip47WalletConnect.rpc.NwcTransaction
import com.vitorpamplona.quartz.nip47WalletConnect.rpc.NwcTransactionType
import java.text.NumberFormat
@OptIn(ExperimentalMaterial3Api::class)
@Composable
@@ -278,14 +278,13 @@ private fun TransactionItem(
val amountSats = (tx.amount ?: 0L) / 1000L
val formattedAmount =
remember(amountSats) {
val fmt = NumberFormat.getIntegerInstance()
(if (isIncoming) "+" else "-") + fmt.format(amountSats)
(if (isIncoming) "+" else "-") + formatGrouped(amountSats)
}
val context = LocalContext.current
val timeOfDay = rememberTimeOfDayFormatter()
val dateText =
remember(tx.created_at, context) {
tx.created_at?.let { formatMonthDayTime(it, context) } ?: ""
remember(tx.created_at, timeOfDay) {
tx.created_at?.let { formatMonthDayTime(it, timeOfDay) } ?: ""
}
val directionLabel =
@@ -101,8 +101,8 @@ import com.vitorpamplona.amethyst.commons.resources.cashu_wizard_title
import com.vitorpamplona.amethyst.commons.resources.cashu_wizard_use_wallet
import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav
import com.vitorpamplona.amethyst.commons.ui.stringRes
import com.vitorpamplona.amethyst.commons.util.formatGrouped
import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel
import java.text.NumberFormat
/**
* Find-or-create wizard for the Cashu wallet. Reached from the wallet screen's
@@ -227,7 +227,7 @@ fun CashuWalletWizardScreen(
}
}
private fun formatSats(sats: Long): String = NumberFormat.getInstance().format(sats)
private fun formatSats(sats: Long): String = formatGrouped(sats)
/**
* Animate a sats figure counting up from zero the first time it's shown, then
@@ -20,7 +20,6 @@
*/
package com.vitorpamplona.amethyst.ui.screen.loggedIn.workouts
import androidx.activity.compose.BackHandler
import androidx.compose.animation.Crossfade
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
@@ -70,6 +69,7 @@ import com.vitorpamplona.amethyst.commons.resources.workout_minutes
import com.vitorpamplona.amethyst.commons.resources.workout_notes
import com.vitorpamplona.amethyst.commons.resources.workout_seconds
import com.vitorpamplona.amethyst.commons.resources.workout_title
import com.vitorpamplona.amethyst.commons.ui.components.PlatformBackHandler
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.PostingTopBar
@@ -91,7 +91,7 @@ fun NewWorkoutScreen(
postViewModel.init(accountViewModel)
postViewModel.prefill(prefill)
BackHandler {
PlatformBackHandler {
postViewModel.cancel()
nav.popBack()
}
@@ -22,6 +22,7 @@ package com.vitorpamplona.amethyst.commons.model.cache
import com.vitorpamplona.amethyst.commons.relays.nip11RelayInfo.Nip11CachedRetriever
import com.vitorpamplona.amethyst.commons.relays.nip11RelayInfo.Nip11Fetcher
import com.vitorpamplona.amethyst.commons.service.UsageCounter
import com.vitorpamplona.quartz.nip01Core.core.HexKey
import com.vitorpamplona.quartz.nip01Core.relay.client.stats.RelayStats
import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl
@@ -81,6 +82,9 @@ interface LocalCacheHost {
*/
fun relaySelfPubKey(relay: NormalizedRelayUrl): HexKey? = null
/** The resource-usage ledger's counters; counts nothing by default. */
val usageCounter: UsageCounter get() = UsageCounter.None
/**
* The relay's NIP-11 document as far as the shell has it in memory. Never fetches: `null`
* (or a document with nothing in it) means "not loaded yet", which callers must treat as
@@ -0,0 +1,34 @@
/*
* Copyright (c) 2025 Vitor Pamplona
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
* the Software without restriction, including without limitation the rights to use,
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
* Software, and to permit persons to whom the Software is furnished to do so,
* subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
package com.vitorpamplona.amethyst.commons.service
/** Adds to a diagnostics counter in the app's resource-usage ledger. Never throws. */
fun interface UsageCounter {
fun add(
key: String,
amount: Long,
)
companion object {
/** Counts nothing: hosts without a ledger. */
val None = UsageCounter { _, _ -> }
}
}
+7
View File
@@ -212,6 +212,8 @@ kotlin {
implementation(libs.androidx.ui.tooling.preview)
// WindowInsetsControllerCompat, for DisappearingScaffold's immersive status bar.
implementation(libs.androidx.core.ktx)
// BackHandler, behind the shared PlatformBackHandler. Apache-2.0, already in the app.
implementation(libs.androidx.activity.compose)
}
}
@@ -229,6 +231,11 @@ kotlin {
val skikoMain =
create("skikoMain") {
dependsOn(commonMain.get())
dependencies {
// BackHandler for desktop (Esc) and iOS (swipe), behind PlatformBackHandler.
// JetBrains Compose, Apache-2.0; Android uses AndroidX's instead.
implementation(libs.jetbrains.compose.ui.backhandler)
}
}
getByName("jvmMain").dependsOn(skikoMain)
iosMain.dependsOn(skikoMain)
@@ -0,0 +1,30 @@
/*
* Copyright (c) 2025 Vitor Pamplona
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
* the Software without restriction, including without limitation the rights to use,
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
* Software, and to permit persons to whom the Software is furnished to do so,
* subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
package com.vitorpamplona.amethyst.commons.ui.components
import androidx.activity.compose.BackHandler
import androidx.compose.runtime.Composable
@Composable
actual fun PlatformBackHandler(
enabled: Boolean,
onBack: () -> Unit,
) = BackHandler(enabled, onBack)
@@ -30,3 +30,9 @@ actual fun rememberShortNotice(): ShortNotice {
val context = LocalContext.current
return remember(context) { ShortNotice { message -> Toast.makeText(context, message, Toast.LENGTH_SHORT).show() } }
}
@Composable
actual fun rememberLongNotice(): ShortNotice {
val context = LocalContext.current
return remember(context) { ShortNotice { message -> Toast.makeText(context, message, Toast.LENGTH_LONG).show() } }
}
@@ -0,0 +1,33 @@
/*
* Copyright (c) 2025 Vitor Pamplona
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
* the Software without restriction, including without limitation the rights to use,
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
* Software, and to permit persons to whom the Software is furnished to do so,
* subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
package com.vitorpamplona.amethyst.commons.ui.components
import androidx.compose.runtime.Composable
/**
* Intercepts the system back gesture/button while [enabled]: AndroidX's `BackHandler` on
* Android, Compose Multiplatform's on desktop (Esc) and iOS (swipe back).
*/
@Composable
expect fun PlatformBackHandler(
enabled: Boolean = true,
onBack: () -> Unit,
)
@@ -33,3 +33,7 @@ fun interface ShortNotice {
*/
@Composable
expect fun rememberShortNotice(): ShortNotice
/** Like [rememberShortNotice], shown longer: for errors the reader needs time to take in. */
@Composable
expect fun rememberLongNotice(): ShortNotice
@@ -0,0 +1,35 @@
/*
* Copyright (c) 2025 Vitor Pamplona
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
* the Software without restriction, including without limitation the rights to use,
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
* Software, and to permit persons to whom the Software is furnished to do so,
* subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
package com.vitorpamplona.amethyst.commons.ui.note
private val monthDay = DateSkeletonFormatter("MMMd")
/**
* Locale-ordered month and day followed by [timeOfDay] ("May 28 14:32" / "28 May 2:32 PM"). Pass
* [rememberTimeOfDayFormatter] so the clock follows the system 12/24-hour setting.
*/
fun formatMonthDayTime(
epochSeconds: Long,
timeOfDay: (epochMillis: Long) -> String,
): String {
val millis = epochSeconds * 1000L
return "${monthDay.format(millis)} ${timeOfDay(millis)}"
}
@@ -27,3 +27,6 @@ private val loggedNotice = ShortNotice { message -> Log.d("ShortNotice") { messa
@Composable
actual fun rememberShortNotice(): ShortNotice = loggedNotice
@Composable
actual fun rememberLongNotice(): ShortNotice = loggedNotice
@@ -27,3 +27,6 @@ private val loggedNotice = ShortNotice { message -> Log.d("ShortNotice") { messa
@Composable
actual fun rememberShortNotice(): ShortNotice = loggedNotice
@Composable
actual fun rememberLongNotice(): ShortNotice = loggedNotice
@@ -0,0 +1,32 @@
/*
* Copyright (c) 2025 Vitor Pamplona
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
* the Software without restriction, including without limitation the rights to use,
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
* Software, and to permit persons to whom the Software is furnished to do so,
* subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
package com.vitorpamplona.amethyst.commons.ui.components
import androidx.compose.runtime.Composable
import androidx.compose.ui.ExperimentalComposeUiApi
import androidx.compose.ui.backhandler.BackHandler
@OptIn(ExperimentalComposeUiApi::class)
@Composable
actual fun PlatformBackHandler(
enabled: Boolean,
onBack: () -> Unit,
) = BackHandler(enabled, onBack)
+1
View File
@@ -197,6 +197,7 @@ jetbrains-compose-runtime = { module = "org.jetbrains.compose.runtime:runtime",
jetbrains-compose-ui = { module = "org.jetbrains.compose.ui:ui", version.ref = "jetbrainsCompose" }
jetbrains-compose-ui-tooling = { module = "org.jetbrains.compose.ui:ui-tooling", version.ref = "jetbrainsCompose" }
jetbrains-compose-ui-tooling-preview = { module = "org.jetbrains.compose.ui:ui-tooling-preview", version.ref = "jetbrainsCompose" }
jetbrains-compose-ui-backhandler = { module = "org.jetbrains.compose.ui:ui-backhandler", version.ref = "jetbrainsCompose" }
jetbrains-compose-ui-test-junit4 = { module = "org.jetbrains.compose.ui:ui-test-junit4", version.ref = "jetbrainsCompose" }
google-mlkit-genai-proofreading = { group = "com.google.mlkit", name = "genai-proofreading", version.ref = "genaiProofreading" }
google-mlkit-genai-prompt = { group = "com.google.mlkit", name = "genai-prompt", version.ref = "genaiPrompt" }