mirror of
https://github.com/vitorpamplona/amethyst.git
synced 2026-10-06 03:38:23 +00:00
refactor: wallet, filename, language and local-time seams for more screens
- WalletAppLauncher opens any payment URI; payInvoice and payBolt12Offer are extensions, so Bolt12OffersDialog stops launching intents itself. - MediaUploader.displayName replaces MarmotFileUploader's content-resolver query; it no longer takes a Context. - LanguageOption/availableLanguages/languageDisplayName expects in commons replace java.util.Locale in the language settings. - RequestToVanishScreen uses LocalClock and rememberIs24HourClock. - Toasts in MarmotInviteDeviceBanner and Bolt12OffersDialog go through the shared notices; PodcastTopSupporters uses the KMP BigDecimal. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01S7FuNBSKiyVecARSoE4B9P
This commit is contained in:
+7
@@ -22,6 +22,7 @@ package com.vitorpamplona.amethyst.service.uploads
|
||||
|
||||
import android.content.Context
|
||||
import android.net.Uri
|
||||
import android.provider.OpenableColumns
|
||||
import android.webkit.MimeTypeMap
|
||||
import com.vitorpamplona.amethyst.Amethyst
|
||||
import com.vitorpamplona.amethyst.commons.model.Account
|
||||
@@ -605,4 +606,10 @@ class AndroidMediaUploader(
|
||||
val mimeType = MimeTypeMap.getSingleton().getMimeTypeFromExtension(fileExtension.lowercase(Locale.getDefault()))
|
||||
FileHeader.prepare(url, mimeType, null) { httpClients.okHttpClientForImage(it) }.getOrNull()
|
||||
}
|
||||
|
||||
override fun displayName(uri: MediaUri): String? =
|
||||
appContext.contentResolver.query(uri, null, null, null, null)?.use {
|
||||
val idx = it.getColumnIndex(OpenableColumns.DISPLAY_NAME)
|
||||
if (idx >= 0 && it.moveToFirst()) it.getString(idx) else null
|
||||
}
|
||||
}
|
||||
|
||||
-4
@@ -45,7 +45,6 @@ import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
@@ -232,7 +231,6 @@ fun MarmotGroupMessageComposer(
|
||||
) {
|
||||
val scope = rememberCoroutineScope()
|
||||
val canPost by remember { derivedStateOf { newMessageModel.canPost() } }
|
||||
val context = LocalContext.current
|
||||
val shortNotice = rememberShortNotice()
|
||||
|
||||
var isUploading by remember { mutableStateOf(false) }
|
||||
@@ -350,7 +348,6 @@ private fun MarmotGroupFileUploadDialog(
|
||||
onUpload: suspend () -> Unit,
|
||||
onCancel: () -> Unit,
|
||||
) {
|
||||
val context = LocalContext.current
|
||||
val shortNotice = rememberShortNotice()
|
||||
val longNotice = rememberLongNotice()
|
||||
val scope = rememberCoroutineScope()
|
||||
@@ -382,7 +379,6 @@ private fun MarmotGroupFileUploadDialog(
|
||||
longNotice.show("$title: $message")
|
||||
}
|
||||
},
|
||||
context = context,
|
||||
uploader = accountViewModel.host.mediaUploader,
|
||||
onceUploaded = { uploads ->
|
||||
MarmotFileSender(nostrGroupId, accountViewModel).send(uploads)
|
||||
|
||||
+7
-6
@@ -20,7 +20,6 @@
|
||||
*/
|
||||
package com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.marmotGroup
|
||||
|
||||
import android.widget.Toast
|
||||
import androidx.compose.animation.AnimatedVisibility
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
@@ -45,7 +44,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.unit.dp
|
||||
import com.vitorpamplona.amethyst.commons.icons.symbols.Icon
|
||||
import com.vitorpamplona.amethyst.commons.icons.symbols.MaterialSymbols
|
||||
@@ -57,6 +55,8 @@ import com.vitorpamplona.amethyst.commons.resources.marmot_invite_device_failure
|
||||
import com.vitorpamplona.amethyst.commons.resources.marmot_invite_device_publish
|
||||
import com.vitorpamplona.amethyst.commons.resources.marmot_invite_device_rejected
|
||||
import com.vitorpamplona.amethyst.commons.resources.marmot_invite_device_success
|
||||
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.stringRes
|
||||
import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel
|
||||
@@ -96,7 +96,8 @@ fun MarmotInviteDeviceBanner(
|
||||
// has already read and waved away.
|
||||
var dismissed by rememberSaveable { mutableStateOf(false) }
|
||||
val scope = rememberCoroutineScope()
|
||||
val context = LocalContext.current
|
||||
val shortNotice = rememberShortNotice()
|
||||
val longNotice = rememberLongNotice()
|
||||
// A read-only login cannot publish at all, so the only action this banner
|
||||
// offers is impossible for it. Better to say nothing than to offer a button
|
||||
// that silently does nothing.
|
||||
@@ -178,9 +179,9 @@ fun MarmotInviteDeviceBanner(
|
||||
val accepted = accountViewModel.republishKeyPackage()
|
||||
launch(Dispatchers.Main) {
|
||||
if (accepted) {
|
||||
Toast.makeText(context, successMessage, Toast.LENGTH_SHORT).show()
|
||||
shortNotice.show(successMessage)
|
||||
} else {
|
||||
Toast.makeText(context, rejectedMessage, Toast.LENGTH_LONG).show()
|
||||
longNotice.show(rejectedMessage)
|
||||
owner = LatestKeyPackageOwner.OTHER_DEVICE
|
||||
}
|
||||
}
|
||||
@@ -194,7 +195,7 @@ fun MarmotInviteDeviceBanner(
|
||||
val failureMessage =
|
||||
loadStringRes(Res.string.marmot_invite_device_failure, e.message ?: "")
|
||||
launch(Dispatchers.Main) {
|
||||
Toast.makeText(context, failureMessage, Toast.LENGTH_LONG).show()
|
||||
longNotice.show(failureMessage)
|
||||
// The warning was right after all, so put it back.
|
||||
owner = LatestKeyPackageOwner.OTHER_DEVICE
|
||||
}
|
||||
|
||||
+2
-17
@@ -20,8 +20,6 @@
|
||||
*/
|
||||
package com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.marmotGroup.send
|
||||
|
||||
import android.content.Context
|
||||
import android.net.Uri
|
||||
import com.vitorpamplona.amethyst.commons.model.Account
|
||||
import com.vitorpamplona.amethyst.commons.resources.Res
|
||||
import com.vitorpamplona.amethyst.commons.resources.failed_to_upload_media_no_details
|
||||
@@ -79,7 +77,6 @@ class MarmotFileUploader(
|
||||
viewState: ChatFileUploadState,
|
||||
exporterSecret: ByteArray,
|
||||
onError: (title: String, message: String) -> Unit,
|
||||
context: Context,
|
||||
uploader: MediaUploader,
|
||||
/**
|
||||
* Produce `encrypted-media-v2` references instead of MIP-04 ones.
|
||||
@@ -98,7 +95,7 @@ class MarmotFileUploader(
|
||||
val item = multiOrchestrator.get(i)
|
||||
val media = item.media
|
||||
val mimeType = media.mimeType ?: "application/octet-stream"
|
||||
val filename = resolveFilename(context, media.uri, mimeType)
|
||||
val filename = uploader.displayName(media.uri) ?: fallbackFilename(mimeType)
|
||||
|
||||
// v2 puts `m` inside both the key derivation and the AEAD
|
||||
// associated data, so it has to be the canonical form and not
|
||||
@@ -197,19 +194,7 @@ class MarmotFileUploader(
|
||||
viewState.mediaUploadTracker.finishUpload()
|
||||
}
|
||||
|
||||
private fun resolveFilename(
|
||||
context: Context,
|
||||
uri: Uri,
|
||||
mimeType: String,
|
||||
): String {
|
||||
val cursor = context.contentResolver.query(uri, null, null, null, null)
|
||||
val name =
|
||||
cursor?.use {
|
||||
val idx = it.getColumnIndex(android.provider.OpenableColumns.DISPLAY_NAME)
|
||||
if (idx >= 0 && it.moveToFirst()) it.getString(idx) else null
|
||||
}
|
||||
if (name != null) return name
|
||||
|
||||
private fun fallbackFilename(mimeType: String): String {
|
||||
val ext =
|
||||
when {
|
||||
mimeType.startsWith("image/jpeg") -> "jpg"
|
||||
|
||||
+2
-2
@@ -62,7 +62,7 @@ import com.vitorpamplona.amethyst.commons.ui.theme.grayText
|
||||
import com.vitorpamplona.amethyst.commons.util.showAmountInteger
|
||||
import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel
|
||||
import com.vitorpamplona.quartz.nip57Zaps.ZapReceiptEvent
|
||||
import java.math.BigDecimal
|
||||
import com.vitorpamplona.quartz.utils.BigDecimal
|
||||
|
||||
private val Gold = Color(0xFFFFC300)
|
||||
private val Silver = Color(0xFFB0B7C0)
|
||||
@@ -151,7 +151,7 @@ private fun SupporterRow(
|
||||
|
||||
ZapIcon(Size16Modifier, BitcoinOrange)
|
||||
Text(
|
||||
text = showAmountInteger(BigDecimal.valueOf(entry.totalSats)),
|
||||
text = showAmountInteger(BigDecimal(entry.totalSats)),
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
fontWeight = FontWeight.Bold,
|
||||
)
|
||||
|
||||
+9
-14
@@ -20,7 +20,6 @@
|
||||
*/
|
||||
package com.vitorpamplona.amethyst.ui.screen.loggedIn.profile.header
|
||||
|
||||
import android.widget.Toast
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
@@ -45,7 +44,6 @@ import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.LocalClipboard
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.text.font.FontFamily
|
||||
import androidx.compose.ui.text.input.KeyboardType
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
@@ -62,13 +60,15 @@ import com.vitorpamplona.amethyst.commons.resources.copy_to_clipboard
|
||||
import com.vitorpamplona.amethyst.commons.resources.no_wallet_found
|
||||
import com.vitorpamplona.amethyst.commons.ui.components.M3ActionDialog
|
||||
import com.vitorpamplona.amethyst.commons.ui.components.M3ActionSection
|
||||
import com.vitorpamplona.amethyst.commons.ui.components.rememberShortNotice
|
||||
import com.vitorpamplona.amethyst.commons.ui.components.util.setText
|
||||
import com.vitorpamplona.amethyst.commons.ui.loadStringRes
|
||||
import com.vitorpamplona.amethyst.commons.ui.stringRes
|
||||
import com.vitorpamplona.amethyst.commons.ui.theme.ButtonBorder
|
||||
import com.vitorpamplona.amethyst.commons.ui.theme.Size20Modifier
|
||||
import com.vitorpamplona.amethyst.commons.ui.wallet.payBolt12Offer
|
||||
import com.vitorpamplona.amethyst.commons.ui.wallet.rememberWalletAppLauncher
|
||||
import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel
|
||||
import com.vitorpamplona.amethyst.ui.note.payViaBolt12Intent
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
@Composable
|
||||
@@ -78,7 +78,8 @@ fun Bolt12OffersDialog(
|
||||
onDismiss: () -> Unit,
|
||||
) {
|
||||
val noWalletFoundStr = stringRes(Res.string.no_wallet_found)
|
||||
val context = LocalContext.current
|
||||
val walletApp = rememberWalletAppLauncher()
|
||||
val shortNotice = rememberShortNotice()
|
||||
val clipboardManager = LocalClipboard.current
|
||||
val scope = rememberCoroutineScope()
|
||||
|
||||
@@ -101,22 +102,16 @@ fun Bolt12OffersDialog(
|
||||
onCopy = {
|
||||
scope.launch {
|
||||
clipboardManager.setText(offer)
|
||||
Toast
|
||||
.makeText(
|
||||
context,
|
||||
loadStringRes(Res.string.copied_to_clipboard),
|
||||
Toast.LENGTH_SHORT,
|
||||
).show()
|
||||
shortNotice.show(loadStringRes(Res.string.copied_to_clipboard))
|
||||
}
|
||||
},
|
||||
onPayViaIntent = {
|
||||
payViaBolt12Intent(
|
||||
walletApp.payBolt12Offer(
|
||||
offer = offer,
|
||||
context = context,
|
||||
noWalletFoundStr,
|
||||
noWalletFound = noWalletFoundStr,
|
||||
onPaid = { onDismiss() },
|
||||
onError = { msg ->
|
||||
Toast.makeText(context, msg, Toast.LENGTH_SHORT).show()
|
||||
shortNotice.show(msg)
|
||||
},
|
||||
)
|
||||
},
|
||||
|
||||
+8
-14
@@ -20,7 +20,6 @@
|
||||
*/
|
||||
package com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.vanish
|
||||
|
||||
import android.text.format.DateFormat
|
||||
import androidx.compose.foundation.border
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
@@ -59,7 +58,6 @@ import androidx.compose.runtime.remember
|
||||
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.text.font.FontWeight
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
@@ -89,12 +87,14 @@ import com.vitorpamplona.amethyst.commons.resources.vanish_select_date
|
||||
import com.vitorpamplona.amethyst.commons.resources.vanish_select_time
|
||||
import com.vitorpamplona.amethyst.commons.resources.vanish_send_request
|
||||
import com.vitorpamplona.amethyst.commons.resources.vanish_target_relay
|
||||
import com.vitorpamplona.amethyst.commons.search.calendar.LocalClock
|
||||
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.note.DateTimeStyle
|
||||
import com.vitorpamplona.amethyst.commons.ui.note.formatDateTime
|
||||
import com.vitorpamplona.amethyst.commons.ui.note.rememberIs24HourClock
|
||||
import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.relays.common.BasicRelaySetupInfoDialog
|
||||
import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.relays.common.RelayUrlEditField
|
||||
import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.relays.common.relaySetupInfoBuilder
|
||||
@@ -108,9 +108,6 @@ import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.normalizer.displayUrl
|
||||
import com.vitorpamplona.quartz.utils.TimeUtils
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
import java.time.Instant
|
||||
import java.time.ZoneId
|
||||
import java.time.ZoneOffset
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
@@ -149,14 +146,13 @@ fun RequestToVanishScreen(
|
||||
initialSelectedDateMillis = vanishDate * 1000,
|
||||
)
|
||||
|
||||
val currentTime = Instant.ofEpochMilli(vanishDate * 1000).atZone(ZoneId.systemDefault()).toLocalDateTime()
|
||||
|
||||
val context = LocalContext.current
|
||||
// The vanish time as a local wall-clock time: seconds since the epoch shifted by this zone's offset.
|
||||
val localSecondOfDay = (vanishDate + LocalClock.utcOffsetSeconds(vanishDate)).mod(TimeUtils.ONE_DAY)
|
||||
val timePickerState =
|
||||
rememberTimePickerState(
|
||||
initialHour = currentTime.hour,
|
||||
initialMinute = currentTime.minute,
|
||||
is24Hour = DateFormat.is24HourFormat(context),
|
||||
initialHour = localSecondOfDay / TimeUtils.ONE_HOUR,
|
||||
initialMinute = (localSecondOfDay % TimeUtils.ONE_HOUR) / TimeUtils.ONE_MINUTE,
|
||||
is24Hour = rememberIs24HourClock(),
|
||||
)
|
||||
|
||||
val relayOptions =
|
||||
@@ -387,9 +383,7 @@ fun RequestToVanishScreen(
|
||||
(timePickerState.minute * TimeUtils.ONE_MINUTE)
|
||||
} ?: TimeUtils.now()
|
||||
|
||||
val offset: ZoneOffset = ZoneId.systemDefault().rules.getOffset(Instant.now())
|
||||
|
||||
vanishDate = datetimeLocalTimeZone - offset.totalSeconds
|
||||
vanishDate = datetimeLocalTimeZone - LocalClock.utcOffsetSeconds(TimeUtils.now())
|
||||
|
||||
showTimePicker = false
|
||||
},
|
||||
|
||||
+13
-17
@@ -91,9 +91,11 @@ import com.vitorpamplona.amethyst.commons.ui.theme.Size10dp
|
||||
import com.vitorpamplona.amethyst.commons.ui.theme.Size20dp
|
||||
import com.vitorpamplona.amethyst.commons.ui.theme.SpacedBy10dp
|
||||
import com.vitorpamplona.amethyst.commons.ui.theme.ThemeComparisonRow
|
||||
import com.vitorpamplona.amethyst.commons.util.LanguageOption
|
||||
import com.vitorpamplona.amethyst.commons.util.availableLanguages
|
||||
import com.vitorpamplona.amethyst.commons.util.languageDisplayName
|
||||
import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel
|
||||
import com.vitorpamplona.amethyst.commons.viewmodels.mockAccountViewModel
|
||||
import java.util.Locale as JavaLocale
|
||||
|
||||
@Preview(device = "spec:width=2160px,height=2340px,dpi=440")
|
||||
@Composable
|
||||
@@ -134,18 +136,12 @@ fun UserSettingsScreen(
|
||||
}
|
||||
}
|
||||
|
||||
private fun getAllLanguagesSorted(): List<JavaLocale> {
|
||||
val seen = mutableSetOf<String>()
|
||||
return JavaLocale
|
||||
.getAvailableLocales()
|
||||
.filter { it.language.isNotBlank() && it.country.isBlank() && seen.add(it.language) }
|
||||
.sortedBy { it.displayName.lowercase() }
|
||||
}
|
||||
private fun getAllLanguagesSorted(): List<LanguageOption> = availableLanguages().sortedBy { it.displayName.lowercase() }
|
||||
|
||||
@Composable
|
||||
private fun SearchableLanguageList(
|
||||
languages: List<JavaLocale>,
|
||||
onSelect: (JavaLocale) -> Unit,
|
||||
languages: List<LanguageOption>,
|
||||
onSelect: (LanguageOption) -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
var searchQuery by remember { mutableStateOf("") }
|
||||
@@ -225,7 +221,7 @@ fun TranslateToSetting(accountViewModel: AccountViewModel) {
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
Text(
|
||||
text = JavaLocale.forLanguageTag(currentTranslateTo).displayName,
|
||||
text = languageDisplayName(currentTranslateTo),
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
fontWeight = FontWeight.Medium,
|
||||
)
|
||||
@@ -277,7 +273,7 @@ fun DontTranslateFromSetting(accountViewModel: AccountViewModel) {
|
||||
InputChip(
|
||||
selected = true,
|
||||
onClick = { accountViewModel.removeDontTranslateFrom(languageCode) },
|
||||
label = { Text(JavaLocale.forLanguageTag(languageCode).displayName) },
|
||||
label = { Text(languageDisplayName(languageCode)) },
|
||||
trailingIcon = {
|
||||
Icon(
|
||||
symbol = MaterialSymbols.Close,
|
||||
@@ -379,8 +375,8 @@ private fun LanguagePreferenceCard(
|
||||
preference: String,
|
||||
accountViewModel: AccountViewModel,
|
||||
) {
|
||||
val sourceName = JavaLocale.forLanguageTag(source).displayName
|
||||
val targetName = JavaLocale.forLanguageTag(target).displayName
|
||||
val sourceName = languageDisplayName(source)
|
||||
val targetName = languageDisplayName(target)
|
||||
|
||||
OutlinedCard(modifier = Modifier.fillMaxWidth()) {
|
||||
Column(modifier = Modifier.padding(12.dp)) {
|
||||
@@ -464,9 +460,9 @@ private fun AddLanguagePairCard(
|
||||
onDismiss: () -> Unit,
|
||||
) {
|
||||
val allLanguages = remember { getAllLanguagesSorted() }
|
||||
var selectedSource by remember { mutableStateOf<JavaLocale?>(null) }
|
||||
var selectedTarget by remember { mutableStateOf<JavaLocale?>(null) }
|
||||
var selectedPreference by remember { mutableStateOf<JavaLocale?>(null) }
|
||||
var selectedSource by remember { mutableStateOf<LanguageOption?>(null) }
|
||||
var selectedTarget by remember { mutableStateOf<LanguageOption?>(null) }
|
||||
var selectedPreference by remember { mutableStateOf<LanguageOption?>(null) }
|
||||
var pickingSource by remember { mutableStateOf(false) }
|
||||
var pickingTarget by remember { mutableStateOf(false) }
|
||||
|
||||
|
||||
+3
@@ -86,6 +86,9 @@ interface MediaUploader {
|
||||
url: String,
|
||||
httpClients: IRoleBasedHttpClientBuilder,
|
||||
): FileHeader?
|
||||
|
||||
/** The file name the picker reported for [uri] (Android's `DISPLAY_NAME`), if any. */
|
||||
fun displayName(uri: MediaUri): String? = uri.lastPathSegmentOrNull()
|
||||
}
|
||||
|
||||
/** No media pipeline on this front end yet: every upload fails with a clear message. */
|
||||
|
||||
+33
@@ -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.util
|
||||
|
||||
/** A language the user can pick: its ISO code and its name in the user's own language. */
|
||||
data class LanguageOption(
|
||||
val language: String,
|
||||
val displayName: String,
|
||||
)
|
||||
|
||||
/** Every language the platform can name, one entry per language code (no regional variants). */
|
||||
expect fun availableLanguages(): List<LanguageOption>
|
||||
|
||||
/** The name of the language [tag] (a BCP-47 tag such as `pt` or `pt-BR`) in the user's language. */
|
||||
expect fun languageDisplayName(tag: String): String
|
||||
+35
@@ -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.util
|
||||
|
||||
import platform.Foundation.NSLocale
|
||||
import platform.Foundation.availableLocaleIdentifiers
|
||||
import platform.Foundation.currentLocale
|
||||
import platform.Foundation.localizedStringForLanguageCode
|
||||
|
||||
actual fun availableLanguages(): List<LanguageOption> =
|
||||
NSLocale.availableLocaleIdentifiers
|
||||
.mapNotNull { (it as? String)?.substringBefore('_')?.substringBefore('-') }
|
||||
.filter { it.isNotBlank() }
|
||||
.distinct()
|
||||
.map { LanguageOption(it, languageDisplayName(it)) }
|
||||
|
||||
actual fun languageDisplayName(tag: String): String = NSLocale.currentLocale.localizedStringForLanguageCode(tag) ?: tag
|
||||
+33
@@ -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.util
|
||||
|
||||
import java.util.Locale
|
||||
|
||||
actual fun availableLanguages(): List<LanguageOption> {
|
||||
val seen = mutableSetOf<String>()
|
||||
return Locale
|
||||
.getAvailableLocales()
|
||||
.filter { it.language.isNotBlank() && it.country.isBlank() && seen.add(it.language) }
|
||||
.map { LanguageOption(it.language, it.displayName) }
|
||||
}
|
||||
|
||||
actual fun languageDisplayName(tag: String): String = Locale.forLanguageTag(tag).displayName
|
||||
+2
-2
@@ -31,9 +31,9 @@ import kotlin.coroutines.cancellation.CancellationException
|
||||
actual fun rememberWalletAppLauncher(): WalletAppLauncher {
|
||||
val context = LocalContext.current
|
||||
return remember(context) {
|
||||
WalletAppLauncher { invoice, noWalletFound, onPaid, onError ->
|
||||
WalletAppLauncher { uri, noWalletFound, onPaid, onError ->
|
||||
try {
|
||||
val intent = Intent(Intent.ACTION_VIEW, "lightning:$invoice".toUri())
|
||||
val intent = Intent(Intent.ACTION_VIEW, uri.toUri())
|
||||
intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK
|
||||
|
||||
context.startActivity(intent)
|
||||
|
||||
+1
@@ -87,6 +87,7 @@ import com.vitorpamplona.amethyst.commons.ui.stringRes
|
||||
import com.vitorpamplona.amethyst.commons.ui.theme.DoubleVertSpacer
|
||||
import com.vitorpamplona.amethyst.commons.ui.theme.SimpleImage75Modifier
|
||||
import com.vitorpamplona.amethyst.commons.ui.theme.Size35dp
|
||||
import com.vitorpamplona.amethyst.commons.ui.wallet.payInvoice
|
||||
import com.vitorpamplona.amethyst.commons.ui.wallet.rememberWalletAppLauncher
|
||||
import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel
|
||||
import com.vitorpamplona.quartz.lightning.LnInvoiceUtil
|
||||
|
||||
+21
-4
@@ -25,16 +25,33 @@ import androidx.compose.runtime.Composable
|
||||
/** Hands a payment to a wallet app installed on the device. */
|
||||
fun interface WalletAppLauncher {
|
||||
/**
|
||||
* Opens [invoice] (BOLT-11) in the wallet app. [onPaid] runs once the wallet took it, which is
|
||||
* not a payment confirmation; [onError] gets [noWalletFound] when no wallet can open it.
|
||||
* Opens the payment [uri] (`lightning:` or `bitcoin:`) in the wallet app. [onPaid] runs once
|
||||
* the wallet took it, which is not a payment confirmation; [onError] gets [noWalletFound] when
|
||||
* no wallet can open it.
|
||||
*/
|
||||
fun payInvoice(
|
||||
invoice: String,
|
||||
fun openPaymentUri(
|
||||
uri: String,
|
||||
noWalletFound: String,
|
||||
onPaid: () -> Unit,
|
||||
onError: (String) -> Unit,
|
||||
)
|
||||
}
|
||||
|
||||
/** Opens a BOLT-11 [invoice] in the wallet app; see [WalletAppLauncher.openPaymentUri]. */
|
||||
fun WalletAppLauncher.payInvoice(
|
||||
invoice: String,
|
||||
noWalletFound: String,
|
||||
onPaid: () -> Unit,
|
||||
onError: (String) -> Unit,
|
||||
) = openPaymentUri("lightning:$invoice", noWalletFound, onPaid, onError)
|
||||
|
||||
/** Opens a BOLT-12 [offer] in the wallet app; see [WalletAppLauncher.openPaymentUri]. */
|
||||
fun WalletAppLauncher.payBolt12Offer(
|
||||
offer: String,
|
||||
noWalletFound: String,
|
||||
onPaid: () -> Unit,
|
||||
onError: (String) -> Unit,
|
||||
) = openPaymentUri("bitcoin:?lno=$offer", noWalletFound, onPaid, onError)
|
||||
|
||||
@Composable
|
||||
expect fun rememberWalletAppLauncher(): WalletAppLauncher
|
||||
|
||||
+3
-3
@@ -25,14 +25,14 @@ import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.platform.LocalUriHandler
|
||||
import kotlin.coroutines.cancellation.CancellationException
|
||||
|
||||
/** Opens the `lightning:` link with whatever the system registered for it. */
|
||||
/** Opens the payment link with whatever the system registered for it. */
|
||||
@Composable
|
||||
actual fun rememberWalletAppLauncher(): WalletAppLauncher {
|
||||
val uriHandler = LocalUriHandler.current
|
||||
return remember(uriHandler) {
|
||||
WalletAppLauncher { invoice, noWalletFound, onPaid, onError ->
|
||||
WalletAppLauncher { uri, noWalletFound, onPaid, onError ->
|
||||
try {
|
||||
uriHandler.openUri("lightning:$invoice")
|
||||
uriHandler.openUri(uri)
|
||||
onPaid()
|
||||
} catch (e: Exception) {
|
||||
if (e is CancellationException) throw e
|
||||
|
||||
+3
-3
@@ -25,14 +25,14 @@ import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.platform.LocalUriHandler
|
||||
import kotlin.coroutines.cancellation.CancellationException
|
||||
|
||||
/** Opens the `lightning:` link with whatever the system registered for it. */
|
||||
/** Opens the payment link with whatever the system registered for it. */
|
||||
@Composable
|
||||
actual fun rememberWalletAppLauncher(): WalletAppLauncher {
|
||||
val uriHandler = LocalUriHandler.current
|
||||
return remember(uriHandler) {
|
||||
WalletAppLauncher { invoice, noWalletFound, onPaid, onError ->
|
||||
WalletAppLauncher { uri, noWalletFound, onPaid, onError ->
|
||||
try {
|
||||
uriHandler.openUri("lightning:$invoice")
|
||||
uriHandler.openUri(uri)
|
||||
onPaid()
|
||||
} catch (e: Exception) {
|
||||
if (e is CancellationException) throw e
|
||||
|
||||
Reference in New Issue
Block a user