refactor: move the profile and the app, NIP-46, Namecoin and OTS settings screens

Clear the platform blockers in the profile and settings screens and move
them to commonsUI:

- Copying an npub, nprofile or picture URL uses the shared Clipboard.setText;
  sharing a profile uses TextSharer; copy confirmations are shared notices.
- The identity-claim brand icons (X, Telegram, Mastodon, GitHub) are Compose
  resources.
- AppPlatform gains SpeakNameButton (Android text-to-speech) and
  rememberAppLanguages (Android reads locales_config.xml).
- AppServices gains namecoinSettings, namecoinClients (a port over the
  JVM-only ElectrumX and Core RPC clients), otsSettings and nip46ClientStore,
  so these screens stop reading Amethyst.instance.
- OtsSettings and OtsSettingsStore move to commonMain on quartz's new
  BitcoinExplorerUrls; RpcProbeResult moves to quartz commonMain;
  deviceDescription() is an expect in commons.
- The payment sheet opens wallets through WalletAppLauncher; payViaIntent
  stays in the app for its remaining callers.
- The profile zaps total and ordering use the shared BigDecimal API.

210 of 252 screens are now shared.

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-10-04 16:42:09 +00:00
parent 19bc32d6c4
commit 6fe2a63dce
97 changed files with 800 additions and 438 deletions
@@ -207,6 +207,8 @@ import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.polls.PollsScreen
import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.polls.results.PollResultsScreen
import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.privacy.PrivacyOptionsScreen
import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.products.ProductsScreen
import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.profile.ProfileScreen
import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.profile.payment.SendPaymentScreen
import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.publicChats.PublicChatsScreen
import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.qrcode.ShowQRScreen
import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.redirect.LoadRedirectScreen
@@ -232,11 +234,14 @@ import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.settings.HomeTabsSe
import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.settings.MessagesSettingsScreen
import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.settings.MutedThreadsScreen
import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.settings.NIP47SetupScreen
import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.settings.NamecoinSettingsScreen
import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.settings.NowPlayingSettingsScreen
import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.settings.OtsSettingsScreen
import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.settings.ProfileUiSettingsScreen
import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.settings.ReactionsSettingsScreen
import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.settings.SearchEngineSettingsScreen
import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.settings.SecurityFiltersScreen
import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.settings.SettingsScreen
import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.settings.SpammingUsersScreen
import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.settings.UpdateZapAmountScreen
import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.settings.UserSettingsScreen
@@ -245,6 +250,7 @@ import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.settings.cordn.Cord
import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.settings.cordn.CordnHubScreen
import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.settings.cordn.CordnKeyPackagesScreen
import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.settings.cordn.CordnLinkScreen
import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.settings.nip46.Nip46ConnectedAppsScreen
import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.settings.nip46.Nip46SignerScreen
import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.shorts.ShortsScreen
import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.softwareapps.SoftwareAppDetailScreen
@@ -328,19 +334,13 @@ import com.vitorpamplona.amethyst.ui.screen.loggedIn.napplets.NappletsScreen
import com.vitorpamplona.amethyst.ui.screen.loggedIn.nests.NestsScreen
import com.vitorpamplona.amethyst.ui.screen.loggedIn.notifications.NotificationScreen
import com.vitorpamplona.amethyst.ui.screen.loggedIn.podcasts.authoring.NewPodcastEpisodeScreen
import com.vitorpamplona.amethyst.ui.screen.loggedIn.profile.ProfileScreen
import com.vitorpamplona.amethyst.ui.screen.loggedIn.profile.payment.SendPaymentScreen
import com.vitorpamplona.amethyst.ui.screen.loggedIn.qrcode.ScanQrImageScreen
import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.nip86.RelayManagementScreen
import com.vitorpamplona.amethyst.ui.screen.loggedIn.scheduledposts.ScheduledPostsScreen
import com.vitorpamplona.amethyst.ui.screen.loggedIn.settings.NamecoinSettingsScreen
import com.vitorpamplona.amethyst.ui.screen.loggedIn.settings.NotificationSettingsScreen
import com.vitorpamplona.amethyst.ui.screen.loggedIn.settings.OtsSettingsScreen
import com.vitorpamplona.amethyst.ui.screen.loggedIn.settings.ResourceUsageScreen
import com.vitorpamplona.amethyst.ui.screen.loggedIn.settings.SettingsScreen
import com.vitorpamplona.amethyst.ui.screen.loggedIn.settings.cordn.CordnBackupScreen
import com.vitorpamplona.amethyst.ui.screen.loggedIn.settings.cordn.CordnMigrateScreen
import com.vitorpamplona.amethyst.ui.screen.loggedIn.settings.nip46.Nip46ConnectedAppsScreen
import com.vitorpamplona.amethyst.ui.screen.loggedIn.video.hls.NewHlsVideoScreen
import com.vitorpamplona.amethyst.ui.screen.loggedIn.webBookmarks.WebBookmarksScreen
import com.vitorpamplona.amethyst.ui.screen.loggedIn.workouts.NewWorkoutScreen
@@ -0,0 +1,78 @@
/*
* Copyright (c) 2025 Vitor Pamplona
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
* the Software without restriction, including without limitation the rights to use,
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
* Software, and to permit persons to whom the Software is furnished to do so,
* subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
package com.vitorpamplona.amethyst.ui.note
import android.content.Context
import android.content.Intent
import androidx.core.net.toUri
import kotlin.coroutines.cancellation.CancellationException
fun payViaIntent(
invoice: String,
context: Context,
noWalletFound: String,
onPaid: () -> Unit,
onError: (String) -> Unit,
) {
try {
val intent = Intent(Intent.ACTION_VIEW, "lightning:$invoice".toUri())
intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK
context.startActivity(intent)
onPaid()
} catch (e: Exception) {
if (e is CancellationException) throw e
// don't display ugly error messages
// if (e.message != null) {
// onError(stringRes(Res.string.no_wallet_found_with_error, e.message!!))
// } else {
onError(noWalletFound)
// }
}
}
/**
* Hands a reusable BOLT12 offer (`lno1…`, from a recipient's kind:10058) off to an
* installed wallet. Unlike a BOLT11 invoice, a BOLT12 offer is NOT a `lightning:`
* payload — that scheme is defined for `lnbc…` invoices. Offers travel as the `lno`
* parameter of a BIP21/BIP321 bitcoin URI (`bitcoin:?lno=lno1…`), where the on-chain
* address is optional so a Lightning-only offer stands on its own. The wallet resolves
* the offer, collects the amount, and completes the payment; this is a plain intent,
* not a NIP-57/NIP-B1 zap, so it produces no Nostr receipt.
*/
fun payViaBolt12Intent(
offer: String,
context: Context,
noWalletFound: String,
onPaid: () -> Unit,
onError: (String) -> Unit,
) {
try {
val intent = Intent(Intent.ACTION_VIEW, "bitcoin:?lno=$offer".toUri())
intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK
context.startActivity(intent)
onPaid()
} catch (e: Exception) {
if (e is CancellationException) throw e
onError(noWalletFound)
}
}
@@ -192,6 +192,7 @@ import com.vitorpamplona.amethyst.commons.ui.note.elements.ShareOptionsBottomShe
import com.vitorpamplona.amethyst.commons.ui.note.platform.MAX_VOICE_RECORD_SECONDS
import com.vitorpamplona.amethyst.commons.ui.note.types.EditState
import com.vitorpamplona.amethyst.commons.ui.richtext.InLineIconRenderer
import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.profile.header.paymentTargetStyleFor
import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.wallet.OnchainZapSendDialog
import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.wallet.navigateToReloadMint
import com.vitorpamplona.amethyst.commons.ui.stringRes
@@ -232,7 +233,6 @@ import com.vitorpamplona.amethyst.model.zap.RailCapabilityResolver
import com.vitorpamplona.amethyst.service.payments.PayToAppAvailability
import com.vitorpamplona.amethyst.ui.actions.uploads.RecordAudioBox
import com.vitorpamplona.amethyst.ui.screen.loggedIn.profile.header.PaymentTargetsDialog
import com.vitorpamplona.amethyst.ui.screen.loggedIn.profile.header.paymentTargetStyleFor
import com.vitorpamplona.quartz.nip01Core.core.Event
import com.vitorpamplona.quartz.nip10Notes.BaseThreadedEvent
import com.vitorpamplona.quartz.nip10Notes.TextNoteEvent
@@ -20,8 +20,6 @@
*/
package com.vitorpamplona.amethyst.ui.note
import android.content.Context
import android.content.Intent
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.ExperimentalLayoutApi
@@ -58,7 +56,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.font.FontWeight
import androidx.compose.ui.text.input.KeyboardCapitalization
import androidx.compose.ui.text.input.KeyboardType
@@ -68,7 +65,6 @@ import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.compose.ui.window.Dialog
import androidx.compose.ui.window.DialogProperties
import androidx.core.net.toUri
import androidx.lifecycle.ViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.lifecycle.viewmodel.compose.viewModel
@@ -118,12 +114,14 @@ import com.vitorpamplona.amethyst.commons.ui.theme.Size55dp
import com.vitorpamplona.amethyst.commons.ui.theme.StdHorzSpacer
import com.vitorpamplona.amethyst.commons.ui.theme.ZeroPadding
import com.vitorpamplona.amethyst.commons.ui.theme.placeholderText
import com.vitorpamplona.amethyst.commons.ui.wallet.payInvoice
import com.vitorpamplona.amethyst.commons.ui.wallet.rememberWalletAppLauncher
import com.vitorpamplona.amethyst.commons.util.showAmount
import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel
import com.vitorpamplona.quartz.nip01Core.core.Event
import com.vitorpamplona.quartz.nip57Zaps.ZapReceiptEvent
import com.vitorpamplona.quartz.utils.BigDecimal
import kotlinx.collections.immutable.ImmutableList
import kotlinx.coroutines.CancellationException
class ZapOptionViewModel : ViewModel() {
private var account: Account? = null
@@ -266,7 +264,7 @@ fun ZapCustomDialog(
onClick = {
postViewModel.customAmount = TextFieldValue(amount.toString())
},
label = { Text("⚡ ${showAmount(amount.toBigDecimal())}") },
label = { Text("⚡ ${showAmount(BigDecimal(amount))}") },
)
}
}
@@ -517,7 +515,7 @@ fun DisplayPayable(
}
Row {
Text(
text = showAmount((payable.amountMilliSats / 1000.0f).toBigDecimal()),
text = showAmount(BigDecimal((payable.amountMilliSats / 1000.0f).toString())),
maxLines = 1,
overflow = TextOverflow.Ellipsis,
fontWeight = FontWeight.Bold,
@@ -535,10 +533,10 @@ fun DisplayPayable(
}
Spacer(modifier = DoubleHorzSpacer)
val context = LocalContext.current
val walletLauncher = rememberWalletAppLauncher()
PayButton(isActive = !paid.value) {
payViaIntent(payable.invoice, context, noWalletFoundStr, { paid.value = true }) {
walletLauncher.payInvoice(payable.invoice, noWalletFoundStr, { paid.value = true }) {
accountViewModel.toastManager.toast(
Res.string.error_dialog_zap_error,
UserBasedErrorMessage(it, payable.info.user),
@@ -548,58 +546,6 @@ fun DisplayPayable(
}
}
fun payViaIntent(
invoice: String,
context: Context,
noWalletFound: String,
onPaid: () -> Unit,
onError: (String) -> Unit,
) {
try {
val intent = Intent(Intent.ACTION_VIEW, "lightning:$invoice".toUri())
intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK
context.startActivity(intent)
onPaid()
} catch (e: Exception) {
if (e is CancellationException) throw e
// don't display ugly error messages
// if (e.message != null) {
// onError(stringRes(Res.string.no_wallet_found_with_error, e.message!!))
// } else {
onError(noWalletFound)
// }
}
}
/**
* Hands a reusable BOLT12 offer (`lno1…`, from a recipient's kind:10058) off to an
* installed wallet. Unlike a BOLT11 invoice, a BOLT12 offer is NOT a `lightning:`
* payload — that scheme is defined for `lnbc…` invoices. Offers travel as the `lno`
* parameter of a BIP21/BIP321 bitcoin URI (`bitcoin:?lno=lno1…`), where the on-chain
* address is optional so a Lightning-only offer stands on its own. The wallet resolves
* the offer, collects the amount, and completes the payment; this is a plain intent,
* not a NIP-57/NIP-B1 zap, so it produces no Nostr receipt.
*/
fun payViaBolt12Intent(
offer: String,
context: Context,
noWalletFound: String,
onPaid: () -> Unit,
onError: (String) -> Unit,
) {
try {
val intent = Intent(Intent.ACTION_VIEW, "bitcoin:?lno=$offer".toUri())
intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK
context.startActivity(intent)
onPaid()
} catch (e: Exception) {
if (e is CancellationException) throw e
onError(noWalletFound)
}
}
@Composable
fun PayButton(
isActive: Boolean,
@@ -57,6 +57,7 @@ import com.vitorpamplona.amethyst.commons.resources.clink_requesting_invoice
import com.vitorpamplona.amethyst.commons.resources.error_dialog_pay_invoice_error
import com.vitorpamplona.amethyst.commons.resources.pay
import com.vitorpamplona.amethyst.commons.resources.sats
import com.vitorpamplona.amethyst.commons.service.ClinkOfferPayer
import com.vitorpamplona.amethyst.commons.ui.loadStringRes
import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav
import com.vitorpamplona.amethyst.commons.ui.navigation.routes.routeFor
@@ -71,7 +72,6 @@ import com.vitorpamplona.amethyst.commons.ui.theme.Size25dp
import com.vitorpamplona.amethyst.commons.ui.theme.SmallBorder
import com.vitorpamplona.amethyst.commons.ui.theme.StdHorzSpacer
import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel
import com.vitorpamplona.amethyst.service.ClinkOfferPayer
import com.vitorpamplona.amethyst.ui.components.PaymentCard
import com.vitorpamplona.amethyst.ui.components.PaymentCardAmount
import com.vitorpamplona.quartz.experimental.clink.common.SatRange
@@ -38,8 +38,10 @@ import com.vitorpamplona.amethyst.commons.ui.settings.SettingsCategory
import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel
import com.vitorpamplona.amethyst.favorites.FavoriteAppLauncher
import com.vitorpamplona.amethyst.ui.navigation.topbars.AndroidAroundMeLocationLabel
import com.vitorpamplona.amethyst.ui.note.DrawPlayName
import com.vitorpamplona.amethyst.ui.screen.loggedIn.nests.room.activity.NestActivity
import com.vitorpamplona.quartz.concord.cord02Community.ImagePointer
import kotlinx.collections.immutable.ImmutableMap
import com.vitorpamplona.amethyst.commons.ui.navigation.bottombars.AppBottomBar as AppBottomBarImpl
import com.vitorpamplona.amethyst.favorites.rememberManifestIconModel as AppRememberManifestIconModel
import com.vitorpamplona.amethyst.favorites.rememberNappletIconModel as AppRememberNappletIconModel
@@ -60,6 +62,15 @@ object AndroidAppPlatform : AppPlatform {
// Per flavour: Play links the hosted policies, F-Droid surfaces none.
override fun legalSettingsCategory(uriHandler: UriHandler): SettingsCategory? = flavorLegalSettingsCategory(uriHandler)
@Composable
override fun SpeakNameButton(name: String) = DrawPlayName(name)
@Composable
override fun rememberAppLanguages(): ImmutableMap<String, String> {
val context = LocalContext.current
return remember(context) { context.getLangPreferenceDropdownEntries() }
}
@Composable
override fun AppBottomBar(
selectedRoute: Route?,
@@ -23,18 +23,26 @@ package com.vitorpamplona.amethyst.ui.platform
import com.vitorpamplona.amethyst.Amethyst
import com.vitorpamplona.amethyst.commons.browser.BrowserHistoryRegistry
import com.vitorpamplona.amethyst.commons.browser.BrowserIconRegistry
import com.vitorpamplona.amethyst.commons.connectedApps.nip46.Nip46ClientStore
import com.vitorpamplona.amethyst.commons.connectedApps.signers.NostrSignerPermissionStore
import com.vitorpamplona.amethyst.commons.favorites.FavoriteAppsRegistry
import com.vitorpamplona.amethyst.commons.model.location.DeviceLocation
import com.vitorpamplona.amethyst.commons.model.preferences.AppPreferenceStores
import com.vitorpamplona.amethyst.commons.model.preferences.NamecoinSettingsStore
import com.vitorpamplona.amethyst.commons.model.preferences.OtsSettingsStore
import com.vitorpamplona.amethyst.commons.napplet.permissions.NappletPermissionLedger
import com.vitorpamplona.amethyst.commons.service.AppServices
import com.vitorpamplona.amethyst.commons.service.BlossomServerFinder
import com.vitorpamplona.amethyst.commons.service.ai.AltTextSuggester
import com.vitorpamplona.amethyst.commons.service.namecoin.NamecoinClients
import com.vitorpamplona.amethyst.commons.tor.TorSettingsFlow
import com.vitorpamplona.amethyst.service.ai.MLKitImageLabelService
import com.vitorpamplona.amethyst.service.location.CachedReversedGeoLocations
import com.vitorpamplona.quartz.nip05DnsIdentifiers.namecoin.ElectrumxServer
import com.vitorpamplona.quartz.nip05DnsIdentifiers.namecoin.NamecoinCoreRpcConfig
import com.vitorpamplona.quartz.nip05DnsIdentifiers.namecoin.NamecoinNameResolver
import com.vitorpamplona.quartz.nip05DnsIdentifiers.namecoin.RpcProbeResult
import com.vitorpamplona.quartz.nip05DnsIdentifiers.namecoin.ServerTestResult
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.map
@@ -64,6 +72,14 @@ object AndroidAppServices : AppServices {
override val namecoinResolver: NamecoinNameResolver get() = Amethyst.instance.namecoinResolver
override val namecoinSettings: NamecoinSettingsStore get() = Amethyst.instance.namecoinPrefs
override val namecoinClients: NamecoinClients = AndroidNamecoinClients
override val otsSettings: OtsSettingsStore get() = Amethyst.instance.otsPrefs
override val nip46ClientStore: Nip46ClientStore get() = Amethyst.instance.nip46ClientStore
override val blossomServerFinder: BlossomServerFinder = AndroidBlossomServerFinder
override val deviceLocation: DeviceLocation get() = Amethyst.instance.locationManager
@@ -85,3 +101,19 @@ private object AndroidBlossomServerFinder : BlossomServerFinder {
.findServers(blossomUri)
?.serverUrl
}
/** The app's ElectrumX and Namecoin Core clients, built lazily by [Amethyst.instance]. */
private object AndroidNamecoinClients : NamecoinClients {
override suspend fun testElectrumxServer(server: ElectrumxServer): ServerTestResult = Amethyst.instance.electrumXClient.testServer(server)
override suspend fun probeCoreRpc(config: NamecoinCoreRpcConfig): RpcProbeResult = Amethyst.instance.namecoinCoreRpcClient.probe(config)
override fun addPinnedCert(pem: String) {
Amethyst.instance.electrumXClient.addPinnedCert(pem)
Amethyst.instance.namecoinCoreRpcClient.addPinnedCert(pem)
}
override fun setCoreRpcConfig(config: NamecoinCoreRpcConfig) {
Amethyst.instance.namecoinCoreRpcClient.setConfig(config)
}
}
@@ -0,0 +1,64 @@
/*
* Copyright (c) 2025 Vitor Pamplona
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
* the Software without restriction, including without limitation the rights to use,
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
* Software, and to permit persons to whom the Software is furnished to do so,
* subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
package com.vitorpamplona.amethyst.ui.platform
import android.content.Context
import androidx.core.os.LocaleListCompat
import com.vitorpamplona.amethyst.R
import kotlinx.collections.immutable.ImmutableMap
import kotlinx.collections.immutable.toImmutableMap
import org.xmlpull.v1.XmlPullParser
import org.xmlpull.v1.XmlPullParserException
import java.io.IOException
private fun Context.getLocaleListFromXml(): LocaleListCompat {
val tagsList = mutableListOf<CharSequence>()
try {
val xpp: XmlPullParser = resources.getXml(R.xml.locales_config)
while (xpp.eventType != XmlPullParser.END_DOCUMENT) {
if (xpp.eventType == XmlPullParser.START_TAG && xpp.name == "locale") {
tagsList.add(xpp.getAttributeValue(0))
}
xpp.next()
}
} catch (e: XmlPullParserException) {
e.printStackTrace()
} catch (e: IOException) {
e.printStackTrace()
}
return LocaleListCompat.forLanguageTags(tagsList.joinToString(","))
}
fun Context.getLangPreferenceDropdownEntries(): ImmutableMap<String, String> {
val localeList = getLocaleListFromXml()
val map = mutableMapOf<String, String>()
for (a in 0 until localeList.size()) {
localeList[a].let {
map.put(
it!!.getDisplayName(it).replaceFirstChar { char -> char.uppercase() },
it.toLanguageTag(),
)
}
}
return map.toImmutableMap()
}
@@ -126,6 +126,12 @@ import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav
import com.vitorpamplona.amethyst.commons.ui.navigation.topbars.TopBarWithBackButton
import com.vitorpamplona.amethyst.commons.ui.platform.rememberManifestIconModel
import com.vitorpamplona.amethyst.commons.ui.platform.rememberWebAppIconModel
import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.settings.nip46.Nip46AppIcon
import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.settings.nip46.Nip46LiveStatus
import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.settings.nip46.Nip46ReconnectPill
import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.settings.nip46.Nip46StatusDot
import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.settings.nip46.nip46AppOnline
import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.settings.nip46.nip46ClientSubtitle
import com.vitorpamplona.amethyst.commons.ui.stringRes
import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel
import com.vitorpamplona.amethyst.napplet.NappletBrokerService
@@ -134,12 +140,6 @@ import com.vitorpamplona.amethyst.napplet.counterpartyLabel
import com.vitorpamplona.amethyst.napplet.descriptionRes
import com.vitorpamplona.amethyst.napplet.labelRes
import com.vitorpamplona.amethyst.napplet.resolveNappletMeta
import com.vitorpamplona.amethyst.ui.screen.loggedIn.settings.nip46.Nip46AppIcon
import com.vitorpamplona.amethyst.ui.screen.loggedIn.settings.nip46.Nip46LiveStatus
import com.vitorpamplona.amethyst.ui.screen.loggedIn.settings.nip46.Nip46ReconnectPill
import com.vitorpamplona.amethyst.ui.screen.loggedIn.settings.nip46.Nip46StatusDot
import com.vitorpamplona.amethyst.ui.screen.loggedIn.settings.nip46.nip46AppOnline
import com.vitorpamplona.amethyst.ui.screen.loggedIn.settings.nip46.nip46ClientSubtitle
import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl
import com.vitorpamplona.quartz.nip01Core.relay.normalizer.RelayUrlNormalizer
import com.vitorpamplona.quartz.nip42RelayAuth.RelayAuthEvent
@@ -66,6 +66,8 @@ import com.vitorpamplona.amethyst.commons.ui.components.M3ActionSection
import com.vitorpamplona.amethyst.commons.ui.components.util.setText
import com.vitorpamplona.amethyst.commons.ui.loadStringRes
import com.vitorpamplona.amethyst.commons.ui.note.ErrorMessageDialog
import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.profile.header.PaymentTargetPill
import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.profile.header.paymentTargetUri
import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.qrcode.QrCodeDrawer
import com.vitorpamplona.amethyst.commons.ui.stringRes
import com.vitorpamplona.amethyst.commons.ui.theme.Size20Modifier
@@ -29,8 +29,8 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav
import com.vitorpamplona.amethyst.commons.ui.screen.RefresheableFeedView
import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.profile.pinnedNotes.dal.UserProfilePinnedNotesFeedViewModel
import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel
import com.vitorpamplona.amethyst.ui.screen.loggedIn.profile.pinnedNotes.dal.UserProfilePinnedNotesFeedViewModel
@Composable
fun TabPinnedNotes(
@@ -1,4 +0,0 @@
<vector android:height="128dp" android:viewportHeight="512"
android:viewportWidth="512" android:width="128dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="#3A6CAC" android:fillType="evenOdd" android:pathData="M296.13,354.17c49.88,-5.89 102.94,-24.03 102.94,-110.19c0,-24.49 -8.62,-44.45 -22.67,-59.87c2.27,-5.89 9.52,-28.11 -2.73,-58.95c0,0 -18.14,-5.9 -60.76,22.67c-18.14,-4.98 -38.09,-8.16 -56.68,-8.16c-19.05,0 -39.01,3.18 -56.7,8.16c-43.08,-28.57 -61.22,-22.67 -61.22,-22.67c-12.24,30.83 -4.98,53.06 -2.72,58.95c-14.06,15.42 -22.68,35.38 -22.68,59.87c0,86.16 53.06,104.3 102.94,110.19c-6.34,5.45 -12.24,15.87 -14.51,30.39c-12.7,5.44 -45.81,15.87 -65.76,-18.59c0,0 -11.8,-21.31 -34.01,-22.67c0,0 -22.22,-0.45 -1.81,13.59c0,0 14.96,6.81 24.94,32.65c0,0 13.6,43.09 76.18,29.48v38.54c0,5.91 -4.53,12.7 -15.86,10.89C96.14,438.98 32.2,354.63 32.2,255.77c0,-123.81 100.22,-224.02 224.03,-224.02c123.35,0 224.02,100.22 223.57,224.02c0,98.86 -63.95,182.75 -152.83,212.69c-11.34,2.27 -15.87,-4.53 -15.87,-10.89V395.45C311.1,374.58 304.29,360.98 296.13,354.17L296.13,354.17zM512,256.23C512,114.73 397.26,0 256.23,0C114.73,0 0,114.73 0,256.23C0,397.26 114.73,512 256.23,512C397.26,512 512,397.26 512,256.23L512,256.23z"/>
</vector>
@@ -477,7 +477,16 @@ assemblers, `EventSync`, …). Packages are renamed on the way:
voice reply, the calendar collections and event detail, the geocache detail and editor,
the live-activity channel, chess, Cordn group creation, the QR share and the compose
settings. 204 of 252 screens shared.
- **Next:** 48 screens remain, and what blocks them is the platform itself:
- **Wave 11 (2026-10-04):** Profile and settings. `AppServices` gained `namecoinSettings`,
`namecoinClients` (a port over the JVM-only ElectrumX and Core RPC clients), `otsSettings`
and `nip46ClientStore`. `AppPlatform` gained `rememberAppLanguages` (Android reads
`locales_config.xml`) and `SpeakNameButton` (text-to-speech). `OtsSettings` and its store are
in commonMain, on quartz's new `BitcoinExplorerUrls`; `RpcProbeResult` is too.
`deviceDescription()` is an expect in commons. Copy-to-clipboard goes through the shared
`Clipboard.setText`; the brand icons for identity claims are Compose resources. Moved: the
profile (60 files: header, tabs, payment sheet), the app, NIP-46, Namecoin and OTS settings.
210 of 252 screens shared.
- **Next:** 42 screens remain, and what blocks them is the platform itself:
- `ReactionsRow` (2.7k lines: pay-to-app, voice replies, wallet intents).
- The Nests activity and PiP.
- The call screens.
@@ -0,0 +1,25 @@
/*
* Copyright (c) 2025 Vitor Pamplona
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
* the Software without restriction, including without limitation the rights to use,
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
* Software, and to permit persons to whom the Software is furnished to do so,
* subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
package com.vitorpamplona.amethyst.commons.util
import android.os.Build
actual fun deviceDescription(): String = "${Build.MANUFACTURER} ${Build.MODEL}, Android ${Build.VERSION.RELEASE} (API ${Build.VERSION.SDK_INT})"
@@ -21,7 +21,7 @@
package com.vitorpamplona.amethyst.commons.model.nip03Timestamp
import androidx.compose.runtime.Stable
import com.vitorpamplona.quartz.nip03Timestamp.okhttp.OkHttpBitcoinExplorer
import com.vitorpamplona.quartz.nip03Timestamp.BitcoinExplorerUrls
import kotlinx.serialization.Serializable
/**
@@ -57,8 +57,8 @@ data class OtsSettings(
val KNOWN_EXPLORERS =
listOf(
OkHttpBitcoinExplorer.MEMPOOL_API_URL to "mempool.space (Tor-friendly)",
OkHttpBitcoinExplorer.BLOCKSTREAM_API_URL to "blockstream.info",
BitcoinExplorerUrls.MEMPOOL to "mempool.space (Tor-friendly)",
BitcoinExplorerUrls.BLOCKSTREAM to "blockstream.info",
)
fun isValidUrl(url: String): Boolean {
@@ -22,12 +22,16 @@ package com.vitorpamplona.amethyst.commons.service
import com.vitorpamplona.amethyst.commons.browser.BrowserHistoryRegistry
import com.vitorpamplona.amethyst.commons.browser.BrowserIconRegistry
import com.vitorpamplona.amethyst.commons.connectedApps.nip46.Nip46ClientStore
import com.vitorpamplona.amethyst.commons.connectedApps.signers.NostrSignerPermissionStore
import com.vitorpamplona.amethyst.commons.favorites.FavoriteAppsRegistry
import com.vitorpamplona.amethyst.commons.model.location.DeviceLocation
import com.vitorpamplona.amethyst.commons.model.preferences.AppPreferenceStores
import com.vitorpamplona.amethyst.commons.model.preferences.NamecoinSettingsStore
import com.vitorpamplona.amethyst.commons.model.preferences.OtsSettingsStore
import com.vitorpamplona.amethyst.commons.napplet.permissions.NappletPermissionLedger
import com.vitorpamplona.amethyst.commons.service.ai.AltTextSuggester
import com.vitorpamplona.amethyst.commons.service.namecoin.NamecoinClients
import com.vitorpamplona.amethyst.commons.tor.TorSettingsFlow
import com.vitorpamplona.quartz.nip05DnsIdentifiers.namecoin.NamecoinNameResolver
import kotlinx.coroutines.flow.Flow
@@ -64,6 +68,18 @@ interface AppServices {
/** Resolves `.bit` names and `d/`/`id/` identifiers over the configured ElectrumX servers. */
val namecoinResolver: NamecoinNameResolver
/** The Namecoin backend settings (servers, Core RPC, pinned certificates). */
val namecoinSettings: NamecoinSettingsStore
/** The live Namecoin clients the settings screen tests and reconfigures. */
val namecoinClients: NamecoinClients get() = NamecoinClients.None
/** The OpenTimestamps blockchain explorer setting. */
val otsSettings: OtsSettingsStore
/** The NIP-46 remote-signer clients this device has connected to. */
val nip46ClientStore: Nip46ClientStore
/** Finds the server behind a `blossom:` URI (BUD-10). */
val blossomServerFinder: BlossomServerFinder get() = BlossomServerFinder.None
@@ -0,0 +1,53 @@
/*
* 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.namecoin
import com.vitorpamplona.quartz.nip05DnsIdentifiers.namecoin.ElectrumxServer
import com.vitorpamplona.quartz.nip05DnsIdentifiers.namecoin.NamecoinCoreRpcConfig
import com.vitorpamplona.quartz.nip05DnsIdentifiers.namecoin.RpcProbeResult
import com.vitorpamplona.quartz.nip05DnsIdentifiers.namecoin.ServerTestResult
/**
* What the Namecoin settings screen asks of the live clients: test a server, probe a Namecoin
* Core node, and hand them a pinned certificate or a new Core RPC config. The ElectrumX and Core
* RPC clients themselves are JVM-only (raw TLS sockets, OkHttp), so the screen reaches them here.
*/
interface NamecoinClients {
suspend fun testElectrumxServer(server: ElectrumxServer): ServerTestResult
suspend fun probeCoreRpc(config: NamecoinCoreRpcConfig): RpcProbeResult
/** Pins a self-signed certificate on both backends, so the user confirms it once. */
fun addPinnedCert(pem: String)
fun setCoreRpcConfig(config: NamecoinCoreRpcConfig)
/** No clients: every test reports that nothing is available. */
object None : NamecoinClients {
override suspend fun testElectrumxServer(server: ElectrumxServer): ServerTestResult = ServerTestResult(server = server, success = false, responseTimeMs = 0, error = "Namecoin is not available on this platform")
override suspend fun probeCoreRpc(config: NamecoinCoreRpcConfig): RpcProbeResult = RpcProbeResult(success = false, elapsedMs = 0, error = "Namecoin is not available on this platform")
override fun addPinnedCert(pem: String) {}
override fun setCoreRpcConfig(config: NamecoinCoreRpcConfig) {}
}
}
@@ -0,0 +1,24 @@
/*
* Copyright (c) 2025 Vitor Pamplona
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
* the Software without restriction, including without limitation the rights to use,
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
* Software, and to permit persons to whom the Software is furnished to do so,
* subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
package com.vitorpamplona.amethyst.commons.util
/** A one-line description of this device and OS, for diagnostic screens: "Google Pixel 8, Android 15 (API 35)". */
expect fun deviceDescription(): String
@@ -0,0 +1,28 @@
/*
* 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.UIKit.UIDevice
actual fun deviceDescription(): String {
val device = UIDevice.currentDevice
return "${device.model}, ${device.systemName} ${device.systemVersion}"
}
@@ -0,0 +1,23 @@
/*
* 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
actual fun deviceDescription(): String = "${System.getProperty("os.name")} ${System.getProperty("os.version")} (${System.getProperty("os.arch")}), Java ${System.getProperty("java.version")}"
@@ -18,7 +18,7 @@
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
package com.vitorpamplona.amethyst.service
package com.vitorpamplona.amethyst.commons.service
import com.vitorpamplona.amethyst.commons.model.Account
import com.vitorpamplona.amethyst.commons.tor.MoneyOpRelayRouting
@@ -36,6 +36,7 @@ import com.vitorpamplona.quartz.nip01Core.signers.NostrSignerInternal
import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.CompletableDeferred
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.IO
import kotlinx.coroutines.withContext
import kotlinx.coroutines.withTimeoutOrNull
@@ -18,9 +18,8 @@
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
package com.vitorpamplona.amethyst.ui.components.util
package com.vitorpamplona.amethyst.commons.ui.components.util
import android.widget.Toast
import androidx.compose.foundation.combinedClickable
import androidx.compose.material3.LocalTextStyle
import androidx.compose.material3.MaterialTheme
@@ -30,12 +29,12 @@ import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalClipboard
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.style.TextOverflow
import com.vitorpamplona.amethyst.commons.resources.Res
import com.vitorpamplona.amethyst.commons.resources.copied_to_clipboard
import com.vitorpamplona.amethyst.commons.resources.copy_to_clipboard
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
@@ -44,7 +43,7 @@ import org.jetbrains.compose.resources.StringResource
/**
* Text composable that opens [onClick] on a tap and copies [copyValue] to the
* system clipboard on a long-press (with a Toast confirmation).
* system clipboard on a long-press (with a short notice confirming it).
*
* This is the long-press-to-copy counterpart of [com.vitorpamplona.amethyst.commons.ui.components.ClickableTextPrimary].
* It deliberately uses a plain [Text] + [combinedClickable] outer modifier
@@ -58,7 +57,7 @@ import org.jetbrains.compose.resources.StringResource
* @param copyValue raw value placed on the clipboard on long-press
* (typically the full, unmodified profile field).
* @param onClick tap action — usually "open the link" or "expand zap UI".
* @param toastResId string resource shown via [Toast.LENGTH_SHORT] after the
* @param toastResId string resource shown as a short notice after the
* value is placed on the clipboard. Defaults to a generic
* "Copied to clipboard" message.
* @param onLongClickLabelResId accessibility label exposed to TalkBack for
@@ -78,7 +77,7 @@ fun LongPressCopyText(
toastResId: StringResource = Res.string.copied_to_clipboard,
onLongClickLabelResId: StringResource = Res.string.copy_to_clipboard,
) {
val context = LocalContext.current
val shortNotice = rememberShortNotice()
val clipboard = LocalClipboard.current
val scope = rememberCoroutineScope()
val longClickLabel = stringRes(onLongClickLabelResId)
@@ -96,7 +95,7 @@ fun LongPressCopyText(
onLongClick = {
scope.launch {
clipboard.setText(copyValue)
Toast.makeText(context, loadStringRes(toastResId), Toast.LENGTH_SHORT).show()
shortNotice.show(loadStringRes(toastResId))
}
},
onLongClickLabel = longClickLabel,
@@ -32,6 +32,8 @@ import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.qrcode.ScanOutcome
import com.vitorpamplona.amethyst.commons.ui.settings.SettingsCategory
import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel
import com.vitorpamplona.quartz.concord.cord02Community.ImagePointer
import kotlinx.collections.immutable.ImmutableMap
import kotlinx.collections.immutable.persistentMapOf
/**
* The app shell's pieces that shared screens embed but only the front end can draw yet: the
@@ -47,6 +49,13 @@ interface AppPlatform {
/** The settings screen's legal links, when the build's distribution calls for them. */
fun legalSettingsCategory(uriHandler: UriHandler): SettingsCategory? = null
/**
* The languages the app is translated into, as display name ("Deutsch") to language tag
* ("de"), for the language picker. Empty where the platform follows the system language only.
*/
@Composable
fun rememberAppLanguages(): ImmutableMap<String, String> = persistentMapOf()
@Composable
fun AppBottomBar(
selectedRoute: Route?,
@@ -58,6 +67,10 @@ interface AppPlatform {
@Composable
fun AroundMeLocationLabel() {}
/** A button that reads [name] aloud with the platform's text-to-speech; nothing where there is none. */
@Composable
fun SpeakNameButton(name: String) {}
/** A full-screen dialog to pick a place (map, search, "use my location"), as a geohash. */
@Composable
fun GeohashLocationPickerDialog(
@@ -135,6 +148,9 @@ fun AppBottomBar(
@Composable
fun AroundMeLocationLabel() = LocalAppPlatform.current.AroundMeLocationLabel()
@Composable
fun SpeakNameButton(name: String) = LocalAppPlatform.current.SpeakNameButton(name)
@Composable
fun GeohashLocationPickerDialog(
initialGeohash: String?,
@@ -18,7 +18,7 @@
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
package com.vitorpamplona.amethyst.ui.screen.loggedIn.profile
package com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.profile
import androidx.compose.foundation.ScrollState
import androidx.compose.foundation.gestures.scrollBy
@@ -63,7 +63,6 @@ import androidx.compose.ui.layout.onSizeChanged
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.unit.IntSize
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.lifecycle.viewmodel.compose.viewModel
import com.vitorpamplona.amethyst.commons.model.User
import com.vitorpamplona.amethyst.commons.model.cache.LocalCache
import com.vitorpamplona.amethyst.commons.model.navigation.Route
@@ -76,47 +75,48 @@ import com.vitorpamplona.amethyst.commons.resources.mutual
import com.vitorpamplona.amethyst.commons.resources.notes
import com.vitorpamplona.amethyst.commons.resources.profile_tab_apps
import com.vitorpamplona.amethyst.commons.resources.replies
import com.vitorpamplona.amethyst.commons.ui.components.rememberViewModel
import com.vitorpamplona.amethyst.commons.ui.feeds.UserBlockedFeed
import com.vitorpamplona.amethyst.commons.ui.feeds.WatchLifecycleAndUpdateModel
import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav
import com.vitorpamplona.amethyst.commons.ui.platform.AppBottomBar
import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.profile.apps.TabApps
import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.profile.apps.dal.UserProfileAppsFeedViewModel
import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.profile.bookmarks.BookmarkTabHeader
import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.profile.bookmarks.TabBookmarks
import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.profile.bookmarks.dal.UserProfileBookmarksFeedViewModel
import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.profile.conversations.TabNotesConversations
import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.profile.conversations.dal.UserProfileConversationsFeedViewModel
import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.profile.followers.FollowersTabHeader
import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.profile.followers.TabFollowers
import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.profile.followers.dal.UserProfileFollowersUserFeedViewModel
import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.profile.follows.FollowTabHeader
import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.profile.follows.TabFollows
import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.profile.follows.dal.UserProfileFollowsUserFeedViewModel
import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.profile.gallery.TabGallery
import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.profile.gallery.dal.UserProfileGalleryFeedViewModel
import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.profile.hashtags.FollowedTagsTabHeader
import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.profile.hashtags.TabFollowedTags
import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.profile.header.ProfileHeader
import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.profile.header.ProfileTopBar
import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.profile.header.apps.UserAppRecommendationsFeedViewModel
import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.profile.mutual.TabMutualConversations
import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.profile.mutual.dal.UserProfileMutualFeedViewModel
import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.profile.newthreads.TabNotesNewThreads
import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.profile.newthreads.dal.UserProfileNewThreadsFeedViewModel
import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.profile.pinnedNotes.dal.UserProfilePinnedNotesFeedViewModel
import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.profile.relays.TabRelays
import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.profile.reports.ReportsTabHeader
import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.profile.reports.TabReports
import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.profile.reports.dal.UserProfileReportFeedViewModel
import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.profile.zaps.TabReceivedZaps
import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.profile.zaps.ZapTabHeader
import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.profile.zaps.dal.UserProfileZapsViewModel
import com.vitorpamplona.amethyst.commons.ui.stringRes
import com.vitorpamplona.amethyst.commons.ui.theme.DividerThickness
import com.vitorpamplona.amethyst.commons.ui.theme.Size8dp
import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel
import com.vitorpamplona.amethyst.commons.viewmodels.UserExternalIdentitiesViewModel
import com.vitorpamplona.amethyst.ui.screen.loggedIn.profile.apps.TabApps
import com.vitorpamplona.amethyst.ui.screen.loggedIn.profile.apps.dal.UserProfileAppsFeedViewModel
import com.vitorpamplona.amethyst.ui.screen.loggedIn.profile.bookmarks.BookmarkTabHeader
import com.vitorpamplona.amethyst.ui.screen.loggedIn.profile.bookmarks.TabBookmarks
import com.vitorpamplona.amethyst.ui.screen.loggedIn.profile.bookmarks.dal.UserProfileBookmarksFeedViewModel
import com.vitorpamplona.amethyst.ui.screen.loggedIn.profile.conversations.TabNotesConversations
import com.vitorpamplona.amethyst.ui.screen.loggedIn.profile.conversations.dal.UserProfileConversationsFeedViewModel
import com.vitorpamplona.amethyst.ui.screen.loggedIn.profile.followers.FollowersTabHeader
import com.vitorpamplona.amethyst.ui.screen.loggedIn.profile.followers.TabFollowers
import com.vitorpamplona.amethyst.ui.screen.loggedIn.profile.followers.dal.UserProfileFollowersUserFeedViewModel
import com.vitorpamplona.amethyst.ui.screen.loggedIn.profile.follows.FollowTabHeader
import com.vitorpamplona.amethyst.ui.screen.loggedIn.profile.follows.TabFollows
import com.vitorpamplona.amethyst.ui.screen.loggedIn.profile.follows.dal.UserProfileFollowsUserFeedViewModel
import com.vitorpamplona.amethyst.ui.screen.loggedIn.profile.gallery.TabGallery
import com.vitorpamplona.amethyst.ui.screen.loggedIn.profile.gallery.dal.UserProfileGalleryFeedViewModel
import com.vitorpamplona.amethyst.ui.screen.loggedIn.profile.hashtags.FollowedTagsTabHeader
import com.vitorpamplona.amethyst.ui.screen.loggedIn.profile.hashtags.TabFollowedTags
import com.vitorpamplona.amethyst.ui.screen.loggedIn.profile.header.ProfileHeader
import com.vitorpamplona.amethyst.ui.screen.loggedIn.profile.header.ProfileTopBar
import com.vitorpamplona.amethyst.ui.screen.loggedIn.profile.header.apps.UserAppRecommendationsFeedViewModel
import com.vitorpamplona.amethyst.ui.screen.loggedIn.profile.mutual.TabMutualConversations
import com.vitorpamplona.amethyst.ui.screen.loggedIn.profile.mutual.dal.UserProfileMutualFeedViewModel
import com.vitorpamplona.amethyst.ui.screen.loggedIn.profile.newthreads.TabNotesNewThreads
import com.vitorpamplona.amethyst.ui.screen.loggedIn.profile.newthreads.dal.UserProfileNewThreadsFeedViewModel
import com.vitorpamplona.amethyst.ui.screen.loggedIn.profile.pinnedNotes.dal.UserProfilePinnedNotesFeedViewModel
import com.vitorpamplona.amethyst.ui.screen.loggedIn.profile.relays.TabRelays
import com.vitorpamplona.amethyst.ui.screen.loggedIn.profile.reports.ReportsTabHeader
import com.vitorpamplona.amethyst.ui.screen.loggedIn.profile.reports.TabReports
import com.vitorpamplona.amethyst.ui.screen.loggedIn.profile.reports.dal.UserProfileReportFeedViewModel
import com.vitorpamplona.amethyst.ui.screen.loggedIn.profile.zaps.TabReceivedZaps
import com.vitorpamplona.amethyst.ui.screen.loggedIn.profile.zaps.ZapTabHeader
import com.vitorpamplona.amethyst.ui.screen.loggedIn.profile.zaps.dal.UserProfileZapsViewModel
import kotlinx.coroutines.launch
@Composable
@@ -154,7 +154,7 @@ fun PrepareViewModels(
nav: INav,
) {
val followsFeedViewModel: UserProfileFollowsUserFeedViewModel =
viewModel(
rememberViewModel(
key = baseUser.pubkeyHex + "UserProfileFollowsUserFeedViewModel",
factory =
UserProfileFollowsUserFeedViewModel.Factory(
@@ -164,7 +164,7 @@ fun PrepareViewModels(
)
val galleryFeedViewModel: UserProfileGalleryFeedViewModel =
viewModel(
rememberViewModel(
key = baseUser.pubkeyHex + "UserGalleryFeedViewModel",
factory =
UserProfileGalleryFeedViewModel.Factory(
@@ -174,7 +174,7 @@ fun PrepareViewModels(
)
val followersFeedViewModel: UserProfileFollowersUserFeedViewModel =
viewModel(
rememberViewModel(
key = baseUser.pubkeyHex + "UserProfileFollowersUserFeedViewModel",
factory =
UserProfileFollowersUserFeedViewModel.Factory(
@@ -184,26 +184,26 @@ fun PrepareViewModels(
)
val appRecommendations: UserAppRecommendationsFeedViewModel =
viewModel(
rememberViewModel(
key = baseUser.pubkeyHex + "UserAppRecommendationsFeedViewModel",
factory = UserAppRecommendationsFeedViewModel.Factory(baseUser),
)
val externalIdentities: UserExternalIdentitiesViewModel =
viewModel(
rememberViewModel(
key = baseUser.pubkeyHex + "UserExternalIdentitiesViewModel",
factory = UserExternalIdentitiesViewModel.Factory(baseUser),
)
val zapFeedViewModel: UserProfileZapsViewModel =
viewModel(
rememberViewModel(
key = baseUser.pubkeyHex + "UserProfileZapsFeedViewModel",
factory =
UserProfileZapsViewModel.Factory(baseUser, accountViewModel.account),
)
val threadsViewModel: UserProfileNewThreadsFeedViewModel =
viewModel(
rememberViewModel(
key = baseUser.pubkeyHex + "UserProfileNewThreadsFeedViewModel",
factory =
UserProfileNewThreadsFeedViewModel.Factory(
@@ -213,7 +213,7 @@ fun PrepareViewModels(
)
val repliesViewModel: UserProfileConversationsFeedViewModel =
viewModel(
rememberViewModel(
key = baseUser.pubkeyHex + "UserProfileConversationsFeedViewModel",
factory =
UserProfileConversationsFeedViewModel.Factory(
@@ -223,7 +223,7 @@ fun PrepareViewModels(
)
val mutualViewModel: UserProfileMutualFeedViewModel =
viewModel(
rememberViewModel(
key = baseUser.pubkeyHex + "UserProfileMutualFeedViewModel",
factory =
UserProfileMutualFeedViewModel.Factory(
@@ -233,7 +233,7 @@ fun PrepareViewModels(
)
val bookmarksFeedViewModel: UserProfileBookmarksFeedViewModel =
viewModel(
rememberViewModel(
key = baseUser.pubkeyHex + "UserProfileBookmarksFeedViewModel",
factory =
UserProfileBookmarksFeedViewModel.Factory(
@@ -243,7 +243,7 @@ fun PrepareViewModels(
)
val pinnedNotesFeedViewModel: UserProfilePinnedNotesFeedViewModel =
viewModel(
rememberViewModel(
key = baseUser.pubkeyHex + "UserProfilePinnedNotesFeedViewModel",
factory =
UserProfilePinnedNotesFeedViewModel.Factory(
@@ -253,7 +253,7 @@ fun PrepareViewModels(
)
val reportsFeedViewModel: UserProfileReportFeedViewModel =
viewModel(
rememberViewModel(
key = baseUser.pubkeyHex + "UserProfileReportFeedViewModel",
factory =
UserProfileReportFeedViewModel.Factory(
@@ -263,7 +263,7 @@ fun PrepareViewModels(
)
val appsFeedViewModel: UserProfileAppsFeedViewModel =
viewModel(
rememberViewModel(
key = baseUser.pubkeyHex + "UserProfileAppsFeedViewModel",
factory =
UserProfileAppsFeedViewModel.Factory(
@@ -18,7 +18,7 @@
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
package com.vitorpamplona.amethyst.ui.screen.loggedIn.profile.apps
package com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.profile.apps
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxHeight
@@ -29,8 +29,8 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav
import com.vitorpamplona.amethyst.commons.ui.screen.RefresheableFeedView
import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.profile.apps.dal.UserProfileAppsFeedViewModel
import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel
import com.vitorpamplona.amethyst.ui.screen.loggedIn.profile.apps.dal.UserProfileAppsFeedViewModel
/**
* Profile tab listing the user's published nsites and napplets. Each row is the inert feed card;
@@ -18,7 +18,7 @@
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
package com.vitorpamplona.amethyst.ui.screen.loggedIn.profile.apps.dal
package com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.profile.apps.dal
import com.vitorpamplona.amethyst.commons.feeds.AdditiveFeedFilter
import com.vitorpamplona.amethyst.commons.feeds.FilterByListParams
@@ -18,14 +18,16 @@
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
package com.vitorpamplona.amethyst.ui.screen.loggedIn.profile.apps.dal
package com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.profile.apps.dal
import androidx.compose.runtime.Stable
import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.viewmodel.CreationExtras
import com.vitorpamplona.amethyst.commons.model.Account
import com.vitorpamplona.amethyst.commons.model.User
import com.vitorpamplona.amethyst.commons.ui.screen.AndroidFeedViewModel
import kotlin.reflect.KClass
@Stable
class UserProfileAppsFeedViewModel(
@@ -37,6 +39,9 @@ class UserProfileAppsFeedViewModel(
val account: Account,
) : ViewModelProvider.Factory {
@Suppress("UNCHECKED_CAST")
override fun <T : ViewModel> create(modelClass: Class<T>): T = UserProfileAppsFeedViewModel(user, account) as T
override fun <T : ViewModel> create(
modelClass: KClass<T>,
extras: CreationExtras,
): T = UserProfileAppsFeedViewModel(user, account) as T
}
}
@@ -18,7 +18,7 @@
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
package com.vitorpamplona.amethyst.ui.screen.loggedIn.profile.bookmarks
package com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.profile.bookmarks
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
@@ -18,7 +18,7 @@
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
package com.vitorpamplona.amethyst.ui.screen.loggedIn.profile.bookmarks
package com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.profile.bookmarks
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxHeight
@@ -29,8 +29,8 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav
import com.vitorpamplona.amethyst.commons.ui.screen.RefresheableFeedView
import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.profile.bookmarks.dal.UserProfileBookmarksFeedViewModel
import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel
import com.vitorpamplona.amethyst.ui.screen.loggedIn.profile.bookmarks.dal.UserProfileBookmarksFeedViewModel
@Composable
fun TabBookmarks(
@@ -18,7 +18,7 @@
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
package com.vitorpamplona.amethyst.ui.screen.loggedIn.profile.bookmarks.dal
package com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.profile.bookmarks.dal
import com.vitorpamplona.amethyst.commons.feeds.FeedFilter
import com.vitorpamplona.amethyst.commons.model.Account
@@ -18,14 +18,16 @@
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
package com.vitorpamplona.amethyst.ui.screen.loggedIn.profile.bookmarks.dal
package com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.profile.bookmarks.dal
import androidx.compose.runtime.Stable
import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.viewmodel.CreationExtras
import com.vitorpamplona.amethyst.commons.model.Account
import com.vitorpamplona.amethyst.commons.model.User
import com.vitorpamplona.amethyst.commons.ui.screen.AndroidFeedViewModel
import kotlin.reflect.KClass
@Stable
class UserProfileBookmarksFeedViewModel(
@@ -37,6 +39,9 @@ class UserProfileBookmarksFeedViewModel(
val account: Account,
) : ViewModelProvider.Factory {
@Suppress("UNCHECKED_CAST")
override fun <T : ViewModel> create(modelClass: Class<T>): T = UserProfileBookmarksFeedViewModel(user, account) as T
override fun <T : ViewModel> create(
modelClass: KClass<T>,
extras: CreationExtras,
): T = UserProfileBookmarksFeedViewModel(user, account) as T
}
}
@@ -18,7 +18,7 @@
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
package com.vitorpamplona.amethyst.ui.screen.loggedIn.profile.conversations
package com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.profile.conversations
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxHeight
@@ -26,8 +26,8 @@ import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav
import com.vitorpamplona.amethyst.commons.ui.screen.RefresheableFeedView
import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.profile.conversations.dal.UserProfileConversationsFeedViewModel
import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel
import com.vitorpamplona.amethyst.ui.screen.loggedIn.profile.conversations.dal.UserProfileConversationsFeedViewModel
@Composable
fun TabNotesConversations(
@@ -20,7 +20,7 @@
*/
@file:Suppress("DEPRECATION")
package com.vitorpamplona.amethyst.ui.screen.loggedIn.profile.conversations.dal
package com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.profile.conversations.dal
import com.vitorpamplona.amethyst.commons.feeds.AdditiveFeedFilter
import com.vitorpamplona.amethyst.commons.feeds.sortedByDefaultFeedOrder
@@ -18,14 +18,16 @@
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
package com.vitorpamplona.amethyst.ui.screen.loggedIn.profile.conversations.dal
package com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.profile.conversations.dal
import androidx.compose.runtime.Stable
import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.viewmodel.CreationExtras
import com.vitorpamplona.amethyst.commons.model.Account
import com.vitorpamplona.amethyst.commons.model.User
import com.vitorpamplona.amethyst.commons.ui.screen.AndroidFeedViewModel
import kotlin.reflect.KClass
@Stable
class UserProfileConversationsFeedViewModel(
@@ -37,6 +39,9 @@ class UserProfileConversationsFeedViewModel(
val account: Account,
) : ViewModelProvider.Factory {
@Suppress("UNCHECKED_CAST")
override fun <T : ViewModel> create(modelClass: Class<T>): T = UserProfileConversationsFeedViewModel(user, account) as T
override fun <T : ViewModel> create(
modelClass: KClass<T>,
extras: CreationExtras,
): T = UserProfileConversationsFeedViewModel(user, account) as T
}
}
@@ -18,7 +18,7 @@
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
package com.vitorpamplona.amethyst.ui.screen.loggedIn.profile.followers
package com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.profile.followers
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
@@ -28,9 +28,9 @@ import com.vitorpamplona.amethyst.commons.model.User
import com.vitorpamplona.amethyst.commons.relayClient.user.observeUserAssertionsFollowerCount
import com.vitorpamplona.amethyst.commons.resources.Res
import com.vitorpamplona.amethyst.commons.resources.number_followers
import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.profile.followers.dal.UserProfileFollowersUserFeedViewModel
import com.vitorpamplona.amethyst.commons.ui.stringRes
import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel
import com.vitorpamplona.amethyst.ui.screen.loggedIn.profile.followers.dal.UserProfileFollowersUserFeedViewModel
@Composable
fun FollowersTabHeader(
@@ -18,7 +18,7 @@
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
package com.vitorpamplona.amethyst.ui.screen.loggedIn.profile.followers
package com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.profile.followers
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxHeight
@@ -33,10 +33,10 @@ import androidx.compose.ui.Modifier
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav
import com.vitorpamplona.amethyst.commons.ui.note.UserCompose
import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.profile.followers.dal.UserProfileFollowersUserFeedViewModel
import com.vitorpamplona.amethyst.commons.ui.theme.DividerThickness
import com.vitorpamplona.amethyst.commons.ui.theme.FeedPadding
import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel
import com.vitorpamplona.amethyst.ui.screen.loggedIn.profile.followers.dal.UserProfileFollowersUserFeedViewModel
@Composable
fun TabFollowers(
@@ -18,24 +18,27 @@
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
package com.vitorpamplona.amethyst.ui.screen.loggedIn.profile.followers.dal
package com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.profile.followers.dal
import androidx.compose.runtime.Stable
import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.viewModelScope
import androidx.lifecycle.viewmodel.CreationExtras
import com.vitorpamplona.amethyst.commons.model.Account
import com.vitorpamplona.amethyst.commons.model.User
import com.vitorpamplona.quartz.nip01Core.core.Event
import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter
import com.vitorpamplona.quartz.nip02FollowList.ContactListEvent
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.IO
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.flowOn
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.sample
import kotlinx.coroutines.flow.stateIn
import kotlin.reflect.KClass
@Stable
class UserProfileFollowersUserFeedViewModel(
@@ -92,6 +95,9 @@ class UserProfileFollowersUserFeedViewModel(
val account: Account,
) : ViewModelProvider.Factory {
@Suppress("UNCHECKED_CAST")
override fun <T : ViewModel> create(modelClass: Class<T>): T = UserProfileFollowersUserFeedViewModel(user, account) as T
override fun <T : ViewModel> create(
modelClass: KClass<T>,
extras: CreationExtras,
): T = UserProfileFollowersUserFeedViewModel(user, account) as T
}
}
@@ -18,7 +18,7 @@
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
package com.vitorpamplona.amethyst.ui.screen.loggedIn.profile.follows
package com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.profile.follows
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
@@ -26,9 +26,9 @@ import androidx.compose.runtime.getValue
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.vitorpamplona.amethyst.commons.resources.Res
import com.vitorpamplona.amethyst.commons.resources.number_following
import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.profile.follows.dal.UserProfileFollowsUserFeedViewModel
import com.vitorpamplona.amethyst.commons.ui.stringRes
import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel
import com.vitorpamplona.amethyst.ui.screen.loggedIn.profile.follows.dal.UserProfileFollowsUserFeedViewModel
@Composable
fun FollowTabHeader(
@@ -18,7 +18,7 @@
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
package com.vitorpamplona.amethyst.ui.screen.loggedIn.profile.follows
package com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.profile.follows
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxHeight
@@ -33,10 +33,10 @@ import androidx.compose.ui.Modifier
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav
import com.vitorpamplona.amethyst.commons.ui.note.UserCompose
import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.profile.follows.dal.UserProfileFollowsUserFeedViewModel
import com.vitorpamplona.amethyst.commons.ui.theme.DividerThickness
import com.vitorpamplona.amethyst.commons.ui.theme.FeedPadding
import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel
import com.vitorpamplona.amethyst.ui.screen.loggedIn.profile.follows.dal.UserProfileFollowsUserFeedViewModel
@Composable
fun TabFollows(
@@ -18,17 +18,19 @@
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
package com.vitorpamplona.amethyst.ui.screen.loggedIn.profile.follows.dal
package com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.profile.follows.dal
import androidx.compose.runtime.Stable
import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.viewModelScope
import androidx.lifecycle.viewmodel.CreationExtras
import com.vitorpamplona.amethyst.commons.model.Account
import com.vitorpamplona.amethyst.commons.model.User
import com.vitorpamplona.amethyst.commons.model.cache.LocalCache
import com.vitorpamplona.quartz.nip02FollowList.ContactListEvent
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.IO
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.flowOn
@@ -36,6 +38,7 @@ import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.onStart
import kotlinx.coroutines.flow.sample
import kotlinx.coroutines.flow.stateIn
import kotlin.reflect.KClass
@Stable
class UserProfileFollowsUserFeedViewModel(
@@ -89,6 +92,9 @@ class UserProfileFollowsUserFeedViewModel(
val account: Account,
) : ViewModelProvider.Factory {
@Suppress("UNCHECKED_CAST")
override fun <T : ViewModel> create(modelClass: Class<T>): T = UserProfileFollowsUserFeedViewModel(user, account) as T
override fun <T : ViewModel> create(
modelClass: KClass<T>,
extras: CreationExtras,
): T = UserProfileFollowsUserFeedViewModel(user, account) as T
}
}
@@ -18,7 +18,7 @@
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
package com.vitorpamplona.amethyst.ui.screen.loggedIn.profile.gallery
package com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.profile.gallery
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.combinedClickable
@@ -18,7 +18,7 @@
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
package com.vitorpamplona.amethyst.ui.screen.loggedIn.profile.gallery
package com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.profile.gallery
import androidx.compose.animation.core.tween
import androidx.compose.foundation.layout.Arrangement.spacedBy
@@ -18,7 +18,7 @@
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
package com.vitorpamplona.amethyst.ui.screen.loggedIn.profile.gallery
package com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.profile.gallery
import androidx.compose.runtime.Composable
import androidx.compose.runtime.mutableStateOf
@@ -18,7 +18,7 @@
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
package com.vitorpamplona.amethyst.ui.screen.loggedIn.profile.gallery
package com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.profile.gallery
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxHeight
@@ -28,8 +28,8 @@ import androidx.compose.ui.Modifier
import com.vitorpamplona.amethyst.commons.ui.feeds.ScrollStateKeys
import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav
import com.vitorpamplona.amethyst.commons.ui.screen.SaveableGridFeedState
import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.profile.gallery.dal.UserProfileGalleryFeedViewModel
import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel
import com.vitorpamplona.amethyst.ui.screen.loggedIn.profile.gallery.dal.UserProfileGalleryFeedViewModel
@Composable
fun TabGallery(
@@ -18,7 +18,7 @@
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
package com.vitorpamplona.amethyst.ui.screen.loggedIn.profile.gallery.dal
package com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.profile.gallery.dal
import com.vitorpamplona.amethyst.commons.feeds.AdditiveFeedFilter
import com.vitorpamplona.amethyst.commons.feeds.FilterByListParams
@@ -18,14 +18,16 @@
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
package com.vitorpamplona.amethyst.ui.screen.loggedIn.profile.gallery.dal
package com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.profile.gallery.dal
import androidx.compose.runtime.Stable
import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.viewmodel.CreationExtras
import com.vitorpamplona.amethyst.commons.model.Account
import com.vitorpamplona.amethyst.commons.model.User
import com.vitorpamplona.amethyst.commons.ui.screen.AndroidFeedViewModel
import kotlin.reflect.KClass
@Stable
class UserProfileGalleryFeedViewModel(
@@ -37,6 +39,9 @@ class UserProfileGalleryFeedViewModel(
val account: Account,
) : ViewModelProvider.Factory {
@Suppress("UNCHECKED_CAST")
override fun <T : ViewModel> create(modelClass: Class<T>): T = UserProfileGalleryFeedViewModel(user, account) as T
override fun <T : ViewModel> create(
modelClass: KClass<T>,
extras: CreationExtras,
): T = UserProfileGalleryFeedViewModel(user, account) as T
}
}
@@ -18,7 +18,7 @@
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
package com.vitorpamplona.amethyst.ui.screen.loggedIn.profile.hashtags
package com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.profile.hashtags
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
@@ -18,7 +18,7 @@
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
package com.vitorpamplona.amethyst.ui.screen.loggedIn.profile.hashtags
package com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.profile.hashtags
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxHeight
@@ -18,7 +18,7 @@
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
package com.vitorpamplona.amethyst.ui.screen.loggedIn.profile.header
package com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.profile.header
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
@@ -18,7 +18,7 @@
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
package com.vitorpamplona.amethyst.ui.screen.loggedIn.profile.header
package com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.profile.header
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
@@ -35,10 +35,10 @@ import com.vitorpamplona.amethyst.commons.resources.Res
import com.vitorpamplona.amethyst.commons.resources.error_dialog_payment_error
import com.vitorpamplona.amethyst.commons.resources.no_payment_app_found_for_type
import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav
import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.profile.payment.ProfilePaymentMethod
import com.vitorpamplona.amethyst.commons.ui.theme.BitcoinOrange
import com.vitorpamplona.amethyst.commons.ui.theme.Size16Modifier
import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel
import com.vitorpamplona.amethyst.ui.screen.loggedIn.profile.payment.ProfilePaymentMethod
import com.vitorpamplona.quartz.nipA3PaymentTargets.PaymentTarget
import com.vitorpamplona.quartz.nipBCOnchainZaps.taproot.SegwitAddress
@@ -18,9 +18,8 @@
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
package com.vitorpamplona.amethyst.ui.screen.loggedIn.profile.header
package com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.profile.header
import android.content.ClipData
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.ExperimentalLayoutApi
import androidx.compose.foundation.layout.Row
@@ -40,16 +39,13 @@ import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.ClipEntry
import androidx.compose.ui.platform.LocalClipboard
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalUriHandler
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.vitorpamplona.amethyst.R
import com.vitorpamplona.amethyst.commons.buzz.ui.BuzzAgentLabel
import com.vitorpamplona.amethyst.commons.icons.symbols.Icon
import com.vitorpamplona.amethyst.commons.icons.symbols.MaterialSymbols
@@ -67,12 +63,19 @@ import com.vitorpamplona.amethyst.commons.resources.show_nprofile_as_a_qr_code
import com.vitorpamplona.amethyst.commons.resources.telegram
import com.vitorpamplona.amethyst.commons.resources.twitter
import com.vitorpamplona.amethyst.commons.resources.website
import com.vitorpamplona.amethyst.commons.resources.x
import com.vitorpamplona.amethyst.commons.ui.components.TranslatableRichTextViewer
import com.vitorpamplona.amethyst.commons.ui.components.util.LongPressCopyText
import com.vitorpamplona.amethyst.commons.ui.components.util.setText
import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav
import com.vitorpamplona.amethyst.commons.ui.note.ObserveAndRenderNIP05VerifiedSymbol
import com.vitorpamplona.amethyst.commons.ui.note.lastSeenSentence
import com.vitorpamplona.amethyst.commons.ui.painterRes
import com.vitorpamplona.amethyst.commons.ui.platform.SpeakNameButton
import com.vitorpamplona.amethyst.commons.ui.richtext.CreateTextWithEmoji
import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.profile.header.apps.DisplayAppRecommendations
import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.profile.header.apps.UserAppRecommendationsFeedViewModel
import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.profile.header.badges.DisplayBadges
import com.vitorpamplona.amethyst.commons.ui.stringRes
import com.vitorpamplona.amethyst.commons.ui.theme.Size15Modifier
import com.vitorpamplona.amethyst.commons.ui.theme.SpacedBy3dp
@@ -82,12 +85,6 @@ import com.vitorpamplona.amethyst.commons.ui.theme.placeholderText
import com.vitorpamplona.amethyst.commons.util.toShortDisplay
import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel
import com.vitorpamplona.amethyst.commons.viewmodels.UserExternalIdentitiesViewModel
import com.vitorpamplona.amethyst.ui.components.util.LongPressCopyText
import com.vitorpamplona.amethyst.ui.note.DrawPlayName
import com.vitorpamplona.amethyst.ui.painterRes
import com.vitorpamplona.amethyst.ui.screen.loggedIn.profile.header.apps.DisplayAppRecommendations
import com.vitorpamplona.amethyst.ui.screen.loggedIn.profile.header.apps.UserAppRecommendationsFeedViewModel
import com.vitorpamplona.amethyst.ui.screen.loggedIn.profile.header.badges.DisplayBadges
import com.vitorpamplona.quartz.nip01Core.core.Event
import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter
import com.vitorpamplona.quartz.nip39ExtIdentities.GitHubIdentity
@@ -96,9 +93,11 @@ import com.vitorpamplona.quartz.nip39ExtIdentities.MastodonIdentity
import com.vitorpamplona.quartz.nip39ExtIdentities.TelegramIdentity
import com.vitorpamplona.quartz.nip39ExtIdentities.TwitterIdentity
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.IO
import kotlinx.coroutines.flow.flowOn
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.launch
import org.jetbrains.compose.resources.DrawableResource
import org.jetbrains.compose.resources.StringResource
private const val IDENTITY_ICON_CACHE_KEY = 0
@@ -151,7 +150,7 @@ fun DrawAdditionalInfo(
Spacer(StdHorzSpacer)
}
DrawPlayName(displayName)
SpeakNameButton(displayName)
}
// A Buzz agent says so, and whose it is: the owner from the NIP-OA tag on its kind 0,
@@ -184,7 +183,7 @@ fun DrawAdditionalInfo(
modifier = Modifier.size(23.dp).padding(start = 5.dp),
onClick = {
scope.launch {
clipboard.setClipEntry(ClipEntry(ClipData.newPlainText("npub", baseUser.pubkeyNpub())))
clipboard.setText(baseUser.pubkeyNpub())
}
},
) {
@@ -208,7 +207,7 @@ fun DrawAdditionalInfo(
modifier = Modifier.size(23.dp).padding(start = 5.dp),
onClick = {
scope.launch {
clipboard.setClipEntry(ClipEntry(ClipData.newPlainText("nprofile", baseUser.toNProfile())))
clipboard.setText(baseUser.toNProfile())
}
},
) {
@@ -271,7 +270,7 @@ fun DrawAdditionalInfo(
Row(verticalAlignment = Alignment.CenterVertically) {
Icon(
tint = Color.Unspecified,
painter = painterRes(resourceId = getIdentityClaimIcon(identity), IDENTITY_ICON_CACHE_KEY),
painter = painterRes(getIdentityClaimIcon(identity), IDENTITY_ICON_CACHE_KEY),
contentDescription = stringRes(getIdentityClaimDescription(identity)),
modifier = Modifier.size(18.dp),
)
@@ -341,7 +340,6 @@ fun DisplayLastSeen(
val lastSeen by lastSeenFlow.collectAsStateWithLifecycle(null)
lastSeen?.let { timestamp ->
val context = LocalContext.current
Text(
text = lastSeenSentence(timestamp),
color = MaterialTheme.colorScheme.placeholderText,
@@ -384,13 +382,13 @@ fun DisplayNip05ProfileStatus(
}
}
fun getIdentityClaimIcon(identity: IdentityClaimTag): Int =
fun getIdentityClaimIcon(identity: IdentityClaimTag): DrawableResource =
when (identity) {
is TwitterIdentity -> R.drawable.x
is TelegramIdentity -> R.drawable.telegram
is MastodonIdentity -> R.drawable.mastodon
is GitHubIdentity -> R.drawable.github
else -> R.drawable.github
is TwitterIdentity -> Res.drawable.x
is TelegramIdentity -> Res.drawable.telegram
is MastodonIdentity -> Res.drawable.mastodon
is GitHubIdentity -> Res.drawable.github
else -> Res.drawable.github
}
fun getIdentityClaimDescription(identity: IdentityClaimTag): StringResource =
@@ -18,7 +18,7 @@
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
package com.vitorpamplona.amethyst.ui.screen.loggedIn.profile.header
package com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.profile.header
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.Image
@@ -18,7 +18,7 @@
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
package com.vitorpamplona.amethyst.ui.screen.loggedIn.profile.header
package com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.profile.header
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.width
@@ -18,7 +18,7 @@
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
package com.vitorpamplona.amethyst.ui.screen.loggedIn.profile.header
package com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.profile.header
import androidx.compose.runtime.Composable
import androidx.compose.runtime.derivedStateOf
@@ -18,9 +18,8 @@
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
package com.vitorpamplona.amethyst.ui.screen.loggedIn.profile.header
package com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.profile.header
import android.content.ClipData
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
@@ -47,11 +46,9 @@ import androidx.compose.ui.draw.clip
import androidx.compose.ui.geometry.Rect
import androidx.compose.ui.layout.boundsInWindow
import androidx.compose.ui.layout.onGloballyPositioned
import androidx.compose.ui.platform.ClipEntry
import androidx.compose.ui.platform.LocalClipboard
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import androidx.lifecycle.viewmodel.compose.viewModel
import com.vitorpamplona.amethyst.commons.icons.symbols.Icon
import com.vitorpamplona.amethyst.commons.icons.symbols.MaterialSymbols
import com.vitorpamplona.amethyst.commons.model.User
@@ -62,16 +59,18 @@ import com.vitorpamplona.amethyst.commons.richtext.RichTextParser
import com.vitorpamplona.amethyst.commons.ui.actions.NewUserMetadataViewModel
import com.vitorpamplona.amethyst.commons.ui.actions.uploads.GallerySelectSingle
import com.vitorpamplona.amethyst.commons.ui.components.LoadingAnimation
import com.vitorpamplona.amethyst.commons.ui.components.rememberViewModel
import com.vitorpamplona.amethyst.commons.ui.components.util.setText
import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav
import com.vitorpamplona.amethyst.commons.ui.note.ClickableUserPicture
import com.vitorpamplona.amethyst.commons.ui.note.platform.ZoomableImageDialog
import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.profile.header.apps.UserAppRecommendationsFeedViewModel
import com.vitorpamplona.amethyst.commons.ui.stringRes
import com.vitorpamplona.amethyst.commons.ui.theme.Size100dp
import com.vitorpamplona.amethyst.commons.ui.theme.Size35dp
import com.vitorpamplona.amethyst.commons.ui.theme.userProfileBorderModifier
import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel
import com.vitorpamplona.amethyst.commons.viewmodels.UserExternalIdentitiesViewModel
import com.vitorpamplona.amethyst.ui.screen.loggedIn.profile.header.apps.UserAppRecommendationsFeedViewModel
import kotlinx.coroutines.launch
@Composable
@@ -148,8 +147,7 @@ fun ZoomableUserPicture(
onLongClick = {
baseUser.profilePicture()?.let { it1 ->
scope.launch {
val clipData = ClipData.newPlainText("profile picture url", it1)
clipboardManager.setClipEntry(ClipEntry(clipData))
clipboardManager.setText(it1)
}
}
},
@@ -191,7 +189,7 @@ private fun ProfilePictureUploadButton(
accountViewModel: AccountViewModel,
size: Dp,
) {
val postViewModel: NewUserMetadataViewModel = viewModel()
val postViewModel: NewUserMetadataViewModel = rememberViewModel { NewUserMetadataViewModel() }
postViewModel.init(accountViewModel)
var showGallerySelect by remember { mutableStateOf(false) }
@@ -18,9 +18,8 @@
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
package com.vitorpamplona.amethyst.ui.screen.loggedIn.profile.header
package com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.profile.header
import android.widget.Toast
import androidx.compose.foundation.BorderStroke
import androidx.compose.foundation.clickable
import androidx.compose.foundation.combinedClickable
@@ -44,7 +43,6 @@ import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalClipboard
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
@@ -68,17 +66,18 @@ import com.vitorpamplona.amethyst.commons.resources.copy_to_clipboard
import com.vitorpamplona.amethyst.commons.resources.send_payment_method_cashu
import com.vitorpamplona.amethyst.commons.resources.send_payment_method_lightning
import com.vitorpamplona.amethyst.commons.resources.send_payment_method_onchain
import com.vitorpamplona.amethyst.commons.ui.components.rememberShortNotice
import com.vitorpamplona.amethyst.commons.ui.components.util.setText
import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav
import com.vitorpamplona.amethyst.commons.ui.note.LoadAddressableNote
import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.profile.header.Bolt12OffersDialog
import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.profile.header.abbreviateBolt12Offer
import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.profile.payment.ProfilePaymentMethod
import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.profile.payment.rememberProfileClinkOffer
import com.vitorpamplona.amethyst.commons.ui.stringRes
import com.vitorpamplona.amethyst.commons.ui.theme.BitcoinOrange
import com.vitorpamplona.amethyst.commons.ui.theme.Size16Modifier
import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel
import com.vitorpamplona.amethyst.ui.screen.loggedIn.profile.payment.ProfilePaymentMethod
import com.vitorpamplona.amethyst.ui.screen.loggedIn.profile.payment.rememberProfileClinkOffer
import com.vitorpamplona.quartz.experimental.clink.pointers.NOffer
import com.vitorpamplona.quartz.nipA3PaymentTargets.PaymentTarget
import com.vitorpamplona.quartz.nipA3PaymentTargets.PaymentTargetsEvent
@@ -315,7 +314,7 @@ fun ProfilePaymentChip(
copyValue: String? = null,
icon: @Composable () -> Unit,
) {
val context = LocalContext.current
val shortNotice = rememberShortNotice()
val clipboard = LocalClipboard.current
val scope = rememberCoroutineScope()
val copyLabel = stringRes(Res.string.copy_to_clipboard)
@@ -328,7 +327,7 @@ fun ProfilePaymentChip(
onLongClick = {
scope.launch {
clipboard.setText(copyValue)
Toast.makeText(context, copiedMessage, Toast.LENGTH_SHORT).show()
shortNotice.show(copiedMessage)
}
},
onLongClickLabel = copyLabel,
@@ -18,7 +18,7 @@
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
package com.vitorpamplona.amethyst.ui.screen.loggedIn.profile.header
package com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.profile.header
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
@@ -18,7 +18,7 @@
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
package com.vitorpamplona.amethyst.ui.screen.loggedIn.profile.header
package com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.profile.header
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
@@ -18,15 +18,13 @@
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
package com.vitorpamplona.amethyst.ui.screen.loggedIn.profile.header
package com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.profile.header
import android.content.Intent
import androidx.compose.runtime.Composable
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.platform.LocalClipboard
import androidx.compose.ui.platform.LocalContext
import com.vitorpamplona.amethyst.commons.icons.symbols.MaterialSymbols
import com.vitorpamplona.amethyst.commons.model.User
import com.vitorpamplona.amethyst.commons.nip85TrustedAssertions.ui.EditNicknameDialog
@@ -47,6 +45,7 @@ import com.vitorpamplona.amethyst.commons.resources.unblock_user
import com.vitorpamplona.amethyst.commons.ui.components.M3ActionDialog
import com.vitorpamplona.amethyst.commons.ui.components.M3ActionRow
import com.vitorpamplona.amethyst.commons.ui.components.M3ActionSection
import com.vitorpamplona.amethyst.commons.ui.components.rememberTextSharer
import com.vitorpamplona.amethyst.commons.ui.components.util.setText
import com.vitorpamplona.amethyst.commons.ui.note.creators.emojiSuggestions.WatchAndLoadMyEmojiList
import com.vitorpamplona.amethyst.commons.ui.note.externalLinkForUser
@@ -85,7 +84,7 @@ fun UserProfileDropDownMenu(
) {
val clipboardManager = LocalClipboard.current
val scope = rememberCoroutineScope()
val context = LocalContext.current
val textSharer = rememberTextSharer()
// Share section
M3ActionSection {
@@ -102,18 +101,7 @@ fun UserProfileDropDownMenu(
icon = MaterialSymbols.Share,
text = stringRes(Res.string.quick_action_share),
) {
val sendIntent =
Intent().apply {
action = Intent.ACTION_SEND
type = "text/plain"
putExtra(Intent.EXTRA_TEXT, externalLinkForUser(user))
putExtra(
Intent.EXTRA_TITLE,
quickActionShareBrowserLinkStr,
)
}
val shareIntent = Intent.createChooser(sendIntent, quickActionShareStr)
context.startActivity(shareIntent)
textSharer.share(externalLinkForUser(user), quickActionShareBrowserLinkStr, quickActionShareStr)
onDismiss()
}
}
@@ -18,7 +18,7 @@
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
package com.vitorpamplona.amethyst.ui.screen.loggedIn.profile.header.apps
package com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.profile.header.apps
import androidx.compose.animation.core.tween
import androidx.compose.foundation.layout.Arrangement
@@ -18,13 +18,15 @@
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
package com.vitorpamplona.amethyst.ui.screen.loggedIn.profile.header.apps
package com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.profile.header.apps
import androidx.compose.runtime.Stable
import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.viewmodel.CreationExtras
import com.vitorpamplona.amethyst.commons.model.User
import com.vitorpamplona.amethyst.commons.ui.screen.AndroidFeedViewModel
import kotlin.reflect.KClass
@Stable
class UserAppRecommendationsFeedViewModel(
@@ -34,6 +36,9 @@ class UserAppRecommendationsFeedViewModel(
val user: User,
) : ViewModelProvider.Factory {
@Suppress("UNCHECKED_CAST")
override fun <T : ViewModel> create(modelClass: Class<T>): T = UserAppRecommendationsFeedViewModel(user) as T
override fun <T : ViewModel> create(
modelClass: KClass<T>,
extras: CreationExtras,
): T = UserAppRecommendationsFeedViewModel(user) as T
}
}
@@ -18,7 +18,7 @@
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
package com.vitorpamplona.amethyst.ui.screen.loggedIn.profile.header.apps
package com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.profile.header.apps
import com.vitorpamplona.amethyst.commons.feeds.AdditiveFeedFilter
import com.vitorpamplona.amethyst.commons.feeds.sortedByDefaultFeedOrder
@@ -18,7 +18,7 @@
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
package com.vitorpamplona.amethyst.ui.screen.loggedIn.profile.header.badges
package com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.profile.header.badges
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
@@ -90,6 +90,7 @@ import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.persistentListOf
import kotlinx.collections.immutable.toImmutableList
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.IO
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.flowOn
@@ -18,7 +18,7 @@
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
package com.vitorpamplona.amethyst.ui.screen.loggedIn.profile.mutual
package com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.profile.mutual
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxHeight
@@ -26,8 +26,8 @@ import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav
import com.vitorpamplona.amethyst.commons.ui.screen.RefresheableFeedView
import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.profile.mutual.dal.UserProfileMutualFeedViewModel
import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel
import com.vitorpamplona.amethyst.ui.screen.loggedIn.profile.mutual.dal.UserProfileMutualFeedViewModel
@Composable
fun TabMutualConversations(
@@ -18,7 +18,7 @@
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
package com.vitorpamplona.amethyst.ui.screen.loggedIn.profile.mutual.dal
package com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.profile.mutual.dal
import com.vitorpamplona.amethyst.commons.feeds.AdditiveFeedFilter
import com.vitorpamplona.amethyst.commons.feeds.isRenderableRepost
@@ -18,14 +18,16 @@
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
package com.vitorpamplona.amethyst.ui.screen.loggedIn.profile.mutual.dal
package com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.profile.mutual.dal
import androidx.compose.runtime.Stable
import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.viewmodel.CreationExtras
import com.vitorpamplona.amethyst.commons.model.Account
import com.vitorpamplona.amethyst.commons.model.User
import com.vitorpamplona.amethyst.commons.ui.screen.AndroidFeedViewModel
import kotlin.reflect.KClass
@Stable
class UserProfileMutualFeedViewModel(
@@ -37,6 +39,9 @@ class UserProfileMutualFeedViewModel(
val account: Account,
) : ViewModelProvider.Factory {
@Suppress("UNCHECKED_CAST")
override fun <T : ViewModel> create(modelClass: Class<T>): T = UserProfileMutualFeedViewModel(user, account) as T
override fun <T : ViewModel> create(
modelClass: KClass<T>,
extras: CreationExtras,
): T = UserProfileMutualFeedViewModel(user, account) as T
}
}
@@ -18,7 +18,7 @@
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
package com.vitorpamplona.amethyst.ui.screen.loggedIn.profile.newthreads
package com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.profile.newthreads
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.layout.Column
@@ -45,11 +45,11 @@ import com.vitorpamplona.amethyst.commons.ui.feeds.StickToTopOnPrepend
import com.vitorpamplona.amethyst.commons.ui.feeds.WatchScrollToTop
import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav
import com.vitorpamplona.amethyst.commons.ui.note.NoteCompose
import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.profile.newthreads.dal.UserProfileNewThreadsFeedViewModel
import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.profile.pinnedNotes.dal.UserProfilePinnedNotesFeedViewModel
import com.vitorpamplona.amethyst.commons.ui.theme.DividerThickness
import com.vitorpamplona.amethyst.commons.ui.theme.FeedPadding
import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel
import com.vitorpamplona.amethyst.ui.screen.loggedIn.profile.newthreads.dal.UserProfileNewThreadsFeedViewModel
import com.vitorpamplona.amethyst.ui.screen.loggedIn.profile.pinnedNotes.dal.UserProfilePinnedNotesFeedViewModel
@Composable
fun TabNotesNewThreads(
@@ -18,7 +18,7 @@
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
package com.vitorpamplona.amethyst.ui.screen.loggedIn.profile.newthreads.dal
package com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.profile.newthreads.dal
import com.vitorpamplona.amethyst.commons.feeds.AdditiveFeedFilter
import com.vitorpamplona.amethyst.commons.feeds.isRenderableRepost
@@ -18,14 +18,16 @@
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
package com.vitorpamplona.amethyst.ui.screen.loggedIn.profile.newthreads.dal
package com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.profile.newthreads.dal
import androidx.compose.runtime.Stable
import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.viewmodel.CreationExtras
import com.vitorpamplona.amethyst.commons.model.Account
import com.vitorpamplona.amethyst.commons.model.User
import com.vitorpamplona.amethyst.commons.ui.screen.AndroidFeedViewModel
import kotlin.reflect.KClass
@Stable
class UserProfileNewThreadsFeedViewModel(
@@ -37,6 +39,9 @@ class UserProfileNewThreadsFeedViewModel(
val account: Account,
) : ViewModelProvider.Factory {
@Suppress("UNCHECKED_CAST")
override fun <T : ViewModel> create(modelClass: Class<T>): T = UserProfileNewThreadsFeedViewModel(user, account) as T
override fun <T : ViewModel> create(
modelClass: KClass<T>,
extras: CreationExtras,
): T = UserProfileNewThreadsFeedViewModel(user, account) as T
}
}
@@ -18,9 +18,8 @@
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
package com.vitorpamplona.amethyst.ui.screen.loggedIn.profile.payment
package com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.profile.payment
import android.util.LruCache
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
@@ -32,7 +31,9 @@ import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel
import com.vitorpamplona.quartz.experimental.clink.pointers.ClinkPointerParser
import com.vitorpamplona.quartz.experimental.clink.pointers.NOffer
import com.vitorpamplona.quartz.nip05DnsIdentifiers.Nip05Id
import com.vitorpamplona.quartz.utils.cache.ConcurrentLruCache
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.IO
import kotlinx.coroutines.withContext
/**
@@ -41,13 +42,13 @@ import kotlinx.coroutines.withContext
* profile visit (and every relay-pushed kind-0 refresh while a profile is open) would
* re-fetch the domain's nostr.json. Caches "no offer" results too so profiles without one
* aren't re-hit. A [ResolvedClinkOffer] wrapper holds the nullable parsed pointer
* (LruCache can't store nulls); absence means "not fetched yet".
* (the cache can't store nulls); absence means "not fetched yet".
*/
private class ResolvedClinkOffer(
val noffer: NOffer?,
)
private val clinkOfferNip05Cache = LruCache<String, ResolvedClinkOffer>(256)
private val clinkOfferNip05Cache = ConcurrentLruCache<String, ResolvedClinkOffer>(256)
/**
* Resolves a profile's advertised CLINK Offer, preferring the kind-0 `clink_offer`
@@ -18,9 +18,8 @@
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
package com.vitorpamplona.amethyst.ui.screen.loggedIn.profile.payment
package com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.profile.payment
import android.widget.Toast
import androidx.compose.foundation.BorderStroke
import androidx.compose.foundation.combinedClickable
import androidx.compose.foundation.layout.Arrangement
@@ -59,7 +58,6 @@ import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalClipboard
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.input.KeyboardCapitalization
import androidx.compose.ui.text.input.KeyboardType
@@ -91,6 +89,7 @@ import com.vitorpamplona.amethyst.commons.resources.send_payment_method_onchain
import com.vitorpamplona.amethyst.commons.resources.send_payment_no_methods
import com.vitorpamplona.amethyst.commons.resources.send_payment_receipt_section
import com.vitorpamplona.amethyst.commons.resources.send_payment_try_again
import com.vitorpamplona.amethyst.commons.ui.components.rememberShortNotice
import com.vitorpamplona.amethyst.commons.ui.components.util.setText
import com.vitorpamplona.amethyst.commons.ui.insets.imePaddingSafe
import com.vitorpamplona.amethyst.commons.ui.stringRes
@@ -100,6 +99,7 @@ import com.vitorpamplona.amethyst.commons.ui.theme.ThemeComparisonColumn
import com.vitorpamplona.amethyst.commons.ui.theme.placeholderText
import com.vitorpamplona.amethyst.commons.util.showAmount
import com.vitorpamplona.quartz.nip57Zaps.ZapReceiptEvent
import com.vitorpamplona.quartz.utils.BigDecimal
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.persistentListOf
import kotlinx.collections.immutable.toImmutableList
@@ -469,7 +469,7 @@ private fun MethodChip(
enabled: Boolean,
onSelect: () -> Unit,
) {
val context = LocalContext.current
val shortNotice = rememberShortNotice()
val clipboard = LocalClipboard.current
val scope = rememberCoroutineScope()
val copyLabel = stringRes(Res.string.copy_to_clipboard)
@@ -493,7 +493,7 @@ private fun MethodChip(
{
scope.launch {
clipboard.setText(value)
Toast.makeText(context, copiedMessage, Toast.LENGTH_SHORT).show()
shortNotice.show(copiedMessage)
}
}
},
@@ -596,7 +596,7 @@ private fun AmountSection(
SuggestionChip(
enabled = enabled,
onClick = { onAmountChange(amount.toString()) },
label = { Text("⚡ ${showAmount(amount.toBigDecimal())}") },
label = { Text("⚡ ${showAmount(BigDecimal(amount))}") },
)
}
}
@@ -18,7 +18,7 @@
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
package com.vitorpamplona.amethyst.ui.screen.loggedIn.profile.payment
package com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.profile.payment
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
@@ -40,7 +40,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.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import androidx.lifecycle.compose.collectAsStateWithLifecycle
@@ -96,6 +95,7 @@ import com.vitorpamplona.amethyst.commons.resources.zap_type_private
import com.vitorpamplona.amethyst.commons.resources.zap_type_private_explainer
import com.vitorpamplona.amethyst.commons.resources.zap_type_public
import com.vitorpamplona.amethyst.commons.resources.zap_type_public_explainer
import com.vitorpamplona.amethyst.commons.service.ClinkOfferPayer
import com.vitorpamplona.amethyst.commons.service.nwc.nwcFailureDetail
import com.vitorpamplona.amethyst.commons.service.nwc.nwcTimeoutMessage
import com.vitorpamplona.amethyst.commons.ui.loadStringRes
@@ -108,10 +108,11 @@ import com.vitorpamplona.amethyst.commons.ui.note.UsernameDisplay
import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.wallet.FeeTier
import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.wallet.rateFor
import com.vitorpamplona.amethyst.commons.ui.stringRes
import com.vitorpamplona.amethyst.commons.ui.wallet.payInvoice
import com.vitorpamplona.amethyst.commons.ui.wallet.rememberWalletAppLauncher
import com.vitorpamplona.amethyst.commons.util.DecimalPatternFormatter
import com.vitorpamplona.amethyst.commons.util.showAmount
import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel
import com.vitorpamplona.amethyst.service.ClinkOfferPayer
import com.vitorpamplona.amethyst.ui.note.payViaIntent
import com.vitorpamplona.quartz.experimental.clink.common.SatRange
import com.vitorpamplona.quartz.experimental.clink.offers.OfferErrorCode
import com.vitorpamplona.quartz.experimental.clink.pointers.ClinkPointerParser
@@ -121,10 +122,12 @@ import com.vitorpamplona.quartz.nip57Zaps.ZapReceiptEvent
import com.vitorpamplona.quartz.nipBCOnchainZaps.chain.FeeEstimates
import com.vitorpamplona.quartz.nipBCOnchainZaps.taproot.SegwitAddress
import com.vitorpamplona.quartz.nipBCOnchainZaps.taproot.TaprootAddress
import com.vitorpamplona.quartz.utils.BigDecimal
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.persistentListOf
import kotlinx.collections.immutable.toImmutableList
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.IO
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
@@ -180,7 +183,7 @@ private fun SendPaymentLoaded(
nav: INav,
modifier: Modifier = Modifier,
) {
val context = LocalContext.current
val walletLauncher = rememberWalletAppLauncher()
val scope = rememberCoroutineScope()
val userInfo by observeUserInfo(user, accountViewModel)
@@ -368,7 +371,7 @@ private fun SendPaymentLoaded(
selectedMethod == ProfilePaymentMethod.CASHU ->
stringRes(
Res.string.send_payment_cashu_balance,
showAmount((cashuFunding?.bestSingleMintSats ?: 0L).toBigDecimal()),
showAmount(BigDecimal(cashuFunding?.bestSingleMintSats ?: 0L)),
)
selectedMethod == ProfilePaymentMethod.CLINK && clinkRange?.min != null && clinkRange?.max != null ->
stringRes(Res.string.clink_offer_amount_range, clinkRange?.min.toString(), clinkRange?.max.toString())
@@ -457,9 +460,8 @@ private fun SendPaymentLoaded(
// startActivity belongs on Main. sendSats now delivers onNewInvoice
// there already; the hop also covers any other caller of payBolt11.
scope.launch {
payViaIntent(
walletLauncher.payInvoice(
invoice,
context,
noWalletFoundStr,
onPaid = { scope.launch { postStage(PaymentFlowStage.Success(successTitle, sentToWalletLabel)) } },
onError = { scope.launch { postStage(PaymentFlowStage.Failure(it)) } },
@@ -664,7 +666,7 @@ private fun SendPaymentLoaded(
canSend = canSend,
sendLabel =
if (amountSats != null && amountSats > 0) {
stringRes(Res.string.send_payment_pay_button, showAmount(amountSats.toBigDecimal()))
stringRes(Res.string.send_payment_pay_button, showAmount(BigDecimal(amountSats)))
} else {
stringRes(Res.string.send_payment_pay_button_empty)
},
@@ -800,7 +802,7 @@ private fun OnchainFeeSection(
stringRes(
Res.string.onchain_send_fee_tier_label_rate_eta,
stringRes(tier.labelRes),
"%.1f".format(rate),
DecimalPatternFormatter("0.0").format(rate),
stringRes(tier.etaLabelRes),
)
} else {
@@ -18,7 +18,7 @@
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
package com.vitorpamplona.amethyst.ui.screen.loggedIn.profile.pinnedNotes.dal
package com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.profile.pinnedNotes.dal
import com.vitorpamplona.amethyst.commons.feeds.FeedFilter
import com.vitorpamplona.amethyst.commons.model.Account
@@ -18,14 +18,16 @@
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
package com.vitorpamplona.amethyst.ui.screen.loggedIn.profile.pinnedNotes.dal
package com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.profile.pinnedNotes.dal
import androidx.compose.runtime.Stable
import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.viewmodel.CreationExtras
import com.vitorpamplona.amethyst.commons.model.Account
import com.vitorpamplona.amethyst.commons.model.User
import com.vitorpamplona.amethyst.commons.ui.screen.AndroidFeedViewModel
import kotlin.reflect.KClass
@Stable
class UserProfilePinnedNotesFeedViewModel(
@@ -37,6 +39,9 @@ class UserProfilePinnedNotesFeedViewModel(
val account: Account,
) : ViewModelProvider.Factory {
@Suppress("UNCHECKED_CAST")
override fun <T : ViewModel> create(modelClass: Class<T>): T = UserProfilePinnedNotesFeedViewModel(user, account) as T
override fun <T : ViewModel> create(
modelClass: KClass<T>,
extras: CreationExtras,
): T = UserProfilePinnedNotesFeedViewModel(user, account) as T
}
}
@@ -18,7 +18,7 @@
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
package com.vitorpamplona.amethyst.ui.screen.loggedIn.profile.relays
package com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.profile.relays
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.padding
@@ -18,7 +18,7 @@
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
package com.vitorpamplona.amethyst.ui.screen.loggedIn.profile.relays
package com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.profile.relays
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxHeight
@@ -28,8 +28,8 @@ import androidx.compose.ui.Modifier
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.LifecycleEventObserver
import androidx.lifecycle.compose.LocalLifecycleOwner
import androidx.lifecycle.viewmodel.compose.viewModel
import com.vitorpamplona.amethyst.commons.model.User
import com.vitorpamplona.amethyst.commons.ui.components.rememberViewModel
import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav
import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel
import com.vitorpamplona.amethyst.commons.viewmodels.RelayFeedViewModel
@@ -40,7 +40,7 @@ fun TabRelays(
accountViewModel: AccountViewModel,
nav: INav,
) {
val feedViewModel: RelayFeedViewModel = viewModel()
val feedViewModel: RelayFeedViewModel = rememberViewModel { RelayFeedViewModel() }
val lifeCycleOwner = LocalLifecycleOwner.current
@@ -18,7 +18,7 @@
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
package com.vitorpamplona.amethyst.ui.screen.loggedIn.profile.reports
package com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.profile.reports
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
@@ -28,9 +28,9 @@ import com.vitorpamplona.amethyst.commons.model.User
import com.vitorpamplona.amethyst.commons.resources.Res
import com.vitorpamplona.amethyst.commons.resources.number_reports
import com.vitorpamplona.amethyst.commons.resources.reports
import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.profile.reports.dal.UserProfileReportFeedViewModel
import com.vitorpamplona.amethyst.commons.ui.stringRes
import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel
import com.vitorpamplona.amethyst.ui.screen.loggedIn.profile.reports.dal.UserProfileReportFeedViewModel
@Composable
fun ReportsTabHeader(
@@ -18,7 +18,7 @@
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
package com.vitorpamplona.amethyst.ui.screen.loggedIn.profile.reports
package com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.profile.reports
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
@@ -36,10 +36,10 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.vitorpamplona.amethyst.commons.model.User
import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav
import com.vitorpamplona.amethyst.commons.ui.note.NoteCompose
import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.profile.reports.dal.UserProfileReportFeedViewModel
import com.vitorpamplona.amethyst.commons.ui.theme.DividerThickness
import com.vitorpamplona.amethyst.commons.ui.theme.FeedPadding
import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel
import com.vitorpamplona.amethyst.ui.screen.loggedIn.profile.reports.dal.UserProfileReportFeedViewModel
@Composable
fun TabReports(
@@ -18,22 +18,25 @@
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
package com.vitorpamplona.amethyst.ui.screen.loggedIn.profile.reports.dal
package com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.profile.reports.dal
import androidx.compose.runtime.Stable
import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.viewModelScope
import androidx.lifecycle.viewmodel.CreationExtras
import com.vitorpamplona.amethyst.commons.model.Account
import com.vitorpamplona.amethyst.commons.model.Note
import com.vitorpamplona.amethyst.commons.model.User
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.IO
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.flowOn
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.sample
import kotlinx.coroutines.flow.stateIn
import kotlin.reflect.KClass
@Stable
class UserProfileReportFeedViewModel(
@@ -76,6 +79,9 @@ class UserProfileReportFeedViewModel(
val account: Account,
) : ViewModelProvider.Factory {
@Suppress("UNCHECKED_CAST")
override fun <T : ViewModel> create(modelClass: Class<T>): T = UserProfileReportFeedViewModel(user, account) as T
override fun <T : ViewModel> create(
modelClass: KClass<T>,
extras: CreationExtras,
): T = UserProfileReportFeedViewModel(user, account) as T
}
}
@@ -18,7 +18,7 @@
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
package com.vitorpamplona.amethyst.ui.screen.loggedIn.profile.zaps
package com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.profile.zaps
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxHeight
@@ -32,10 +32,10 @@ import androidx.compose.ui.Modifier
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.vitorpamplona.amethyst.commons.model.User
import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav
import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.profile.zaps.dal.UserProfileZapsViewModel
import com.vitorpamplona.amethyst.commons.ui.theme.DividerThickness
import com.vitorpamplona.amethyst.commons.ui.theme.FeedPadding
import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel
import com.vitorpamplona.amethyst.ui.screen.loggedIn.profile.zaps.dal.UserProfileZapsViewModel
@Composable
fun TabReceivedZaps(
@@ -18,7 +18,7 @@
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
package com.vitorpamplona.amethyst.ui.screen.loggedIn.profile.zaps
package com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.profile.zaps
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
@@ -42,13 +42,13 @@ import com.vitorpamplona.amethyst.commons.ui.navigation.routes.routeFor
import com.vitorpamplona.amethyst.commons.ui.note.UserActionOptions
import com.vitorpamplona.amethyst.commons.ui.note.UserPicture
import com.vitorpamplona.amethyst.commons.ui.note.UsernameDisplay
import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.profile.zaps.dal.ZapAmount
import com.vitorpamplona.amethyst.commons.ui.theme.BitcoinOrange
import com.vitorpamplona.amethyst.commons.ui.theme.Size55dp
import com.vitorpamplona.amethyst.commons.ui.theme.ThemeComparisonColumn
import com.vitorpamplona.amethyst.commons.util.showAmountInteger
import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel
import com.vitorpamplona.amethyst.commons.viewmodels.mockAccountViewModel
import com.vitorpamplona.amethyst.ui.screen.loggedIn.profile.zaps.dal.ZapAmount
import com.vitorpamplona.quartz.utils.BigDecimal
@Composable
@@ -77,7 +77,7 @@ fun RenderZapNoteSlimPreview() {
ThemeComparisonColumn {
RenderZapNoteSlim(
ZapAmount(user1, BigDecimal.TEN),
ZapAmount(user1, BigDecimal(10)),
accountViewModel,
EmptyNav(),
)
@@ -18,7 +18,7 @@
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
package com.vitorpamplona.amethyst.ui.screen.loggedIn.profile.zaps
package com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.profile.zaps
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
@@ -26,10 +26,10 @@ import androidx.compose.runtime.getValue
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.vitorpamplona.amethyst.commons.resources.Res
import com.vitorpamplona.amethyst.commons.resources.zaps
import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.profile.zaps.dal.UserProfileZapsViewModel
import com.vitorpamplona.amethyst.commons.ui.stringRes
import com.vitorpamplona.amethyst.commons.util.showAmountInteger
import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel
import com.vitorpamplona.amethyst.ui.screen.loggedIn.profile.zaps.dal.UserProfileZapsViewModel
@Composable
fun ZapTabHeader(
@@ -18,13 +18,14 @@
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
package com.vitorpamplona.amethyst.ui.screen.loggedIn.profile.zaps.dal
package com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.profile.zaps.dal
import androidx.compose.runtime.Immutable
import androidx.compose.runtime.Stable
import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.viewModelScope
import androidx.lifecycle.viewmodel.CreationExtras
import com.vitorpamplona.amethyst.commons.model.Account
import com.vitorpamplona.amethyst.commons.model.User
import com.vitorpamplona.amethyst.commons.model.cache.LocalCache
@@ -34,13 +35,16 @@ import com.vitorpamplona.quartz.nip57Zaps.ZapReceiptEvent
import com.vitorpamplona.quartz.nipB1Bolt12Zaps.zap.Bolt12ZapEvent
import com.vitorpamplona.quartz.nipBCOnchainZaps.zap.OnchainZapEvent
import com.vitorpamplona.quartz.utils.BigDecimal
import com.vitorpamplona.quartz.utils.plus
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.IO
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.flowOn
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.sample
import kotlinx.coroutines.flow.stateIn
import kotlin.reflect.KClass
@Immutable
data class ZapAmount(
@@ -59,14 +63,15 @@ class UserProfileZapsViewModel(
tags = mapOf("p" to listOf(user.pubkeyHex)),
)
// Largest amount first. The shared BigDecimal is not Comparable, so the order comes from the sign of the difference.
val sortingModel: Comparator<ZapAmount> =
compareByDescending<ZapAmount> { it.amount }.thenBy { it.user.pubkeyHex }
Comparator<ZapAmount> { a, b -> b.amount.subtract(a.amount).signum() }.thenBy { it.user.pubkeyHex }
suspend fun mapRequest(zapEvent: ZapReceiptEvent): ZapAmount? {
val zapRequest =
zapEvent.zapRequest ?: return ZapAmount(
LocalCache.getOrCreateUser(zapEvent.pubKey),
zapEvent.amount ?: BigDecimal.ZERO,
zapEvent.amount ?: BigDecimal(0),
)
return if (zapRequest.isPrivateZap()) {
@@ -76,24 +81,24 @@ class UserProfileZapsViewModel(
if (cachedPrivateRequest != null) {
ZapAmount(
LocalCache.getOrCreateUser(cachedPrivateRequest.pubKey),
zapEvent.amount ?: BigDecimal.ZERO,
zapEvent.amount ?: BigDecimal(0),
)
} else {
ZapAmount(
LocalCache.getOrCreateUser(zapRequest.pubKey),
zapEvent.amount ?: BigDecimal.ZERO,
zapEvent.amount ?: BigDecimal(0),
)
}
} else {
ZapAmount(
LocalCache.getOrCreateUser(zapRequest.pubKey),
zapEvent.amount ?: BigDecimal.ZERO,
zapEvent.amount ?: BigDecimal(0),
)
}
} else {
ZapAmount(
LocalCache.getOrCreateUser(zapRequest.pubKey),
zapEvent.amount ?: BigDecimal.ZERO,
zapEvent.amount ?: BigDecimal(0),
)
}
}
@@ -128,7 +133,7 @@ class UserProfileZapsViewModel(
else -> null
}
if (zapAmount != null) {
val existingAmount = results[zapAmount.user] ?: BigDecimal.ZERO
val existingAmount = results[zapAmount.user] ?: BigDecimal(0)
results[zapAmount.user] = existingAmount + zapAmount.amount
}
}
@@ -152,11 +157,11 @@ class UserProfileZapsViewModel(
val totalReceivedZaps =
receivedZapAmountsByUser
.map { it.sumOf { it.amount } }
.map { amounts -> amounts.fold(BigDecimal(0)) { total, zap -> total + zap.amount } }
.flowOn(Dispatchers.IO)
.stateIn(
viewModelScope,
initialValue = BigDecimal.ZERO,
initialValue = BigDecimal(0),
started = SharingStarted.Lazily,
)
@@ -165,6 +170,9 @@ class UserProfileZapsViewModel(
val account: Account,
) : ViewModelProvider.Factory {
@Suppress("UNCHECKED_CAST")
override fun <T : ViewModel> create(modelClass: Class<T>): T = UserProfileZapsViewModel(user, account) as T
override fun <T : ViewModel> create(
modelClass: KClass<T>,
extras: CreationExtras,
): T = UserProfileZapsViewModel(user, account) as T
}
}
@@ -18,9 +18,8 @@
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
package com.vitorpamplona.amethyst.ui.screen.loggedIn.settings
package com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.settings
import android.content.Context
import androidx.compose.foundation.background
import androidx.compose.foundation.border
import androidx.compose.foundation.clickable
@@ -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.semantics.contentDescription
import androidx.compose.ui.semantics.semantics
import androidx.compose.ui.text.font.FontFamily
@@ -62,8 +60,6 @@ import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.core.os.LocaleListCompat
import com.vitorpamplona.amethyst.R
import com.vitorpamplona.amethyst.commons.icons.symbols.Icon
import com.vitorpamplona.amethyst.commons.icons.symbols.MaterialSymbol
import com.vitorpamplona.amethyst.commons.icons.symbols.MaterialSymbols
@@ -120,6 +116,7 @@ import com.vitorpamplona.amethyst.commons.ui.components.TitleExplainer
import com.vitorpamplona.amethyst.commons.ui.navigation.navs.EmptyNav
import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav
import com.vitorpamplona.amethyst.commons.ui.navigation.topbars.TopBarWithBackButton
import com.vitorpamplona.amethyst.commons.ui.platform.LocalAppPlatform
import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.settings.SegmentedChoiceTile
import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.settings.SettingsBlockTile
import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.settings.SettingsControlRow
@@ -134,12 +131,8 @@ import com.vitorpamplona.amethyst.commons.ui.theme.toFontFamily
import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel
import kotlinx.collections.immutable.ImmutableMap
import kotlinx.collections.immutable.toImmutableList
import kotlinx.collections.immutable.toImmutableMap
import kotlinx.coroutines.flow.MutableStateFlow
import org.jetbrains.compose.resources.StringResource
import org.xmlpull.v1.XmlPullParser
import org.xmlpull.v1.XmlPullParserException
import java.io.IOException
@Composable
fun SettingsScreen(
@@ -436,9 +429,7 @@ private fun AccentColorTile(sharedPrefs: UiSettingsFlow) {
// matches the app's other "opens a picker" rows instead of leaving a lone text-field dropdown here.
@Composable
private fun LanguageTile(sharedPrefs: UiSettingsFlow) {
val context = LocalContext.current
val languageEntries = remember { context.getLangPreferenceDropdownEntries() }
val languageEntries = LocalAppPlatform.current.rememberAppLanguages()
val languageList = remember { languageEntries.keys.map { TitleExplainer(it) }.toImmutableList() }
val language by sharedPrefs.preferredLanguage.collectAsState()
@@ -482,40 +473,6 @@ private fun LanguageTile(sharedPrefs: UiSettingsFlow) {
}
}
fun Context.getLocaleListFromXml(): LocaleListCompat {
val tagsList = mutableListOf<CharSequence>()
try {
val xpp: XmlPullParser = resources.getXml(R.xml.locales_config)
while (xpp.eventType != XmlPullParser.END_DOCUMENT) {
if (xpp.eventType == XmlPullParser.START_TAG && xpp.name == "locale") {
tagsList.add(xpp.getAttributeValue(0))
}
xpp.next()
}
} catch (e: XmlPullParserException) {
e.printStackTrace()
} catch (e: IOException) {
e.printStackTrace()
}
return LocaleListCompat.forLanguageTags(tagsList.joinToString(","))
}
fun Context.getLangPreferenceDropdownEntries(): ImmutableMap<String, String> {
val localeList = getLocaleListFromXml()
val map = mutableMapOf<String, String>()
for (a in 0 until localeList.size()) {
localeList[a].let {
map.put(
it!!.getDisplayName(it).replaceFirstChar { char -> char.uppercase() },
it.toLanguageTag(),
)
}
}
return map.toImmutableMap()
}
fun getLanguageIndex(
languageEntries: ImmutableMap<String, String>,
language: String?,
@@ -18,7 +18,7 @@
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
package com.vitorpamplona.amethyst.ui.screen.loggedIn.settings
package com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.settings
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
@@ -33,34 +33,28 @@ import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.vitorpamplona.amethyst.Amethyst
import com.vitorpamplona.amethyst.commons.model.preferences.NamecoinSettingsStore
import com.vitorpamplona.amethyst.commons.resources.Res
import com.vitorpamplona.amethyst.commons.resources.namecoin_settings
import com.vitorpamplona.amethyst.commons.service.namecoin.NamecoinClients
import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav
import com.vitorpamplona.amethyst.commons.ui.navigation.topbars.TopBarWithBackButton
import com.vitorpamplona.amethyst.commons.ui.platform.LocalAppServices
import com.vitorpamplona.amethyst.commons.ui.stringRes
import com.vitorpamplona.quartz.nip05DnsIdentifiers.namecoin.ElectrumXClient
import com.vitorpamplona.quartz.nip05DnsIdentifiers.namecoin.NamecoinCoreRpcClient
import kotlinx.coroutines.launch
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun NamecoinSettingsScreen(nav: INav) {
NamecoinSettingsScreen(
Amethyst.instance.namecoinPrefs,
electrumXClient = { Amethyst.instance.electrumXClient },
namecoinCoreRpcClient = { Amethyst.instance.namecoinCoreRpcClient },
nav,
)
val services = LocalAppServices.current
NamecoinSettingsScreen(services.namecoinSettings, services.namecoinClients, nav)
}
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun NamecoinSettingsScreen(
namecoinPrefs: NamecoinSettingsStore,
electrumXClient: () -> ElectrumXClient,
namecoinCoreRpcClient: () -> NamecoinCoreRpcClient,
clients: NamecoinClients,
nav: INav,
) {
val namecoinSettings by namecoinPrefs.settings.collectAsStateWithLifecycle()
@@ -92,14 +86,13 @@ fun NamecoinSettingsScreen(
onReset = {
scope.launch { namecoinPrefs.reset() }
},
onTestServer = { server -> electrumXClient().testServer(server) },
onTestServer = { server -> clients.testElectrumxServer(server) },
onPinCert = { pem ->
scope.launch {
namecoinPrefs.addPinnedCert(pem)
// Share the pinned PEM across both backends so the
// user only has to confirm a self-signed cert once.
electrumXClient().addPinnedCert(pem)
namecoinCoreRpcClient().addPinnedCert(pem)
clients.addPinnedCert(pem)
}
},
onSetBackend = { backend ->
@@ -108,7 +101,7 @@ fun NamecoinSettingsScreen(
onSetCoreRpcConfig = { cfg ->
scope.launch {
namecoinPrefs.setCoreRpcConfig(cfg)
namecoinCoreRpcClient().setConfig(cfg)
clients.setCoreRpcConfig(cfg)
}
},
onSetFallbackToCustomElectrumx = { enabled ->
@@ -117,7 +110,7 @@ fun NamecoinSettingsScreen(
onSetFallbackToDefaultElectrumx = { enabled ->
scope.launch { namecoinPrefs.setFallbackToDefaultElectrumx(enabled) }
},
onTestCoreRpc = { cfg -> namecoinCoreRpcClient().probe(cfg) },
onTestCoreRpc = { cfg -> clients.probeCoreRpc(cfg) },
)
}
}
@@ -18,9 +18,8 @@
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
package com.vitorpamplona.amethyst.ui.screen.loggedIn.settings
package com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.settings
import android.os.Build
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.expandVertically
import androidx.compose.animation.shrinkVertically
@@ -60,7 +59,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.platform.LocalSoftwareKeyboardController
import androidx.compose.ui.text.font.FontFamily
import androidx.compose.ui.text.font.FontWeight
@@ -68,6 +66,7 @@ import androidx.compose.ui.text.input.ImeAction
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import com.vitorpamplona.amethyst.commons.browser.OmniboxInput
import com.vitorpamplona.amethyst.commons.icons.symbols.Icon
import com.vitorpamplona.amethyst.commons.icons.symbols.MaterialSymbols
import com.vitorpamplona.amethyst.commons.model.nip05DnsIdentifiers.namecoin.NamecoinSettings
@@ -89,14 +88,16 @@ import com.vitorpamplona.amethyst.commons.resources.namecoin_tls_info
import com.vitorpamplona.amethyst.commons.ui.note.DateTimeStyle
import com.vitorpamplona.amethyst.commons.ui.note.formatDateTime
import com.vitorpamplona.amethyst.commons.ui.stringRes
import com.vitorpamplona.amethyst.commons.util.deviceDescription
import com.vitorpamplona.quartz.nip05DnsIdentifiers.namecoin.DEFAULT_ELECTRUMX_SERVERS
import com.vitorpamplona.quartz.nip05DnsIdentifiers.namecoin.ElectrumxServer
import com.vitorpamplona.quartz.nip05DnsIdentifiers.namecoin.NamecoinBackend
import com.vitorpamplona.quartz.nip05DnsIdentifiers.namecoin.NamecoinCoreRpcConfig
import com.vitorpamplona.quartz.nip05DnsIdentifiers.namecoin.RpcProbeResult
import com.vitorpamplona.quartz.nip05DnsIdentifiers.namecoin.ServerTestResult
import com.vitorpamplona.quartz.utils.currentTimeMillis
import kotlinx.coroutines.launch
import java.util.Locale
import kotlin.math.roundToLong
/**
* Complete settings section for Namecoin ElectrumX server configuration.
@@ -349,7 +350,7 @@ private fun TestConnectionSection(
)
}
}
lastTestTimestamp = System.currentTimeMillis()
lastTestTimestamp = currentTimeMillis()
isTesting = false
// Show confirmation dialog for each new cert
if (newCerts.isNotEmpty()) {
@@ -512,9 +513,8 @@ private fun DiagnosticCard(
// Last test timestamp
if (lastTestTimestamp != null) {
val context = LocalContext.current
val formatted =
remember(lastTestTimestamp, context) {
remember(lastTestTimestamp) {
formatDateTime(lastTestTimestamp, DateTimeStyle.MEDIUM, DateTimeStyle.SHORT)
}
val successCount = testResults.count { it.success }
@@ -535,7 +535,7 @@ private fun DiagnosticCard(
// Device info
DiagnosticRow(
label = stringRes(Res.string.namecoin_device_info),
value = "${Build.MANUFACTURER} ${Build.MODEL}, Android ${Build.VERSION.RELEASE} (API ${Build.VERSION.SDK_INT})",
value = deviceDescription(),
)
Spacer(Modifier.height(4.dp))
@@ -1108,13 +1108,7 @@ private fun NamecoinCoreRpcSection(
!candidate.usePinnedTrustStore
)
) {
val host =
try {
java.net.URI(candidate.url).host
?: candidate.url
} catch (_: Exception) {
candidate.url
}
val host = OmniboxInput.hostOf(candidate.url) ?: candidate.url
pendingPin =
PendingCertPin(
serverHost = host,
@@ -1167,7 +1161,7 @@ private fun NamecoinCoreRpcSection(
val chain = probe.chain ?: "?"
val blocks = probe.blocks?.toString() ?: "?"
val pct =
probe.verificationProgress?.let { String.format(Locale.ROOT, "%.2f%%", it * 100) }
probe.verificationProgress?.let { percentTwoDecimals(it) }
?: "?"
Text(
"chain=$chain height=$blocks sync=$pct" +
@@ -1316,3 +1310,9 @@ private fun ToggleRow(
)
}
}
/** [fraction] as a percentage with exactly two decimals and a `.` mark ("99.50%"), as Core RPC reports it. */
private fun percentTwoDecimals(fraction: Double): String {
val hundredths = (fraction * 10_000).roundToLong()
return "${hundredths / 100}.${(hundredths % 100).toString().padStart(2, '0')}%"
}
@@ -18,7 +18,7 @@
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
package com.vitorpamplona.amethyst.ui.screen.loggedIn.settings
package com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.settings
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
@@ -33,7 +33,6 @@ import androidx.compose.runtime.getValue
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import com.vitorpamplona.amethyst.Amethyst
import com.vitorpamplona.amethyst.commons.model.preferences.OtsSettingsStore
import com.vitorpamplona.amethyst.commons.resources.Res
import com.vitorpamplona.amethyst.commons.resources.ots_explorer_settings
@@ -41,13 +40,14 @@ import com.vitorpamplona.amethyst.commons.tor.TorSettingsFlow
import com.vitorpamplona.amethyst.commons.tor.TorType
import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav
import com.vitorpamplona.amethyst.commons.ui.navigation.topbars.TopBarWithBackButton
import com.vitorpamplona.amethyst.commons.ui.platform.LocalAppServices
import com.vitorpamplona.amethyst.commons.ui.stringRes
import kotlinx.coroutines.launch
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun OtsSettingsScreen(nav: INav) {
OtsSettingsScreen(Amethyst.instance.otsPrefs, Amethyst.instance.torPrefs.value, nav)
LocalAppServices.current.let { OtsSettingsScreen(it.otsSettings, it.torSettings, nav) }
}
@OptIn(ExperimentalMaterial3Api::class)
@@ -18,7 +18,7 @@
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
package com.vitorpamplona.amethyst.ui.screen.loggedIn.settings
package com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.settings
import androidx.compose.foundation.background
import androidx.compose.foundation.border
@@ -64,7 +64,7 @@ import com.vitorpamplona.amethyst.commons.resources.ots_reset_to_auto_select
import com.vitorpamplona.amethyst.commons.resources.save
import com.vitorpamplona.amethyst.commons.ui.stringRes
import com.vitorpamplona.amethyst.commons.ui.theme.BitcoinOrange
import com.vitorpamplona.quartz.nip03Timestamp.okhttp.OkHttpBitcoinExplorer
import com.vitorpamplona.quartz.nip03Timestamp.BitcoinExplorerUrls
/**
* Settings section for configuring the blockchain explorer used for OTS verification.
@@ -180,7 +180,7 @@ private fun ActiveExplorerDisplay(
val isCustom = settings.hasCustomExplorer
val activeUrl =
settings.normalizedUrl()
?: if (isTorActive) OkHttpBitcoinExplorer.MEMPOOL_API_URL else OkHttpBitcoinExplorer.BLOCKSTREAM_API_URL
?: if (isTorActive) BitcoinExplorerUrls.MEMPOOL else BitcoinExplorerUrls.BLOCKSTREAM
Column {
Row(
@@ -18,9 +18,8 @@
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
package com.vitorpamplona.amethyst.ui.screen.loggedIn.settings.nip46
package com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.settings.nip46
import android.widget.Toast
import androidx.compose.foundation.background
import androidx.compose.foundation.border
import androidx.compose.foundation.clickable
@@ -56,14 +55,12 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.text.font.FontFamily
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import coil3.compose.AsyncImage
import com.vitorpamplona.amethyst.Amethyst
import com.vitorpamplona.amethyst.commons.connectedApps.nip46.Nip46ClientInfo
import com.vitorpamplona.amethyst.commons.connectedApps.nip46.Nip46PermissionAuthorizer
import com.vitorpamplona.amethyst.commons.connectedApps.signers.AppSignerPolicy
@@ -84,11 +81,14 @@ import com.vitorpamplona.amethyst.commons.resources.nip46_signer_apps_empty
import com.vitorpamplona.amethyst.commons.resources.nip46_signer_manage_apps
import com.vitorpamplona.amethyst.commons.resources.nip46_signer_reconnecting
import com.vitorpamplona.amethyst.commons.resources.nip46_signer_remote_app
import com.vitorpamplona.amethyst.commons.service.AppServices
import com.vitorpamplona.amethyst.commons.ui.components.rememberShortNotice
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.timeAgo
import com.vitorpamplona.amethyst.commons.ui.platform.LocalAppServices
import com.vitorpamplona.amethyst.commons.ui.pluralStringRes
import com.vitorpamplona.amethyst.commons.ui.stringRes
import com.vitorpamplona.amethyst.commons.util.toTimeAgo
import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel
import com.vitorpamplona.quartz.nip01Core.core.HexKey
import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl
@@ -125,7 +125,8 @@ fun Nip46ConnectedAppsScreen(
val reconnectingStr = stringRes(Res.string.nip46_signer_reconnecting)
val account = accountViewModel.account
val signerPubKey = remember { account.signer.pubKey }
val context = LocalContext.current
val shortNotice = rememberShortNotice()
val services = LocalAppServices.current
// Live relay state so each app shows whether the signer currently reaches it. An app that brought
// its own relays (nostrconnect) is judged on those; a bunker-flow app rides the inbox relays.
@@ -137,7 +138,7 @@ fun Nip46ConnectedAppsScreen(
var refreshKey by remember { mutableIntStateOf(0) }
LaunchedEffect(refreshKey) {
items = withContext(Dispatchers.Default) { loadNip46Apps(signerPubKey) }
items = withContext(Dispatchers.Default) { loadNip46Apps(services, signerPubKey) }
}
Scaffold(
@@ -187,7 +188,7 @@ fun Nip46ConnectedAppsScreen(
online = online,
onReconnect = {
account.client.reconnect(ignoreRetryDelays = true)
Toast.makeText(context, reconnectingStr, Toast.LENGTH_SHORT).show()
shortNotice.show(reconnectingStr)
},
onClick = { nav.nav(Route.ConnectedAppDetail(entry.coordinate)) },
)
@@ -241,7 +242,7 @@ private fun Nip46AppCard(
val meta =
buildList {
if (relayCount > 0) add(pluralStringRes(Res.plurals.nip46_signer_app_relay_count, relayCount, relayCount))
entry.lastUsedSeconds?.let { add(stringRes(Res.string.nip46_signer_app_last_used, it.toTimeAgo().trim())) }
entry.lastUsedSeconds?.let { add(stringRes(Res.string.nip46_signer_app_last_used, timeAgo(it).trim())) }
}.joinToString(" · ")
if (meta.isNotEmpty()) {
Text(
@@ -291,7 +292,7 @@ private val LiveGreen = Color(0xFF3DDC84)
/** A colored dot: green when the signer currently reaches the relay(s), muted otherwise. */
@Composable
internal fun Nip46StatusDot(online: Boolean) {
fun Nip46StatusDot(online: Boolean) {
Box(Modifier.size(8.dp).clip(CircleShape).background(if (online) LiveGreen else MaterialTheme.colorScheme.outline))
}
@@ -301,7 +302,7 @@ internal fun Nip46StatusDot(online: Boolean) {
* pill inside a clickable card doesn't also open the card.
*/
@Composable
internal fun Nip46ReconnectPill(onClick: () -> Unit) {
fun Nip46ReconnectPill(onClick: () -> Unit) {
Text(
stringRes(Res.string.nip46_signer_app_reconnect),
style = MaterialTheme.typography.labelSmall,
@@ -317,7 +318,7 @@ internal fun Nip46ReconnectPill(onClick: () -> Unit) {
/** A [Nip46StatusDot] + Connected/Offline label showing whether the signer currently reaches an app's relays. */
@Composable
internal fun Nip46LiveStatus(online: Boolean) {
fun Nip46LiveStatus(online: Boolean) {
Row(verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(6.dp)) {
Nip46StatusDot(online)
Text(
@@ -334,7 +335,7 @@ internal fun Nip46LiveStatus(online: Boolean) {
* `null` when neither resolves to any relay (nothing to show a status for). "Online" means at least
* one of the app's relays is in [connectedRelays].
*/
internal fun nip46AppOnline(
fun nip46AppOnline(
appRelays: Set<String>,
inboxRelays: Set<NormalizedRelayUrl>,
connectedRelays: Set<NormalizedRelayUrl>,
@@ -352,7 +353,7 @@ internal fun nip46AppOnline(
* user's IP (see BrowserIconRegistry).
*/
@Composable
internal fun Nip46AppIcon(
fun Nip46AppIcon(
image: String?,
modifier: Modifier = Modifier,
) {
@@ -388,7 +389,7 @@ internal fun Nip46AppIcon(
* advertised one, otherwise its npub. Shared by the list card and the detail header so a row and
* the screen it opens always agree.
*/
internal fun nip46ClientSubtitle(
fun nip46ClientSubtitle(
url: String?,
clientPubKey: HexKey,
): String {
@@ -402,9 +403,12 @@ internal fun nip46ClientSubtitle(
return host ?: runCatching { NPub.create(clientPubKey) }.getOrDefault(clientPubKey.take(12) + "…")
}
private suspend fun loadNip46Apps(signerPubKey: HexKey): List<Nip46AppEntry> {
val store = Amethyst.instance.signerPermissionStore
val clientStore = Amethyst.instance.nip46ClientStore
private suspend fun loadNip46Apps(
services: AppServices,
signerPubKey: HexKey,
): List<Nip46AppEntry> {
val store = services.signerPermissionStore
val clientStore = services.nip46ClientStore
// allPolicies() already carries each app's policy — read it from the map instead of a second
// loadPolicy() call per app.
return store
@@ -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.quartz.nip03Timestamp
/** The public Esplora-compatible block explorers OTS verification can ask. */
object BitcoinExplorerUrls {
/** Blockstream's API; doesn't accept Tor. */
const val BLOCKSTREAM = "https://blockstream.info/api"
/** mempool.space's API; accepts Tor. */
const val MEMPOOL = "https://mempool.space/api/"
}
@@ -129,3 +129,32 @@ data class NamecoinFallbackPolicy(
val NONE = NamecoinFallbackPolicy()
}
}
data class RpcProbeResult(
val success: Boolean,
val elapsedMs: Long,
val chain: String? = null,
val blocks: Int? = null,
val verificationProgress: Double? = null,
val initialBlockDownload: Boolean? = null,
val error: String? = null,
/**
* PEM-encoded server leaf certificate captured during the probe.
* Only populated when the URL scheme is https and the TLS handshake
* succeeded (even if the subsequent HTTP request failed). Used by
* the Settings UI to prompt for a TOFU pin.
*/
val serverCertPem: String? = null,
/**
* SHA-256 fingerprint of the captured leaf certificate, formatted
* as colon-separated uppercase hex bytes (matches
* [ServerTestResult.certFingerprint] for ElectrumX).
*/
val certFingerprint: String? = null,
/**
* True when the probe failed specifically because the TLS handshake
* was rejected (self-signed cert, untrusted CA, hostname mismatch).
* Used by the Settings UI to suggest a TOFU pin as the fix.
*/
val tlsHandshakeFailed: Boolean = false,
)
@@ -21,6 +21,7 @@
package com.vitorpamplona.quartz.nip03Timestamp.okhttp
import com.vitorpamplona.quartz.nip01Core.jackson.JacksonMapper
import com.vitorpamplona.quartz.nip03Timestamp.BitcoinExplorerUrls
import com.vitorpamplona.quartz.nip03Timestamp.ots.BitcoinExplorer
import com.vitorpamplona.quartz.nip03Timestamp.ots.BlockHeader
import com.vitorpamplona.quartz.nip03Timestamp.ots.OtsBlockHeightCache
@@ -116,9 +117,9 @@ class OkHttpBitcoinExplorer(
companion object {
// doesn't accept Tor
const val BLOCKSTREAM_API_URL = "https://blockstream.info/api"
const val BLOCKSTREAM_API_URL = BitcoinExplorerUrls.BLOCKSTREAM
// accepts Tor
const val MEMPOOL_API_URL = "https://mempool.space/api/"
const val MEMPOOL_API_URL = BitcoinExplorerUrls.MEMPOOL
}
}
@@ -567,31 +567,3 @@ class NamecoinCoreRpcClient(
}
/** Outcome of a `Test RPC` probe in Settings. */
data class RpcProbeResult(
val success: Boolean,
val elapsedMs: Long,
val chain: String? = null,
val blocks: Int? = null,
val verificationProgress: Double? = null,
val initialBlockDownload: Boolean? = null,
val error: String? = null,
/**
* PEM-encoded server leaf certificate captured during the probe.
* Only populated when the URL scheme is https and the TLS handshake
* succeeded (even if the subsequent HTTP request failed). Used by
* the Settings UI to prompt for a TOFU pin.
*/
val serverCertPem: String? = null,
/**
* SHA-256 fingerprint of the captured leaf certificate, formatted
* as colon-separated uppercase hex bytes (matches
* [ServerTestResult.certFingerprint] for ElectrumX).
*/
val certFingerprint: String? = null,
/**
* True when the probe failed specifically because the TLS handshake
* was rejected (self-signed cert, untrusted CA, hostname mismatch).
* Used by the Settings UI to suggest a TOFU pin as the fix.
*/
val tlsHandshakeFailed: Boolean = false,
)