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.polls.results.PollResultsScreen
import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.privacy.PrivacyOptionsScreen 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.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.publicChats.PublicChatsScreen
import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.qrcode.ShowQRScreen import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.qrcode.ShowQRScreen
import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.redirect.LoadRedirectScreen 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.MessagesSettingsScreen
import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.settings.MutedThreadsScreen 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.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.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.ProfileUiSettingsScreen
import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.settings.ReactionsSettingsScreen 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.SearchEngineSettingsScreen
import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.settings.SecurityFiltersScreen 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.SpammingUsersScreen
import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.settings.UpdateZapAmountScreen import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.settings.UpdateZapAmountScreen
import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.settings.UserSettingsScreen 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.CordnHubScreen
import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.settings.cordn.CordnKeyPackagesScreen 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.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.settings.nip46.Nip46SignerScreen
import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.shorts.ShortsScreen import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.shorts.ShortsScreen
import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.softwareapps.SoftwareAppDetailScreen 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.nests.NestsScreen
import com.vitorpamplona.amethyst.ui.screen.loggedIn.notifications.NotificationScreen 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.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.qrcode.ScanQrImageScreen
import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.nip86.RelayManagementScreen 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.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.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.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.CordnBackupScreen
import com.vitorpamplona.amethyst.ui.screen.loggedIn.settings.cordn.CordnMigrateScreen 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.video.hls.NewHlsVideoScreen
import com.vitorpamplona.amethyst.ui.screen.loggedIn.webBookmarks.WebBookmarksScreen import com.vitorpamplona.amethyst.ui.screen.loggedIn.webBookmarks.WebBookmarksScreen
import com.vitorpamplona.amethyst.ui.screen.loggedIn.workouts.NewWorkoutScreen 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.platform.MAX_VOICE_RECORD_SECONDS
import com.vitorpamplona.amethyst.commons.ui.note.types.EditState import com.vitorpamplona.amethyst.commons.ui.note.types.EditState
import com.vitorpamplona.amethyst.commons.ui.richtext.InLineIconRenderer 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.OnchainZapSendDialog
import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.wallet.navigateToReloadMint import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.wallet.navigateToReloadMint
import com.vitorpamplona.amethyst.commons.ui.stringRes 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.service.payments.PayToAppAvailability
import com.vitorpamplona.amethyst.ui.actions.uploads.RecordAudioBox 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.PaymentTargetsDialog
import com.vitorpamplona.amethyst.ui.screen.loggedIn.profile.header.paymentTargetStyleFor
import com.vitorpamplona.quartz.nip01Core.core.Event import com.vitorpamplona.quartz.nip01Core.core.Event
import com.vitorpamplona.quartz.nip10Notes.BaseThreadedEvent import com.vitorpamplona.quartz.nip10Notes.BaseThreadedEvent
import com.vitorpamplona.quartz.nip10Notes.TextNoteEvent import com.vitorpamplona.quartz.nip10Notes.TextNoteEvent
@@ -20,8 +20,6 @@
*/ */
package com.vitorpamplona.amethyst.ui.note 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.Arrangement
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.ExperimentalLayoutApi import androidx.compose.foundation.layout.ExperimentalLayoutApi
@@ -58,7 +56,6 @@ import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.input.KeyboardCapitalization import androidx.compose.ui.text.input.KeyboardCapitalization
import androidx.compose.ui.text.input.KeyboardType 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.unit.sp
import androidx.compose.ui.window.Dialog import androidx.compose.ui.window.Dialog
import androidx.compose.ui.window.DialogProperties import androidx.compose.ui.window.DialogProperties
import androidx.core.net.toUri
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.lifecycle.viewmodel.compose.viewModel 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.StdHorzSpacer
import com.vitorpamplona.amethyst.commons.ui.theme.ZeroPadding import com.vitorpamplona.amethyst.commons.ui.theme.ZeroPadding
import com.vitorpamplona.amethyst.commons.ui.theme.placeholderText 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.util.showAmount
import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel
import com.vitorpamplona.quartz.nip01Core.core.Event import com.vitorpamplona.quartz.nip01Core.core.Event
import com.vitorpamplona.quartz.nip57Zaps.ZapReceiptEvent import com.vitorpamplona.quartz.nip57Zaps.ZapReceiptEvent
import com.vitorpamplona.quartz.utils.BigDecimal
import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.ImmutableList
import kotlinx.coroutines.CancellationException
class ZapOptionViewModel : ViewModel() { class ZapOptionViewModel : ViewModel() {
private var account: Account? = null private var account: Account? = null
@@ -266,7 +264,7 @@ fun ZapCustomDialog(
onClick = { onClick = {
postViewModel.customAmount = TextFieldValue(amount.toString()) postViewModel.customAmount = TextFieldValue(amount.toString())
}, },
label = { Text("⚡ ${showAmount(amount.toBigDecimal())}") }, label = { Text("⚡ ${showAmount(BigDecimal(amount))}") },
) )
} }
} }
@@ -517,7 +515,7 @@ fun DisplayPayable(
} }
Row { Row {
Text( Text(
text = showAmount((payable.amountMilliSats / 1000.0f).toBigDecimal()), text = showAmount(BigDecimal((payable.amountMilliSats / 1000.0f).toString())),
maxLines = 1, maxLines = 1,
overflow = TextOverflow.Ellipsis, overflow = TextOverflow.Ellipsis,
fontWeight = FontWeight.Bold, fontWeight = FontWeight.Bold,
@@ -535,10 +533,10 @@ fun DisplayPayable(
} }
Spacer(modifier = DoubleHorzSpacer) Spacer(modifier = DoubleHorzSpacer)
val context = LocalContext.current val walletLauncher = rememberWalletAppLauncher()
PayButton(isActive = !paid.value) { PayButton(isActive = !paid.value) {
payViaIntent(payable.invoice, context, noWalletFoundStr, { paid.value = true }) { walletLauncher.payInvoice(payable.invoice, noWalletFoundStr, { paid.value = true }) {
accountViewModel.toastManager.toast( accountViewModel.toastManager.toast(
Res.string.error_dialog_zap_error, Res.string.error_dialog_zap_error,
UserBasedErrorMessage(it, payable.info.user), 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 @Composable
fun PayButton( fun PayButton(
isActive: Boolean, 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.error_dialog_pay_invoice_error
import com.vitorpamplona.amethyst.commons.resources.pay import com.vitorpamplona.amethyst.commons.resources.pay
import com.vitorpamplona.amethyst.commons.resources.sats 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.loadStringRes
import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav
import com.vitorpamplona.amethyst.commons.ui.navigation.routes.routeFor 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.SmallBorder
import com.vitorpamplona.amethyst.commons.ui.theme.StdHorzSpacer import com.vitorpamplona.amethyst.commons.ui.theme.StdHorzSpacer
import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel 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.PaymentCard
import com.vitorpamplona.amethyst.ui.components.PaymentCardAmount import com.vitorpamplona.amethyst.ui.components.PaymentCardAmount
import com.vitorpamplona.quartz.experimental.clink.common.SatRange 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.commons.viewmodels.AccountViewModel
import com.vitorpamplona.amethyst.favorites.FavoriteAppLauncher import com.vitorpamplona.amethyst.favorites.FavoriteAppLauncher
import com.vitorpamplona.amethyst.ui.navigation.topbars.AndroidAroundMeLocationLabel 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.amethyst.ui.screen.loggedIn.nests.room.activity.NestActivity
import com.vitorpamplona.quartz.concord.cord02Community.ImagePointer 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.commons.ui.navigation.bottombars.AppBottomBar as AppBottomBarImpl
import com.vitorpamplona.amethyst.favorites.rememberManifestIconModel as AppRememberManifestIconModel import com.vitorpamplona.amethyst.favorites.rememberManifestIconModel as AppRememberManifestIconModel
import com.vitorpamplona.amethyst.favorites.rememberNappletIconModel as AppRememberNappletIconModel 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. // Per flavour: Play links the hosted policies, F-Droid surfaces none.
override fun legalSettingsCategory(uriHandler: UriHandler): SettingsCategory? = flavorLegalSettingsCategory(uriHandler) 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 @Composable
override fun AppBottomBar( override fun AppBottomBar(
selectedRoute: Route?, selectedRoute: Route?,
@@ -23,18 +23,26 @@ package com.vitorpamplona.amethyst.ui.platform
import com.vitorpamplona.amethyst.Amethyst import com.vitorpamplona.amethyst.Amethyst
import com.vitorpamplona.amethyst.commons.browser.BrowserHistoryRegistry import com.vitorpamplona.amethyst.commons.browser.BrowserHistoryRegistry
import com.vitorpamplona.amethyst.commons.browser.BrowserIconRegistry 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.connectedApps.signers.NostrSignerPermissionStore
import com.vitorpamplona.amethyst.commons.favorites.FavoriteAppsRegistry import com.vitorpamplona.amethyst.commons.favorites.FavoriteAppsRegistry
import com.vitorpamplona.amethyst.commons.model.location.DeviceLocation import com.vitorpamplona.amethyst.commons.model.location.DeviceLocation
import com.vitorpamplona.amethyst.commons.model.preferences.AppPreferenceStores 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.napplet.permissions.NappletPermissionLedger
import com.vitorpamplona.amethyst.commons.service.AppServices import com.vitorpamplona.amethyst.commons.service.AppServices
import com.vitorpamplona.amethyst.commons.service.BlossomServerFinder import com.vitorpamplona.amethyst.commons.service.BlossomServerFinder
import com.vitorpamplona.amethyst.commons.service.ai.AltTextSuggester 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.commons.tor.TorSettingsFlow
import com.vitorpamplona.amethyst.service.ai.MLKitImageLabelService import com.vitorpamplona.amethyst.service.ai.MLKitImageLabelService
import com.vitorpamplona.amethyst.service.location.CachedReversedGeoLocations 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.NamecoinNameResolver
import com.vitorpamplona.quartz.nip05DnsIdentifiers.namecoin.RpcProbeResult
import com.vitorpamplona.quartz.nip05DnsIdentifiers.namecoin.ServerTestResult
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.map
@@ -64,6 +72,14 @@ object AndroidAppServices : AppServices {
override val namecoinResolver: NamecoinNameResolver get() = Amethyst.instance.namecoinResolver 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 blossomServerFinder: BlossomServerFinder = AndroidBlossomServerFinder
override val deviceLocation: DeviceLocation get() = Amethyst.instance.locationManager override val deviceLocation: DeviceLocation get() = Amethyst.instance.locationManager
@@ -85,3 +101,19 @@ private object AndroidBlossomServerFinder : BlossomServerFinder {
.findServers(blossomUri) .findServers(blossomUri)
?.serverUrl ?.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.navigation.topbars.TopBarWithBackButton
import com.vitorpamplona.amethyst.commons.ui.platform.rememberManifestIconModel import com.vitorpamplona.amethyst.commons.ui.platform.rememberManifestIconModel
import com.vitorpamplona.amethyst.commons.ui.platform.rememberWebAppIconModel 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.ui.stringRes
import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel
import com.vitorpamplona.amethyst.napplet.NappletBrokerService 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.descriptionRes
import com.vitorpamplona.amethyst.napplet.labelRes import com.vitorpamplona.amethyst.napplet.labelRes
import com.vitorpamplona.amethyst.napplet.resolveNappletMeta 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.NormalizedRelayUrl
import com.vitorpamplona.quartz.nip01Core.relay.normalizer.RelayUrlNormalizer import com.vitorpamplona.quartz.nip01Core.relay.normalizer.RelayUrlNormalizer
import com.vitorpamplona.quartz.nip42RelayAuth.RelayAuthEvent 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.components.util.setText
import com.vitorpamplona.amethyst.commons.ui.loadStringRes import com.vitorpamplona.amethyst.commons.ui.loadStringRes
import com.vitorpamplona.amethyst.commons.ui.note.ErrorMessageDialog 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.screen.loggedIn.qrcode.QrCodeDrawer
import com.vitorpamplona.amethyst.commons.ui.stringRes import com.vitorpamplona.amethyst.commons.ui.stringRes
import com.vitorpamplona.amethyst.commons.ui.theme.Size20Modifier import com.vitorpamplona.amethyst.commons.ui.theme.Size20Modifier
@@ -29,8 +29,8 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav
import com.vitorpamplona.amethyst.commons.ui.screen.RefresheableFeedView 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.commons.viewmodels.AccountViewModel
import com.vitorpamplona.amethyst.ui.screen.loggedIn.profile.pinnedNotes.dal.UserProfilePinnedNotesFeedViewModel
@Composable @Composable
fun TabPinnedNotes( 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, 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 the live-activity channel, chess, Cordn group creation, the QR share and the compose
settings. 204 of 252 screens shared. 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). - `ReactionsRow` (2.7k lines: pay-to-app, voice replies, wallet intents).
- The Nests activity and PiP. - The Nests activity and PiP.
- The call screens. - 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 package com.vitorpamplona.amethyst.commons.model.nip03Timestamp
import androidx.compose.runtime.Stable import androidx.compose.runtime.Stable
import com.vitorpamplona.quartz.nip03Timestamp.okhttp.OkHttpBitcoinExplorer import com.vitorpamplona.quartz.nip03Timestamp.BitcoinExplorerUrls
import kotlinx.serialization.Serializable import kotlinx.serialization.Serializable
/** /**
@@ -57,8 +57,8 @@ data class OtsSettings(
val KNOWN_EXPLORERS = val KNOWN_EXPLORERS =
listOf( listOf(
OkHttpBitcoinExplorer.MEMPOOL_API_URL to "mempool.space (Tor-friendly)", BitcoinExplorerUrls.MEMPOOL to "mempool.space (Tor-friendly)",
OkHttpBitcoinExplorer.BLOCKSTREAM_API_URL to "blockstream.info", BitcoinExplorerUrls.BLOCKSTREAM to "blockstream.info",
) )
fun isValidUrl(url: String): Boolean { 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.BrowserHistoryRegistry
import com.vitorpamplona.amethyst.commons.browser.BrowserIconRegistry 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.connectedApps.signers.NostrSignerPermissionStore
import com.vitorpamplona.amethyst.commons.favorites.FavoriteAppsRegistry import com.vitorpamplona.amethyst.commons.favorites.FavoriteAppsRegistry
import com.vitorpamplona.amethyst.commons.model.location.DeviceLocation import com.vitorpamplona.amethyst.commons.model.location.DeviceLocation
import com.vitorpamplona.amethyst.commons.model.preferences.AppPreferenceStores 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.napplet.permissions.NappletPermissionLedger
import com.vitorpamplona.amethyst.commons.service.ai.AltTextSuggester 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.commons.tor.TorSettingsFlow
import com.vitorpamplona.quartz.nip05DnsIdentifiers.namecoin.NamecoinNameResolver import com.vitorpamplona.quartz.nip05DnsIdentifiers.namecoin.NamecoinNameResolver
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
@@ -64,6 +68,18 @@ interface AppServices {
/** Resolves `.bit` names and `d/`/`id/` identifiers over the configured ElectrumX servers. */ /** Resolves `.bit` names and `d/`/`id/` identifiers over the configured ElectrumX servers. */
val namecoinResolver: NamecoinNameResolver 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). */ /** Finds the server behind a `blossom:` URI (BUD-10). */
val blossomServerFinder: BlossomServerFinder get() = BlossomServerFinder.None 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 * 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. * 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.model.Account
import com.vitorpamplona.amethyst.commons.tor.MoneyOpRelayRouting 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.CancellationException
import kotlinx.coroutines.CompletableDeferred import kotlinx.coroutines.CompletableDeferred
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.IO
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import kotlinx.coroutines.withTimeoutOrNull import kotlinx.coroutines.withTimeoutOrNull
@@ -18,9 +18,8 @@
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION * 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. * 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.foundation.combinedClickable
import androidx.compose.material3.LocalTextStyle import androidx.compose.material3.LocalTextStyle
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
@@ -30,12 +29,12 @@ import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalClipboard import androidx.compose.ui.platform.LocalClipboard
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.text.TextStyle import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.text.style.TextOverflow
import com.vitorpamplona.amethyst.commons.resources.Res import com.vitorpamplona.amethyst.commons.resources.Res
import com.vitorpamplona.amethyst.commons.resources.copied_to_clipboard import com.vitorpamplona.amethyst.commons.resources.copied_to_clipboard
import com.vitorpamplona.amethyst.commons.resources.copy_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.components.util.setText
import com.vitorpamplona.amethyst.commons.ui.loadStringRes import com.vitorpamplona.amethyst.commons.ui.loadStringRes
import com.vitorpamplona.amethyst.commons.ui.stringRes 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 * 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]. * 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 * 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 * @param copyValue raw value placed on the clipboard on long-press
* (typically the full, unmodified profile field). * (typically the full, unmodified profile field).
* @param onClick tap action — usually "open the link" or "expand zap UI". * @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 * value is placed on the clipboard. Defaults to a generic
* "Copied to clipboard" message. * "Copied to clipboard" message.
* @param onLongClickLabelResId accessibility label exposed to TalkBack for * @param onLongClickLabelResId accessibility label exposed to TalkBack for
@@ -78,7 +77,7 @@ fun LongPressCopyText(
toastResId: StringResource = Res.string.copied_to_clipboard, toastResId: StringResource = Res.string.copied_to_clipboard,
onLongClickLabelResId: StringResource = Res.string.copy_to_clipboard, onLongClickLabelResId: StringResource = Res.string.copy_to_clipboard,
) { ) {
val context = LocalContext.current val shortNotice = rememberShortNotice()
val clipboard = LocalClipboard.current val clipboard = LocalClipboard.current
val scope = rememberCoroutineScope() val scope = rememberCoroutineScope()
val longClickLabel = stringRes(onLongClickLabelResId) val longClickLabel = stringRes(onLongClickLabelResId)
@@ -96,7 +95,7 @@ fun LongPressCopyText(
onLongClick = { onLongClick = {
scope.launch { scope.launch {
clipboard.setText(copyValue) clipboard.setText(copyValue)
Toast.makeText(context, loadStringRes(toastResId), Toast.LENGTH_SHORT).show() shortNotice.show(loadStringRes(toastResId))
} }
}, },
onLongClickLabel = longClickLabel, 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.ui.settings.SettingsCategory
import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel
import com.vitorpamplona.quartz.concord.cord02Community.ImagePointer 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 * 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. */ /** The settings screen's legal links, when the build's distribution calls for them. */
fun legalSettingsCategory(uriHandler: UriHandler): SettingsCategory? = null 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 @Composable
fun AppBottomBar( fun AppBottomBar(
selectedRoute: Route?, selectedRoute: Route?,
@@ -58,6 +67,10 @@ interface AppPlatform {
@Composable @Composable
fun AroundMeLocationLabel() {} 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. */ /** A full-screen dialog to pick a place (map, search, "use my location"), as a geohash. */
@Composable @Composable
fun GeohashLocationPickerDialog( fun GeohashLocationPickerDialog(
@@ -135,6 +148,9 @@ fun AppBottomBar(
@Composable @Composable
fun AroundMeLocationLabel() = LocalAppPlatform.current.AroundMeLocationLabel() fun AroundMeLocationLabel() = LocalAppPlatform.current.AroundMeLocationLabel()
@Composable
fun SpeakNameButton(name: String) = LocalAppPlatform.current.SpeakNameButton(name)
@Composable @Composable
fun GeohashLocationPickerDialog( fun GeohashLocationPickerDialog(
initialGeohash: String?, initialGeohash: String?,
@@ -18,7 +18,7 @@
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION * 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. * 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.ScrollState
import androidx.compose.foundation.gestures.scrollBy 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.platform.LocalDensity
import androidx.compose.ui.unit.IntSize import androidx.compose.ui.unit.IntSize
import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.lifecycle.viewmodel.compose.viewModel
import com.vitorpamplona.amethyst.commons.model.User import com.vitorpamplona.amethyst.commons.model.User
import com.vitorpamplona.amethyst.commons.model.cache.LocalCache import com.vitorpamplona.amethyst.commons.model.cache.LocalCache
import com.vitorpamplona.amethyst.commons.model.navigation.Route 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.notes
import com.vitorpamplona.amethyst.commons.resources.profile_tab_apps import com.vitorpamplona.amethyst.commons.resources.profile_tab_apps
import com.vitorpamplona.amethyst.commons.resources.replies 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.UserBlockedFeed
import com.vitorpamplona.amethyst.commons.ui.feeds.WatchLifecycleAndUpdateModel import com.vitorpamplona.amethyst.commons.ui.feeds.WatchLifecycleAndUpdateModel
import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav
import com.vitorpamplona.amethyst.commons.ui.platform.AppBottomBar 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.stringRes
import com.vitorpamplona.amethyst.commons.ui.theme.DividerThickness import com.vitorpamplona.amethyst.commons.ui.theme.DividerThickness
import com.vitorpamplona.amethyst.commons.ui.theme.Size8dp import com.vitorpamplona.amethyst.commons.ui.theme.Size8dp
import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel
import com.vitorpamplona.amethyst.commons.viewmodels.UserExternalIdentitiesViewModel 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 import kotlinx.coroutines.launch
@Composable @Composable
@@ -154,7 +154,7 @@ fun PrepareViewModels(
nav: INav, nav: INav,
) { ) {
val followsFeedViewModel: UserProfileFollowsUserFeedViewModel = val followsFeedViewModel: UserProfileFollowsUserFeedViewModel =
viewModel( rememberViewModel(
key = baseUser.pubkeyHex + "UserProfileFollowsUserFeedViewModel", key = baseUser.pubkeyHex + "UserProfileFollowsUserFeedViewModel",
factory = factory =
UserProfileFollowsUserFeedViewModel.Factory( UserProfileFollowsUserFeedViewModel.Factory(
@@ -164,7 +164,7 @@ fun PrepareViewModels(
) )
val galleryFeedViewModel: UserProfileGalleryFeedViewModel = val galleryFeedViewModel: UserProfileGalleryFeedViewModel =
viewModel( rememberViewModel(
key = baseUser.pubkeyHex + "UserGalleryFeedViewModel", key = baseUser.pubkeyHex + "UserGalleryFeedViewModel",
factory = factory =
UserProfileGalleryFeedViewModel.Factory( UserProfileGalleryFeedViewModel.Factory(
@@ -174,7 +174,7 @@ fun PrepareViewModels(
) )
val followersFeedViewModel: UserProfileFollowersUserFeedViewModel = val followersFeedViewModel: UserProfileFollowersUserFeedViewModel =
viewModel( rememberViewModel(
key = baseUser.pubkeyHex + "UserProfileFollowersUserFeedViewModel", key = baseUser.pubkeyHex + "UserProfileFollowersUserFeedViewModel",
factory = factory =
UserProfileFollowersUserFeedViewModel.Factory( UserProfileFollowersUserFeedViewModel.Factory(
@@ -184,26 +184,26 @@ fun PrepareViewModels(
) )
val appRecommendations: UserAppRecommendationsFeedViewModel = val appRecommendations: UserAppRecommendationsFeedViewModel =
viewModel( rememberViewModel(
key = baseUser.pubkeyHex + "UserAppRecommendationsFeedViewModel", key = baseUser.pubkeyHex + "UserAppRecommendationsFeedViewModel",
factory = UserAppRecommendationsFeedViewModel.Factory(baseUser), factory = UserAppRecommendationsFeedViewModel.Factory(baseUser),
) )
val externalIdentities: UserExternalIdentitiesViewModel = val externalIdentities: UserExternalIdentitiesViewModel =
viewModel( rememberViewModel(
key = baseUser.pubkeyHex + "UserExternalIdentitiesViewModel", key = baseUser.pubkeyHex + "UserExternalIdentitiesViewModel",
factory = UserExternalIdentitiesViewModel.Factory(baseUser), factory = UserExternalIdentitiesViewModel.Factory(baseUser),
) )
val zapFeedViewModel: UserProfileZapsViewModel = val zapFeedViewModel: UserProfileZapsViewModel =
viewModel( rememberViewModel(
key = baseUser.pubkeyHex + "UserProfileZapsFeedViewModel", key = baseUser.pubkeyHex + "UserProfileZapsFeedViewModel",
factory = factory =
UserProfileZapsViewModel.Factory(baseUser, accountViewModel.account), UserProfileZapsViewModel.Factory(baseUser, accountViewModel.account),
) )
val threadsViewModel: UserProfileNewThreadsFeedViewModel = val threadsViewModel: UserProfileNewThreadsFeedViewModel =
viewModel( rememberViewModel(
key = baseUser.pubkeyHex + "UserProfileNewThreadsFeedViewModel", key = baseUser.pubkeyHex + "UserProfileNewThreadsFeedViewModel",
factory = factory =
UserProfileNewThreadsFeedViewModel.Factory( UserProfileNewThreadsFeedViewModel.Factory(
@@ -213,7 +213,7 @@ fun PrepareViewModels(
) )
val repliesViewModel: UserProfileConversationsFeedViewModel = val repliesViewModel: UserProfileConversationsFeedViewModel =
viewModel( rememberViewModel(
key = baseUser.pubkeyHex + "UserProfileConversationsFeedViewModel", key = baseUser.pubkeyHex + "UserProfileConversationsFeedViewModel",
factory = factory =
UserProfileConversationsFeedViewModel.Factory( UserProfileConversationsFeedViewModel.Factory(
@@ -223,7 +223,7 @@ fun PrepareViewModels(
) )
val mutualViewModel: UserProfileMutualFeedViewModel = val mutualViewModel: UserProfileMutualFeedViewModel =
viewModel( rememberViewModel(
key = baseUser.pubkeyHex + "UserProfileMutualFeedViewModel", key = baseUser.pubkeyHex + "UserProfileMutualFeedViewModel",
factory = factory =
UserProfileMutualFeedViewModel.Factory( UserProfileMutualFeedViewModel.Factory(
@@ -233,7 +233,7 @@ fun PrepareViewModels(
) )
val bookmarksFeedViewModel: UserProfileBookmarksFeedViewModel = val bookmarksFeedViewModel: UserProfileBookmarksFeedViewModel =
viewModel( rememberViewModel(
key = baseUser.pubkeyHex + "UserProfileBookmarksFeedViewModel", key = baseUser.pubkeyHex + "UserProfileBookmarksFeedViewModel",
factory = factory =
UserProfileBookmarksFeedViewModel.Factory( UserProfileBookmarksFeedViewModel.Factory(
@@ -243,7 +243,7 @@ fun PrepareViewModels(
) )
val pinnedNotesFeedViewModel: UserProfilePinnedNotesFeedViewModel = val pinnedNotesFeedViewModel: UserProfilePinnedNotesFeedViewModel =
viewModel( rememberViewModel(
key = baseUser.pubkeyHex + "UserProfilePinnedNotesFeedViewModel", key = baseUser.pubkeyHex + "UserProfilePinnedNotesFeedViewModel",
factory = factory =
UserProfilePinnedNotesFeedViewModel.Factory( UserProfilePinnedNotesFeedViewModel.Factory(
@@ -253,7 +253,7 @@ fun PrepareViewModels(
) )
val reportsFeedViewModel: UserProfileReportFeedViewModel = val reportsFeedViewModel: UserProfileReportFeedViewModel =
viewModel( rememberViewModel(
key = baseUser.pubkeyHex + "UserProfileReportFeedViewModel", key = baseUser.pubkeyHex + "UserProfileReportFeedViewModel",
factory = factory =
UserProfileReportFeedViewModel.Factory( UserProfileReportFeedViewModel.Factory(
@@ -263,7 +263,7 @@ fun PrepareViewModels(
) )
val appsFeedViewModel: UserProfileAppsFeedViewModel = val appsFeedViewModel: UserProfileAppsFeedViewModel =
viewModel( rememberViewModel(
key = baseUser.pubkeyHex + "UserProfileAppsFeedViewModel", key = baseUser.pubkeyHex + "UserProfileAppsFeedViewModel",
factory = factory =
UserProfileAppsFeedViewModel.Factory( UserProfileAppsFeedViewModel.Factory(
@@ -18,7 +18,7 @@
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION * 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. * 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.Column
import androidx.compose.foundation.layout.fillMaxHeight import androidx.compose.foundation.layout.fillMaxHeight
@@ -29,8 +29,8 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav
import com.vitorpamplona.amethyst.commons.ui.screen.RefresheableFeedView 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.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; * 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 * 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. * 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.AdditiveFeedFilter
import com.vitorpamplona.amethyst.commons.feeds.FilterByListParams 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 * 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. * 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.compose.runtime.Stable
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.viewmodel.CreationExtras
import com.vitorpamplona.amethyst.commons.model.Account import com.vitorpamplona.amethyst.commons.model.Account
import com.vitorpamplona.amethyst.commons.model.User import com.vitorpamplona.amethyst.commons.model.User
import com.vitorpamplona.amethyst.commons.ui.screen.AndroidFeedViewModel import com.vitorpamplona.amethyst.commons.ui.screen.AndroidFeedViewModel
import kotlin.reflect.KClass
@Stable @Stable
class UserProfileAppsFeedViewModel( class UserProfileAppsFeedViewModel(
@@ -37,6 +39,9 @@ class UserProfileAppsFeedViewModel(
val account: Account, val account: Account,
) : ViewModelProvider.Factory { ) : ViewModelProvider.Factory {
@Suppress("UNCHECKED_CAST") @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 * 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. * 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.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
@@ -18,7 +18,7 @@
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION * 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. * 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.Column
import androidx.compose.foundation.layout.fillMaxHeight import androidx.compose.foundation.layout.fillMaxHeight
@@ -29,8 +29,8 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav
import com.vitorpamplona.amethyst.commons.ui.screen.RefresheableFeedView 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.commons.viewmodels.AccountViewModel
import com.vitorpamplona.amethyst.ui.screen.loggedIn.profile.bookmarks.dal.UserProfileBookmarksFeedViewModel
@Composable @Composable
fun TabBookmarks( fun TabBookmarks(
@@ -18,7 +18,7 @@
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION * 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. * 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.feeds.FeedFilter
import com.vitorpamplona.amethyst.commons.model.Account 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 * 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. * 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.compose.runtime.Stable
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.viewmodel.CreationExtras
import com.vitorpamplona.amethyst.commons.model.Account import com.vitorpamplona.amethyst.commons.model.Account
import com.vitorpamplona.amethyst.commons.model.User import com.vitorpamplona.amethyst.commons.model.User
import com.vitorpamplona.amethyst.commons.ui.screen.AndroidFeedViewModel import com.vitorpamplona.amethyst.commons.ui.screen.AndroidFeedViewModel
import kotlin.reflect.KClass
@Stable @Stable
class UserProfileBookmarksFeedViewModel( class UserProfileBookmarksFeedViewModel(
@@ -37,6 +39,9 @@ class UserProfileBookmarksFeedViewModel(
val account: Account, val account: Account,
) : ViewModelProvider.Factory { ) : ViewModelProvider.Factory {
@Suppress("UNCHECKED_CAST") @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 * 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. * 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.Column
import androidx.compose.foundation.layout.fillMaxHeight import androidx.compose.foundation.layout.fillMaxHeight
@@ -26,8 +26,8 @@ import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav
import com.vitorpamplona.amethyst.commons.ui.screen.RefresheableFeedView 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.commons.viewmodels.AccountViewModel
import com.vitorpamplona.amethyst.ui.screen.loggedIn.profile.conversations.dal.UserProfileConversationsFeedViewModel
@Composable @Composable
fun TabNotesConversations( fun TabNotesConversations(
@@ -20,7 +20,7 @@
*/ */
@file:Suppress("DEPRECATION") @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.AdditiveFeedFilter
import com.vitorpamplona.amethyst.commons.feeds.sortedByDefaultFeedOrder 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 * 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. * 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.compose.runtime.Stable
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.viewmodel.CreationExtras
import com.vitorpamplona.amethyst.commons.model.Account import com.vitorpamplona.amethyst.commons.model.Account
import com.vitorpamplona.amethyst.commons.model.User import com.vitorpamplona.amethyst.commons.model.User
import com.vitorpamplona.amethyst.commons.ui.screen.AndroidFeedViewModel import com.vitorpamplona.amethyst.commons.ui.screen.AndroidFeedViewModel
import kotlin.reflect.KClass
@Stable @Stable
class UserProfileConversationsFeedViewModel( class UserProfileConversationsFeedViewModel(
@@ -37,6 +39,9 @@ class UserProfileConversationsFeedViewModel(
val account: Account, val account: Account,
) : ViewModelProvider.Factory { ) : ViewModelProvider.Factory {
@Suppress("UNCHECKED_CAST") @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 * 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. * 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.material3.Text
import androidx.compose.runtime.Composable 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.relayClient.user.observeUserAssertionsFollowerCount
import com.vitorpamplona.amethyst.commons.resources.Res import com.vitorpamplona.amethyst.commons.resources.Res
import com.vitorpamplona.amethyst.commons.resources.number_followers 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.ui.stringRes
import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel
import com.vitorpamplona.amethyst.ui.screen.loggedIn.profile.followers.dal.UserProfileFollowersUserFeedViewModel
@Composable @Composable
fun FollowersTabHeader( fun FollowersTabHeader(
@@ -18,7 +18,7 @@
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION * 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. * 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.Column
import androidx.compose.foundation.layout.fillMaxHeight import androidx.compose.foundation.layout.fillMaxHeight
@@ -33,10 +33,10 @@ import androidx.compose.ui.Modifier
import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav
import com.vitorpamplona.amethyst.commons.ui.note.UserCompose 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.DividerThickness
import com.vitorpamplona.amethyst.commons.ui.theme.FeedPadding import com.vitorpamplona.amethyst.commons.ui.theme.FeedPadding
import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel
import com.vitorpamplona.amethyst.ui.screen.loggedIn.profile.followers.dal.UserProfileFollowersUserFeedViewModel
@Composable @Composable
fun TabFollowers( fun TabFollowers(
@@ -18,24 +18,27 @@
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION * 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. * 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.compose.runtime.Stable
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import androidx.lifecycle.viewmodel.CreationExtras
import com.vitorpamplona.amethyst.commons.model.Account import com.vitorpamplona.amethyst.commons.model.Account
import com.vitorpamplona.amethyst.commons.model.User import com.vitorpamplona.amethyst.commons.model.User
import com.vitorpamplona.quartz.nip01Core.core.Event import com.vitorpamplona.quartz.nip01Core.core.Event
import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter
import com.vitorpamplona.quartz.nip02FollowList.ContactListEvent import com.vitorpamplona.quartz.nip02FollowList.ContactListEvent
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.IO
import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.flowOn import kotlinx.coroutines.flow.flowOn
import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.sample import kotlinx.coroutines.flow.sample
import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.flow.stateIn
import kotlin.reflect.KClass
@Stable @Stable
class UserProfileFollowersUserFeedViewModel( class UserProfileFollowersUserFeedViewModel(
@@ -92,6 +95,9 @@ class UserProfileFollowersUserFeedViewModel(
val account: Account, val account: Account,
) : ViewModelProvider.Factory { ) : ViewModelProvider.Factory {
@Suppress("UNCHECKED_CAST") @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 * 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. * 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.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
@@ -26,9 +26,9 @@ import androidx.compose.runtime.getValue
import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.vitorpamplona.amethyst.commons.resources.Res import com.vitorpamplona.amethyst.commons.resources.Res
import com.vitorpamplona.amethyst.commons.resources.number_following 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.ui.stringRes
import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel
import com.vitorpamplona.amethyst.ui.screen.loggedIn.profile.follows.dal.UserProfileFollowsUserFeedViewModel
@Composable @Composable
fun FollowTabHeader( fun FollowTabHeader(
@@ -18,7 +18,7 @@
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION * 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. * 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.Column
import androidx.compose.foundation.layout.fillMaxHeight import androidx.compose.foundation.layout.fillMaxHeight
@@ -33,10 +33,10 @@ import androidx.compose.ui.Modifier
import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav
import com.vitorpamplona.amethyst.commons.ui.note.UserCompose 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.DividerThickness
import com.vitorpamplona.amethyst.commons.ui.theme.FeedPadding import com.vitorpamplona.amethyst.commons.ui.theme.FeedPadding
import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel
import com.vitorpamplona.amethyst.ui.screen.loggedIn.profile.follows.dal.UserProfileFollowsUserFeedViewModel
@Composable @Composable
fun TabFollows( fun TabFollows(
@@ -18,17 +18,19 @@
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION * 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. * 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.compose.runtime.Stable
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import androidx.lifecycle.viewmodel.CreationExtras
import com.vitorpamplona.amethyst.commons.model.Account import com.vitorpamplona.amethyst.commons.model.Account
import com.vitorpamplona.amethyst.commons.model.User import com.vitorpamplona.amethyst.commons.model.User
import com.vitorpamplona.amethyst.commons.model.cache.LocalCache import com.vitorpamplona.amethyst.commons.model.cache.LocalCache
import com.vitorpamplona.quartz.nip02FollowList.ContactListEvent import com.vitorpamplona.quartz.nip02FollowList.ContactListEvent
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.IO
import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.flowOn import kotlinx.coroutines.flow.flowOn
@@ -36,6 +38,7 @@ import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.onStart import kotlinx.coroutines.flow.onStart
import kotlinx.coroutines.flow.sample import kotlinx.coroutines.flow.sample
import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.flow.stateIn
import kotlin.reflect.KClass
@Stable @Stable
class UserProfileFollowsUserFeedViewModel( class UserProfileFollowsUserFeedViewModel(
@@ -89,6 +92,9 @@ class UserProfileFollowsUserFeedViewModel(
val account: Account, val account: Account,
) : ViewModelProvider.Factory { ) : ViewModelProvider.Factory {
@Suppress("UNCHECKED_CAST") @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 * 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. * 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.ExperimentalFoundationApi
import androidx.compose.foundation.combinedClickable import androidx.compose.foundation.combinedClickable
@@ -18,7 +18,7 @@
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION * 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. * 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.animation.core.tween
import androidx.compose.foundation.layout.Arrangement.spacedBy 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 * 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. * 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.Composable
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
@@ -18,7 +18,7 @@
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION * 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. * 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.Column
import androidx.compose.foundation.layout.fillMaxHeight 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.feeds.ScrollStateKeys
import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav
import com.vitorpamplona.amethyst.commons.ui.screen.SaveableGridFeedState 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.commons.viewmodels.AccountViewModel
import com.vitorpamplona.amethyst.ui.screen.loggedIn.profile.gallery.dal.UserProfileGalleryFeedViewModel
@Composable @Composable
fun TabGallery( fun TabGallery(
@@ -18,7 +18,7 @@
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION * 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. * 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.AdditiveFeedFilter
import com.vitorpamplona.amethyst.commons.feeds.FilterByListParams 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 * 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. * 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.compose.runtime.Stable
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.viewmodel.CreationExtras
import com.vitorpamplona.amethyst.commons.model.Account import com.vitorpamplona.amethyst.commons.model.Account
import com.vitorpamplona.amethyst.commons.model.User import com.vitorpamplona.amethyst.commons.model.User
import com.vitorpamplona.amethyst.commons.ui.screen.AndroidFeedViewModel import com.vitorpamplona.amethyst.commons.ui.screen.AndroidFeedViewModel
import kotlin.reflect.KClass
@Stable @Stable
class UserProfileGalleryFeedViewModel( class UserProfileGalleryFeedViewModel(
@@ -37,6 +39,9 @@ class UserProfileGalleryFeedViewModel(
val account: Account, val account: Account,
) : ViewModelProvider.Factory { ) : ViewModelProvider.Factory {
@Suppress("UNCHECKED_CAST") @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 * 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. * 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.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
@@ -18,7 +18,7 @@
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION * 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. * 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.Column
import androidx.compose.foundation.layout.fillMaxHeight import androidx.compose.foundation.layout.fillMaxHeight
@@ -18,7 +18,7 @@
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION * 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. * 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.Composable
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
@@ -18,7 +18,7 @@
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION * 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. * 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.Composable
import androidx.compose.runtime.remember 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.error_dialog_payment_error
import com.vitorpamplona.amethyst.commons.resources.no_payment_app_found_for_type 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.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.BitcoinOrange
import com.vitorpamplona.amethyst.commons.ui.theme.Size16Modifier import com.vitorpamplona.amethyst.commons.ui.theme.Size16Modifier
import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel 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.nipA3PaymentTargets.PaymentTarget
import com.vitorpamplona.quartz.nipBCOnchainZaps.taproot.SegwitAddress 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 * 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. * 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.Column
import androidx.compose.foundation.layout.ExperimentalLayoutApi import androidx.compose.foundation.layout.ExperimentalLayoutApi
import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Row
@@ -40,16 +39,13 @@ import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.ClipEntry
import androidx.compose.ui.platform.LocalClipboard import androidx.compose.ui.platform.LocalClipboard
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalUriHandler import androidx.compose.ui.platform.LocalUriHandler
import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp import androidx.compose.ui.unit.sp
import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.vitorpamplona.amethyst.R
import com.vitorpamplona.amethyst.commons.buzz.ui.BuzzAgentLabel import com.vitorpamplona.amethyst.commons.buzz.ui.BuzzAgentLabel
import com.vitorpamplona.amethyst.commons.icons.symbols.Icon import com.vitorpamplona.amethyst.commons.icons.symbols.Icon
import com.vitorpamplona.amethyst.commons.icons.symbols.MaterialSymbols 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.telegram
import com.vitorpamplona.amethyst.commons.resources.twitter import com.vitorpamplona.amethyst.commons.resources.twitter
import com.vitorpamplona.amethyst.commons.resources.website 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.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.navigation.navs.INav
import com.vitorpamplona.amethyst.commons.ui.note.ObserveAndRenderNIP05VerifiedSymbol import com.vitorpamplona.amethyst.commons.ui.note.ObserveAndRenderNIP05VerifiedSymbol
import com.vitorpamplona.amethyst.commons.ui.note.lastSeenSentence import com.vitorpamplona.amethyst.commons.ui.note.lastSeenSentence
import com.vitorpamplona.amethyst.commons.ui.painterRes 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.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.stringRes
import com.vitorpamplona.amethyst.commons.ui.theme.Size15Modifier import com.vitorpamplona.amethyst.commons.ui.theme.Size15Modifier
import com.vitorpamplona.amethyst.commons.ui.theme.SpacedBy3dp 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.util.toShortDisplay
import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel
import com.vitorpamplona.amethyst.commons.viewmodels.UserExternalIdentitiesViewModel 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.core.Event
import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter
import com.vitorpamplona.quartz.nip39ExtIdentities.GitHubIdentity 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.TelegramIdentity
import com.vitorpamplona.quartz.nip39ExtIdentities.TwitterIdentity import com.vitorpamplona.quartz.nip39ExtIdentities.TwitterIdentity
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.IO
import kotlinx.coroutines.flow.flowOn import kotlinx.coroutines.flow.flowOn
import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.map
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import org.jetbrains.compose.resources.DrawableResource
import org.jetbrains.compose.resources.StringResource import org.jetbrains.compose.resources.StringResource
private const val IDENTITY_ICON_CACHE_KEY = 0 private const val IDENTITY_ICON_CACHE_KEY = 0
@@ -151,7 +150,7 @@ fun DrawAdditionalInfo(
Spacer(StdHorzSpacer) 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, // 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), modifier = Modifier.size(23.dp).padding(start = 5.dp),
onClick = { onClick = {
scope.launch { 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), modifier = Modifier.size(23.dp).padding(start = 5.dp),
onClick = { onClick = {
scope.launch { scope.launch {
clipboard.setClipEntry(ClipEntry(ClipData.newPlainText("nprofile", baseUser.toNProfile()))) clipboard.setText(baseUser.toNProfile())
} }
}, },
) { ) {
@@ -271,7 +270,7 @@ fun DrawAdditionalInfo(
Row(verticalAlignment = Alignment.CenterVertically) { Row(verticalAlignment = Alignment.CenterVertically) {
Icon( Icon(
tint = Color.Unspecified, tint = Color.Unspecified,
painter = painterRes(resourceId = getIdentityClaimIcon(identity), IDENTITY_ICON_CACHE_KEY), painter = painterRes(getIdentityClaimIcon(identity), IDENTITY_ICON_CACHE_KEY),
contentDescription = stringRes(getIdentityClaimDescription(identity)), contentDescription = stringRes(getIdentityClaimDescription(identity)),
modifier = Modifier.size(18.dp), modifier = Modifier.size(18.dp),
) )
@@ -341,7 +340,6 @@ fun DisplayLastSeen(
val lastSeen by lastSeenFlow.collectAsStateWithLifecycle(null) val lastSeen by lastSeenFlow.collectAsStateWithLifecycle(null)
lastSeen?.let { timestamp -> lastSeen?.let { timestamp ->
val context = LocalContext.current
Text( Text(
text = lastSeenSentence(timestamp), text = lastSeenSentence(timestamp),
color = MaterialTheme.colorScheme.placeholderText, color = MaterialTheme.colorScheme.placeholderText,
@@ -384,13 +382,13 @@ fun DisplayNip05ProfileStatus(
} }
} }
fun getIdentityClaimIcon(identity: IdentityClaimTag): Int = fun getIdentityClaimIcon(identity: IdentityClaimTag): DrawableResource =
when (identity) { when (identity) {
is TwitterIdentity -> R.drawable.x is TwitterIdentity -> Res.drawable.x
is TelegramIdentity -> R.drawable.telegram is TelegramIdentity -> Res.drawable.telegram
is MastodonIdentity -> R.drawable.mastodon is MastodonIdentity -> Res.drawable.mastodon
is GitHubIdentity -> R.drawable.github is GitHubIdentity -> Res.drawable.github
else -> R.drawable.github else -> Res.drawable.github
} }
fun getIdentityClaimDescription(identity: IdentityClaimTag): StringResource = fun getIdentityClaimDescription(identity: IdentityClaimTag): StringResource =
@@ -18,7 +18,7 @@
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION * 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. * 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.ExperimentalFoundationApi
import androidx.compose.foundation.Image import androidx.compose.foundation.Image
@@ -18,7 +18,7 @@
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION * 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. * 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.padding
import androidx.compose.foundation.layout.width import androidx.compose.foundation.layout.width
@@ -18,7 +18,7 @@
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION * 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. * 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.Composable
import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.derivedStateOf
@@ -18,9 +18,8 @@
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION * 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. * 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.background
import androidx.compose.foundation.clickable import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement 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.geometry.Rect
import androidx.compose.ui.layout.boundsInWindow import androidx.compose.ui.layout.boundsInWindow
import androidx.compose.ui.layout.onGloballyPositioned import androidx.compose.ui.layout.onGloballyPositioned
import androidx.compose.ui.platform.ClipEntry
import androidx.compose.ui.platform.LocalClipboard import androidx.compose.ui.platform.LocalClipboard
import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.Dp
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.Icon
import com.vitorpamplona.amethyst.commons.icons.symbols.MaterialSymbols import com.vitorpamplona.amethyst.commons.icons.symbols.MaterialSymbols
import com.vitorpamplona.amethyst.commons.model.User 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.NewUserMetadataViewModel
import com.vitorpamplona.amethyst.commons.ui.actions.uploads.GallerySelectSingle import com.vitorpamplona.amethyst.commons.ui.actions.uploads.GallerySelectSingle
import com.vitorpamplona.amethyst.commons.ui.components.LoadingAnimation 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.navigation.navs.INav
import com.vitorpamplona.amethyst.commons.ui.note.ClickableUserPicture import com.vitorpamplona.amethyst.commons.ui.note.ClickableUserPicture
import com.vitorpamplona.amethyst.commons.ui.note.platform.ZoomableImageDialog 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.stringRes
import com.vitorpamplona.amethyst.commons.ui.theme.Size100dp import com.vitorpamplona.amethyst.commons.ui.theme.Size100dp
import com.vitorpamplona.amethyst.commons.ui.theme.Size35dp import com.vitorpamplona.amethyst.commons.ui.theme.Size35dp
import com.vitorpamplona.amethyst.commons.ui.theme.userProfileBorderModifier import com.vitorpamplona.amethyst.commons.ui.theme.userProfileBorderModifier
import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel
import com.vitorpamplona.amethyst.commons.viewmodels.UserExternalIdentitiesViewModel import com.vitorpamplona.amethyst.commons.viewmodels.UserExternalIdentitiesViewModel
import com.vitorpamplona.amethyst.ui.screen.loggedIn.profile.header.apps.UserAppRecommendationsFeedViewModel
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
@Composable @Composable
@@ -148,8 +147,7 @@ fun ZoomableUserPicture(
onLongClick = { onLongClick = {
baseUser.profilePicture()?.let { it1 -> baseUser.profilePicture()?.let { it1 ->
scope.launch { scope.launch {
val clipData = ClipData.newPlainText("profile picture url", it1) clipboardManager.setText(it1)
clipboardManager.setClipEntry(ClipEntry(clipData))
} }
} }
}, },
@@ -191,7 +189,7 @@ private fun ProfilePictureUploadButton(
accountViewModel: AccountViewModel, accountViewModel: AccountViewModel,
size: Dp, size: Dp,
) { ) {
val postViewModel: NewUserMetadataViewModel = viewModel() val postViewModel: NewUserMetadataViewModel = rememberViewModel { NewUserMetadataViewModel() }
postViewModel.init(accountViewModel) postViewModel.init(accountViewModel)
var showGallerySelect by remember { mutableStateOf(false) } var showGallerySelect by remember { mutableStateOf(false) }
@@ -18,9 +18,8 @@
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION * 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. * 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.BorderStroke
import androidx.compose.foundation.clickable import androidx.compose.foundation.clickable
import androidx.compose.foundation.combinedClickable import androidx.compose.foundation.combinedClickable
@@ -44,7 +43,6 @@ import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalClipboard import androidx.compose.ui.platform.LocalClipboard
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp 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_cashu
import com.vitorpamplona.amethyst.commons.resources.send_payment_method_lightning 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.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.components.util.setText
import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav
import com.vitorpamplona.amethyst.commons.ui.note.LoadAddressableNote 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.Bolt12OffersDialog
import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.profile.header.abbreviateBolt12Offer 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.stringRes
import com.vitorpamplona.amethyst.commons.ui.theme.BitcoinOrange import com.vitorpamplona.amethyst.commons.ui.theme.BitcoinOrange
import com.vitorpamplona.amethyst.commons.ui.theme.Size16Modifier import com.vitorpamplona.amethyst.commons.ui.theme.Size16Modifier
import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel 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.experimental.clink.pointers.NOffer
import com.vitorpamplona.quartz.nipA3PaymentTargets.PaymentTarget import com.vitorpamplona.quartz.nipA3PaymentTargets.PaymentTarget
import com.vitorpamplona.quartz.nipA3PaymentTargets.PaymentTargetsEvent import com.vitorpamplona.quartz.nipA3PaymentTargets.PaymentTargetsEvent
@@ -315,7 +314,7 @@ fun ProfilePaymentChip(
copyValue: String? = null, copyValue: String? = null,
icon: @Composable () -> Unit, icon: @Composable () -> Unit,
) { ) {
val context = LocalContext.current val shortNotice = rememberShortNotice()
val clipboard = LocalClipboard.current val clipboard = LocalClipboard.current
val scope = rememberCoroutineScope() val scope = rememberCoroutineScope()
val copyLabel = stringRes(Res.string.copy_to_clipboard) val copyLabel = stringRes(Res.string.copy_to_clipboard)
@@ -328,7 +327,7 @@ fun ProfilePaymentChip(
onLongClick = { onLongClick = {
scope.launch { scope.launch {
clipboard.setText(copyValue) clipboard.setText(copyValue)
Toast.makeText(context, copiedMessage, Toast.LENGTH_SHORT).show() shortNotice.show(copiedMessage)
} }
}, },
onLongClickLabel = copyLabel, onLongClickLabel = copyLabel,
@@ -18,7 +18,7 @@
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION * 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. * 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.padding
import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.size
@@ -18,7 +18,7 @@
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION * 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. * 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.Arrangement
import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Box
@@ -18,15 +18,13 @@
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION * 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. * 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.Composable
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.platform.LocalClipboard 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.icons.symbols.MaterialSymbols
import com.vitorpamplona.amethyst.commons.model.User import com.vitorpamplona.amethyst.commons.model.User
import com.vitorpamplona.amethyst.commons.nip85TrustedAssertions.ui.EditNicknameDialog 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.M3ActionDialog
import com.vitorpamplona.amethyst.commons.ui.components.M3ActionRow import com.vitorpamplona.amethyst.commons.ui.components.M3ActionRow
import com.vitorpamplona.amethyst.commons.ui.components.M3ActionSection 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.components.util.setText
import com.vitorpamplona.amethyst.commons.ui.note.creators.emojiSuggestions.WatchAndLoadMyEmojiList import com.vitorpamplona.amethyst.commons.ui.note.creators.emojiSuggestions.WatchAndLoadMyEmojiList
import com.vitorpamplona.amethyst.commons.ui.note.externalLinkForUser import com.vitorpamplona.amethyst.commons.ui.note.externalLinkForUser
@@ -85,7 +84,7 @@ fun UserProfileDropDownMenu(
) { ) {
val clipboardManager = LocalClipboard.current val clipboardManager = LocalClipboard.current
val scope = rememberCoroutineScope() val scope = rememberCoroutineScope()
val context = LocalContext.current val textSharer = rememberTextSharer()
// Share section // Share section
M3ActionSection { M3ActionSection {
@@ -102,18 +101,7 @@ fun UserProfileDropDownMenu(
icon = MaterialSymbols.Share, icon = MaterialSymbols.Share,
text = stringRes(Res.string.quick_action_share), text = stringRes(Res.string.quick_action_share),
) { ) {
val sendIntent = textSharer.share(externalLinkForUser(user), quickActionShareBrowserLinkStr, quickActionShareStr)
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)
onDismiss() onDismiss()
} }
} }
@@ -18,7 +18,7 @@
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION * 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. * 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.animation.core.tween
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Arrangement
@@ -18,13 +18,15 @@
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION * 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. * 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.compose.runtime.Stable
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.viewmodel.CreationExtras
import com.vitorpamplona.amethyst.commons.model.User import com.vitorpamplona.amethyst.commons.model.User
import com.vitorpamplona.amethyst.commons.ui.screen.AndroidFeedViewModel import com.vitorpamplona.amethyst.commons.ui.screen.AndroidFeedViewModel
import kotlin.reflect.KClass
@Stable @Stable
class UserAppRecommendationsFeedViewModel( class UserAppRecommendationsFeedViewModel(
@@ -34,6 +36,9 @@ class UserAppRecommendationsFeedViewModel(
val user: User, val user: User,
) : ViewModelProvider.Factory { ) : ViewModelProvider.Factory {
@Suppress("UNCHECKED_CAST") @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 * 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. * 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.AdditiveFeedFilter
import com.vitorpamplona.amethyst.commons.feeds.sortedByDefaultFeedOrder 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 * 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. * 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.background
import androidx.compose.foundation.clickable import androidx.compose.foundation.clickable
@@ -90,6 +90,7 @@ import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.persistentListOf import kotlinx.collections.immutable.persistentListOf
import kotlinx.collections.immutable.toImmutableList import kotlinx.collections.immutable.toImmutableList
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.IO
import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.flowOn import kotlinx.coroutines.flow.flowOn
@@ -18,7 +18,7 @@
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION * 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. * 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.Column
import androidx.compose.foundation.layout.fillMaxHeight import androidx.compose.foundation.layout.fillMaxHeight
@@ -26,8 +26,8 @@ import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav
import com.vitorpamplona.amethyst.commons.ui.screen.RefresheableFeedView 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.commons.viewmodels.AccountViewModel
import com.vitorpamplona.amethyst.ui.screen.loggedIn.profile.mutual.dal.UserProfileMutualFeedViewModel
@Composable @Composable
fun TabMutualConversations( fun TabMutualConversations(
@@ -18,7 +18,7 @@
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION * 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. * 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.AdditiveFeedFilter
import com.vitorpamplona.amethyst.commons.feeds.isRenderableRepost 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 * 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. * 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.compose.runtime.Stable
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.viewmodel.CreationExtras
import com.vitorpamplona.amethyst.commons.model.Account import com.vitorpamplona.amethyst.commons.model.Account
import com.vitorpamplona.amethyst.commons.model.User import com.vitorpamplona.amethyst.commons.model.User
import com.vitorpamplona.amethyst.commons.ui.screen.AndroidFeedViewModel import com.vitorpamplona.amethyst.commons.ui.screen.AndroidFeedViewModel
import kotlin.reflect.KClass
@Stable @Stable
class UserProfileMutualFeedViewModel( class UserProfileMutualFeedViewModel(
@@ -37,6 +39,9 @@ class UserProfileMutualFeedViewModel(
val account: Account, val account: Account,
) : ViewModelProvider.Factory { ) : ViewModelProvider.Factory {
@Suppress("UNCHECKED_CAST") @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 * 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. * 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.ExperimentalFoundationApi
import androidx.compose.foundation.layout.Column 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.feeds.WatchScrollToTop
import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav
import com.vitorpamplona.amethyst.commons.ui.note.NoteCompose 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.DividerThickness
import com.vitorpamplona.amethyst.commons.ui.theme.FeedPadding import com.vitorpamplona.amethyst.commons.ui.theme.FeedPadding
import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel 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 @Composable
fun TabNotesNewThreads( fun TabNotesNewThreads(
@@ -18,7 +18,7 @@
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION * 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. * 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.AdditiveFeedFilter
import com.vitorpamplona.amethyst.commons.feeds.isRenderableRepost 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 * 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. * 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.compose.runtime.Stable
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.viewmodel.CreationExtras
import com.vitorpamplona.amethyst.commons.model.Account import com.vitorpamplona.amethyst.commons.model.Account
import com.vitorpamplona.amethyst.commons.model.User import com.vitorpamplona.amethyst.commons.model.User
import com.vitorpamplona.amethyst.commons.ui.screen.AndroidFeedViewModel import com.vitorpamplona.amethyst.commons.ui.screen.AndroidFeedViewModel
import kotlin.reflect.KClass
@Stable @Stable
class UserProfileNewThreadsFeedViewModel( class UserProfileNewThreadsFeedViewModel(
@@ -37,6 +39,9 @@ class UserProfileNewThreadsFeedViewModel(
val account: Account, val account: Account,
) : ViewModelProvider.Factory { ) : ViewModelProvider.Factory {
@Suppress("UNCHECKED_CAST") @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 * 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. * 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.Composable
import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue 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.ClinkPointerParser
import com.vitorpamplona.quartz.experimental.clink.pointers.NOffer import com.vitorpamplona.quartz.experimental.clink.pointers.NOffer
import com.vitorpamplona.quartz.nip05DnsIdentifiers.Nip05Id import com.vitorpamplona.quartz.nip05DnsIdentifiers.Nip05Id
import com.vitorpamplona.quartz.utils.cache.ConcurrentLruCache
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.IO
import kotlinx.coroutines.withContext 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 * 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 * 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 * 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( private class ResolvedClinkOffer(
val noffer: NOffer?, 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` * 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 * 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. * 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.BorderStroke
import androidx.compose.foundation.combinedClickable import androidx.compose.foundation.combinedClickable
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Arrangement
@@ -59,7 +58,6 @@ import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalClipboard import androidx.compose.ui.platform.LocalClipboard
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.input.KeyboardCapitalization import androidx.compose.ui.text.input.KeyboardCapitalization
import androidx.compose.ui.text.input.KeyboardType 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_no_methods
import com.vitorpamplona.amethyst.commons.resources.send_payment_receipt_section 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.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.components.util.setText
import com.vitorpamplona.amethyst.commons.ui.insets.imePaddingSafe import com.vitorpamplona.amethyst.commons.ui.insets.imePaddingSafe
import com.vitorpamplona.amethyst.commons.ui.stringRes 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.ui.theme.placeholderText
import com.vitorpamplona.amethyst.commons.util.showAmount import com.vitorpamplona.amethyst.commons.util.showAmount
import com.vitorpamplona.quartz.nip57Zaps.ZapReceiptEvent import com.vitorpamplona.quartz.nip57Zaps.ZapReceiptEvent
import com.vitorpamplona.quartz.utils.BigDecimal
import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.persistentListOf import kotlinx.collections.immutable.persistentListOf
import kotlinx.collections.immutable.toImmutableList import kotlinx.collections.immutable.toImmutableList
@@ -469,7 +469,7 @@ private fun MethodChip(
enabled: Boolean, enabled: Boolean,
onSelect: () -> Unit, onSelect: () -> Unit,
) { ) {
val context = LocalContext.current val shortNotice = rememberShortNotice()
val clipboard = LocalClipboard.current val clipboard = LocalClipboard.current
val scope = rememberCoroutineScope() val scope = rememberCoroutineScope()
val copyLabel = stringRes(Res.string.copy_to_clipboard) val copyLabel = stringRes(Res.string.copy_to_clipboard)
@@ -493,7 +493,7 @@ private fun MethodChip(
{ {
scope.launch { scope.launch {
clipboard.setText(value) clipboard.setText(value)
Toast.makeText(context, copiedMessage, Toast.LENGTH_SHORT).show() shortNotice.show(copiedMessage)
} }
} }
}, },
@@ -596,7 +596,7 @@ private fun AmountSection(
SuggestionChip( SuggestionChip(
enabled = enabled, enabled = enabled,
onClick = { onAmountChange(amount.toString()) }, 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 * 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. * 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.Arrangement
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
@@ -40,7 +40,6 @@ import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.lifecycle.compose.collectAsStateWithLifecycle 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_private_explainer
import com.vitorpamplona.amethyst.commons.resources.zap_type_public import com.vitorpamplona.amethyst.commons.resources.zap_type_public
import com.vitorpamplona.amethyst.commons.resources.zap_type_public_explainer 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.nwcFailureDetail
import com.vitorpamplona.amethyst.commons.service.nwc.nwcTimeoutMessage import com.vitorpamplona.amethyst.commons.service.nwc.nwcTimeoutMessage
import com.vitorpamplona.amethyst.commons.ui.loadStringRes 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.FeeTier
import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.wallet.rateFor import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.wallet.rateFor
import com.vitorpamplona.amethyst.commons.ui.stringRes 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.util.showAmount
import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel 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.common.SatRange
import com.vitorpamplona.quartz.experimental.clink.offers.OfferErrorCode import com.vitorpamplona.quartz.experimental.clink.offers.OfferErrorCode
import com.vitorpamplona.quartz.experimental.clink.pointers.ClinkPointerParser 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.chain.FeeEstimates
import com.vitorpamplona.quartz.nipBCOnchainZaps.taproot.SegwitAddress import com.vitorpamplona.quartz.nipBCOnchainZaps.taproot.SegwitAddress
import com.vitorpamplona.quartz.nipBCOnchainZaps.taproot.TaprootAddress import com.vitorpamplona.quartz.nipBCOnchainZaps.taproot.TaprootAddress
import com.vitorpamplona.quartz.utils.BigDecimal
import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.persistentListOf import kotlinx.collections.immutable.persistentListOf
import kotlinx.collections.immutable.toImmutableList import kotlinx.collections.immutable.toImmutableList
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.IO
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
@@ -180,7 +183,7 @@ private fun SendPaymentLoaded(
nav: INav, nav: INav,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
) { ) {
val context = LocalContext.current val walletLauncher = rememberWalletAppLauncher()
val scope = rememberCoroutineScope() val scope = rememberCoroutineScope()
val userInfo by observeUserInfo(user, accountViewModel) val userInfo by observeUserInfo(user, accountViewModel)
@@ -368,7 +371,7 @@ private fun SendPaymentLoaded(
selectedMethod == ProfilePaymentMethod.CASHU -> selectedMethod == ProfilePaymentMethod.CASHU ->
stringRes( stringRes(
Res.string.send_payment_cashu_balance, 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 -> selectedMethod == ProfilePaymentMethod.CLINK && clinkRange?.min != null && clinkRange?.max != null ->
stringRes(Res.string.clink_offer_amount_range, clinkRange?.min.toString(), clinkRange?.max.toString()) 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 // startActivity belongs on Main. sendSats now delivers onNewInvoice
// there already; the hop also covers any other caller of payBolt11. // there already; the hop also covers any other caller of payBolt11.
scope.launch { scope.launch {
payViaIntent( walletLauncher.payInvoice(
invoice, invoice,
context,
noWalletFoundStr, noWalletFoundStr,
onPaid = { scope.launch { postStage(PaymentFlowStage.Success(successTitle, sentToWalletLabel)) } }, onPaid = { scope.launch { postStage(PaymentFlowStage.Success(successTitle, sentToWalletLabel)) } },
onError = { scope.launch { postStage(PaymentFlowStage.Failure(it)) } }, onError = { scope.launch { postStage(PaymentFlowStage.Failure(it)) } },
@@ -664,7 +666,7 @@ private fun SendPaymentLoaded(
canSend = canSend, canSend = canSend,
sendLabel = sendLabel =
if (amountSats != null && amountSats > 0) { 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 { } else {
stringRes(Res.string.send_payment_pay_button_empty) stringRes(Res.string.send_payment_pay_button_empty)
}, },
@@ -800,7 +802,7 @@ private fun OnchainFeeSection(
stringRes( stringRes(
Res.string.onchain_send_fee_tier_label_rate_eta, Res.string.onchain_send_fee_tier_label_rate_eta,
stringRes(tier.labelRes), stringRes(tier.labelRes),
"%.1f".format(rate), DecimalPatternFormatter("0.0").format(rate),
stringRes(tier.etaLabelRes), stringRes(tier.etaLabelRes),
) )
} else { } else {
@@ -18,7 +18,7 @@
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION * 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. * 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.feeds.FeedFilter
import com.vitorpamplona.amethyst.commons.model.Account 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 * 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. * 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.compose.runtime.Stable
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.viewmodel.CreationExtras
import com.vitorpamplona.amethyst.commons.model.Account import com.vitorpamplona.amethyst.commons.model.Account
import com.vitorpamplona.amethyst.commons.model.User import com.vitorpamplona.amethyst.commons.model.User
import com.vitorpamplona.amethyst.commons.ui.screen.AndroidFeedViewModel import com.vitorpamplona.amethyst.commons.ui.screen.AndroidFeedViewModel
import kotlin.reflect.KClass
@Stable @Stable
class UserProfilePinnedNotesFeedViewModel( class UserProfilePinnedNotesFeedViewModel(
@@ -37,6 +39,9 @@ class UserProfilePinnedNotesFeedViewModel(
val account: Account, val account: Account,
) : ViewModelProvider.Factory { ) : ViewModelProvider.Factory {
@Suppress("UNCHECKED_CAST") @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 * 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. * 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.PaddingValues
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
@@ -18,7 +18,7 @@
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION * 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. * 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.Column
import androidx.compose.foundation.layout.fillMaxHeight import androidx.compose.foundation.layout.fillMaxHeight
@@ -28,8 +28,8 @@ import androidx.compose.ui.Modifier
import androidx.lifecycle.Lifecycle import androidx.lifecycle.Lifecycle
import androidx.lifecycle.LifecycleEventObserver import androidx.lifecycle.LifecycleEventObserver
import androidx.lifecycle.compose.LocalLifecycleOwner import androidx.lifecycle.compose.LocalLifecycleOwner
import androidx.lifecycle.viewmodel.compose.viewModel
import com.vitorpamplona.amethyst.commons.model.User 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.ui.navigation.navs.INav
import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel
import com.vitorpamplona.amethyst.commons.viewmodels.RelayFeedViewModel import com.vitorpamplona.amethyst.commons.viewmodels.RelayFeedViewModel
@@ -40,7 +40,7 @@ fun TabRelays(
accountViewModel: AccountViewModel, accountViewModel: AccountViewModel,
nav: INav, nav: INav,
) { ) {
val feedViewModel: RelayFeedViewModel = viewModel() val feedViewModel: RelayFeedViewModel = rememberViewModel { RelayFeedViewModel() }
val lifeCycleOwner = LocalLifecycleOwner.current val lifeCycleOwner = LocalLifecycleOwner.current
@@ -18,7 +18,7 @@
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION * 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. * 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.material3.Text
import androidx.compose.runtime.Composable 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.Res
import com.vitorpamplona.amethyst.commons.resources.number_reports import com.vitorpamplona.amethyst.commons.resources.number_reports
import com.vitorpamplona.amethyst.commons.resources.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.ui.stringRes
import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel
import com.vitorpamplona.amethyst.ui.screen.loggedIn.profile.reports.dal.UserProfileReportFeedViewModel
@Composable @Composable
fun ReportsTabHeader( fun ReportsTabHeader(
@@ -18,7 +18,7 @@
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION * 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. * 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.Column
import androidx.compose.foundation.layout.Row 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.model.User
import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav
import com.vitorpamplona.amethyst.commons.ui.note.NoteCompose 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.DividerThickness
import com.vitorpamplona.amethyst.commons.ui.theme.FeedPadding import com.vitorpamplona.amethyst.commons.ui.theme.FeedPadding
import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel
import com.vitorpamplona.amethyst.ui.screen.loggedIn.profile.reports.dal.UserProfileReportFeedViewModel
@Composable @Composable
fun TabReports( fun TabReports(
@@ -18,22 +18,25 @@
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION * 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. * 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.compose.runtime.Stable
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import androidx.lifecycle.viewmodel.CreationExtras
import com.vitorpamplona.amethyst.commons.model.Account import com.vitorpamplona.amethyst.commons.model.Account
import com.vitorpamplona.amethyst.commons.model.Note import com.vitorpamplona.amethyst.commons.model.Note
import com.vitorpamplona.amethyst.commons.model.User import com.vitorpamplona.amethyst.commons.model.User
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.IO
import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.flowOn import kotlinx.coroutines.flow.flowOn
import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.sample import kotlinx.coroutines.flow.sample
import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.flow.stateIn
import kotlin.reflect.KClass
@Stable @Stable
class UserProfileReportFeedViewModel( class UserProfileReportFeedViewModel(
@@ -76,6 +79,9 @@ class UserProfileReportFeedViewModel(
val account: Account, val account: Account,
) : ViewModelProvider.Factory { ) : ViewModelProvider.Factory {
@Suppress("UNCHECKED_CAST") @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 * 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. * 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.Column
import androidx.compose.foundation.layout.fillMaxHeight import androidx.compose.foundation.layout.fillMaxHeight
@@ -32,10 +32,10 @@ import androidx.compose.ui.Modifier
import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.vitorpamplona.amethyst.commons.model.User import com.vitorpamplona.amethyst.commons.model.User
import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav 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.DividerThickness
import com.vitorpamplona.amethyst.commons.ui.theme.FeedPadding import com.vitorpamplona.amethyst.commons.ui.theme.FeedPadding
import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel
import com.vitorpamplona.amethyst.ui.screen.loggedIn.profile.zaps.dal.UserProfileZapsViewModel
@Composable @Composable
fun TabReceivedZaps( fun TabReceivedZaps(
@@ -18,7 +18,7 @@
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION * 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. * 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.clickable
import androidx.compose.foundation.layout.Arrangement 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.UserActionOptions
import com.vitorpamplona.amethyst.commons.ui.note.UserPicture import com.vitorpamplona.amethyst.commons.ui.note.UserPicture
import com.vitorpamplona.amethyst.commons.ui.note.UsernameDisplay 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.BitcoinOrange
import com.vitorpamplona.amethyst.commons.ui.theme.Size55dp import com.vitorpamplona.amethyst.commons.ui.theme.Size55dp
import com.vitorpamplona.amethyst.commons.ui.theme.ThemeComparisonColumn import com.vitorpamplona.amethyst.commons.ui.theme.ThemeComparisonColumn
import com.vitorpamplona.amethyst.commons.util.showAmountInteger import com.vitorpamplona.amethyst.commons.util.showAmountInteger
import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel
import com.vitorpamplona.amethyst.commons.viewmodels.mockAccountViewModel import com.vitorpamplona.amethyst.commons.viewmodels.mockAccountViewModel
import com.vitorpamplona.amethyst.ui.screen.loggedIn.profile.zaps.dal.ZapAmount
import com.vitorpamplona.quartz.utils.BigDecimal import com.vitorpamplona.quartz.utils.BigDecimal
@Composable @Composable
@@ -77,7 +77,7 @@ fun RenderZapNoteSlimPreview() {
ThemeComparisonColumn { ThemeComparisonColumn {
RenderZapNoteSlim( RenderZapNoteSlim(
ZapAmount(user1, BigDecimal.TEN), ZapAmount(user1, BigDecimal(10)),
accountViewModel, accountViewModel,
EmptyNav(), EmptyNav(),
) )
@@ -18,7 +18,7 @@
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION * 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. * 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.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
@@ -26,10 +26,10 @@ import androidx.compose.runtime.getValue
import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.vitorpamplona.amethyst.commons.resources.Res import com.vitorpamplona.amethyst.commons.resources.Res
import com.vitorpamplona.amethyst.commons.resources.zaps 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.ui.stringRes
import com.vitorpamplona.amethyst.commons.util.showAmountInteger import com.vitorpamplona.amethyst.commons.util.showAmountInteger
import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel
import com.vitorpamplona.amethyst.ui.screen.loggedIn.profile.zaps.dal.UserProfileZapsViewModel
@Composable @Composable
fun ZapTabHeader( fun ZapTabHeader(
@@ -18,13 +18,14 @@
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION * 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. * 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.Immutable
import androidx.compose.runtime.Stable import androidx.compose.runtime.Stable
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import androidx.lifecycle.viewmodel.CreationExtras
import com.vitorpamplona.amethyst.commons.model.Account import com.vitorpamplona.amethyst.commons.model.Account
import com.vitorpamplona.amethyst.commons.model.User import com.vitorpamplona.amethyst.commons.model.User
import com.vitorpamplona.amethyst.commons.model.cache.LocalCache 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.nipB1Bolt12Zaps.zap.Bolt12ZapEvent
import com.vitorpamplona.quartz.nipBCOnchainZaps.zap.OnchainZapEvent import com.vitorpamplona.quartz.nipBCOnchainZaps.zap.OnchainZapEvent
import com.vitorpamplona.quartz.utils.BigDecimal import com.vitorpamplona.quartz.utils.BigDecimal
import com.vitorpamplona.quartz.utils.plus
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.IO
import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.flowOn import kotlinx.coroutines.flow.flowOn
import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.sample import kotlinx.coroutines.flow.sample
import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.flow.stateIn
import kotlin.reflect.KClass
@Immutable @Immutable
data class ZapAmount( data class ZapAmount(
@@ -59,14 +63,15 @@ class UserProfileZapsViewModel(
tags = mapOf("p" to listOf(user.pubkeyHex)), 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> = 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? { suspend fun mapRequest(zapEvent: ZapReceiptEvent): ZapAmount? {
val zapRequest = val zapRequest =
zapEvent.zapRequest ?: return ZapAmount( zapEvent.zapRequest ?: return ZapAmount(
LocalCache.getOrCreateUser(zapEvent.pubKey), LocalCache.getOrCreateUser(zapEvent.pubKey),
zapEvent.amount ?: BigDecimal.ZERO, zapEvent.amount ?: BigDecimal(0),
) )
return if (zapRequest.isPrivateZap()) { return if (zapRequest.isPrivateZap()) {
@@ -76,24 +81,24 @@ class UserProfileZapsViewModel(
if (cachedPrivateRequest != null) { if (cachedPrivateRequest != null) {
ZapAmount( ZapAmount(
LocalCache.getOrCreateUser(cachedPrivateRequest.pubKey), LocalCache.getOrCreateUser(cachedPrivateRequest.pubKey),
zapEvent.amount ?: BigDecimal.ZERO, zapEvent.amount ?: BigDecimal(0),
) )
} else { } else {
ZapAmount( ZapAmount(
LocalCache.getOrCreateUser(zapRequest.pubKey), LocalCache.getOrCreateUser(zapRequest.pubKey),
zapEvent.amount ?: BigDecimal.ZERO, zapEvent.amount ?: BigDecimal(0),
) )
} }
} else { } else {
ZapAmount( ZapAmount(
LocalCache.getOrCreateUser(zapRequest.pubKey), LocalCache.getOrCreateUser(zapRequest.pubKey),
zapEvent.amount ?: BigDecimal.ZERO, zapEvent.amount ?: BigDecimal(0),
) )
} }
} else { } else {
ZapAmount( ZapAmount(
LocalCache.getOrCreateUser(zapRequest.pubKey), LocalCache.getOrCreateUser(zapRequest.pubKey),
zapEvent.amount ?: BigDecimal.ZERO, zapEvent.amount ?: BigDecimal(0),
) )
} }
} }
@@ -128,7 +133,7 @@ class UserProfileZapsViewModel(
else -> null else -> null
} }
if (zapAmount != null) { if (zapAmount != null) {
val existingAmount = results[zapAmount.user] ?: BigDecimal.ZERO val existingAmount = results[zapAmount.user] ?: BigDecimal(0)
results[zapAmount.user] = existingAmount + zapAmount.amount results[zapAmount.user] = existingAmount + zapAmount.amount
} }
} }
@@ -152,11 +157,11 @@ class UserProfileZapsViewModel(
val totalReceivedZaps = val totalReceivedZaps =
receivedZapAmountsByUser receivedZapAmountsByUser
.map { it.sumOf { it.amount } } .map { amounts -> amounts.fold(BigDecimal(0)) { total, zap -> total + zap.amount } }
.flowOn(Dispatchers.IO) .flowOn(Dispatchers.IO)
.stateIn( .stateIn(
viewModelScope, viewModelScope,
initialValue = BigDecimal.ZERO, initialValue = BigDecimal(0),
started = SharingStarted.Lazily, started = SharingStarted.Lazily,
) )
@@ -165,6 +170,9 @@ class UserProfileZapsViewModel(
val account: Account, val account: Account,
) : ViewModelProvider.Factory { ) : ViewModelProvider.Factory {
@Suppress("UNCHECKED_CAST") @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 * 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. * 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.background
import androidx.compose.foundation.border import androidx.compose.foundation.border
import androidx.compose.foundation.clickable import androidx.compose.foundation.clickable
@@ -53,7 +52,6 @@ import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.semantics.contentDescription import androidx.compose.ui.semantics.contentDescription
import androidx.compose.ui.semantics.semantics import androidx.compose.ui.semantics.semantics
import androidx.compose.ui.text.font.FontFamily 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.tooling.preview.Preview
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp 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.Icon
import com.vitorpamplona.amethyst.commons.icons.symbols.MaterialSymbol import com.vitorpamplona.amethyst.commons.icons.symbols.MaterialSymbol
import com.vitorpamplona.amethyst.commons.icons.symbols.MaterialSymbols 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.EmptyNav
import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav
import com.vitorpamplona.amethyst.commons.ui.navigation.topbars.TopBarWithBackButton 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.SegmentedChoiceTile
import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.settings.SettingsBlockTile import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.settings.SettingsBlockTile
import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.settings.SettingsControlRow 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 com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel
import kotlinx.collections.immutable.ImmutableMap import kotlinx.collections.immutable.ImmutableMap
import kotlinx.collections.immutable.toImmutableList import kotlinx.collections.immutable.toImmutableList
import kotlinx.collections.immutable.toImmutableMap
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
import org.jetbrains.compose.resources.StringResource import org.jetbrains.compose.resources.StringResource
import org.xmlpull.v1.XmlPullParser
import org.xmlpull.v1.XmlPullParserException
import java.io.IOException
@Composable @Composable
fun SettingsScreen( 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. // matches the app's other "opens a picker" rows instead of leaving a lone text-field dropdown here.
@Composable @Composable
private fun LanguageTile(sharedPrefs: UiSettingsFlow) { private fun LanguageTile(sharedPrefs: UiSettingsFlow) {
val context = LocalContext.current val languageEntries = LocalAppPlatform.current.rememberAppLanguages()
val languageEntries = remember { context.getLangPreferenceDropdownEntries() }
val languageList = remember { languageEntries.keys.map { TitleExplainer(it) }.toImmutableList() } val languageList = remember { languageEntries.keys.map { TitleExplainer(it) }.toImmutableList() }
val language by sharedPrefs.preferredLanguage.collectAsState() 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( fun getLanguageIndex(
languageEntries: ImmutableMap<String, String>, languageEntries: ImmutableMap<String, String>,
language: String?, language: String?,
@@ -18,7 +18,7 @@
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION * 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. * 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.Column
import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxSize
@@ -33,34 +33,28 @@ import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.vitorpamplona.amethyst.Amethyst
import com.vitorpamplona.amethyst.commons.model.preferences.NamecoinSettingsStore import com.vitorpamplona.amethyst.commons.model.preferences.NamecoinSettingsStore
import com.vitorpamplona.amethyst.commons.resources.Res import com.vitorpamplona.amethyst.commons.resources.Res
import com.vitorpamplona.amethyst.commons.resources.namecoin_settings 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.navs.INav
import com.vitorpamplona.amethyst.commons.ui.navigation.topbars.TopBarWithBackButton import com.vitorpamplona.amethyst.commons.ui.navigation.topbars.TopBarWithBackButton
import com.vitorpamplona.amethyst.commons.ui.platform.LocalAppServices
import com.vitorpamplona.amethyst.commons.ui.stringRes import com.vitorpamplona.amethyst.commons.ui.stringRes
import com.vitorpamplona.quartz.nip05DnsIdentifiers.namecoin.ElectrumXClient
import com.vitorpamplona.quartz.nip05DnsIdentifiers.namecoin.NamecoinCoreRpcClient
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
@OptIn(ExperimentalMaterial3Api::class) @OptIn(ExperimentalMaterial3Api::class)
@Composable @Composable
fun NamecoinSettingsScreen(nav: INav) { fun NamecoinSettingsScreen(nav: INav) {
NamecoinSettingsScreen( val services = LocalAppServices.current
Amethyst.instance.namecoinPrefs, NamecoinSettingsScreen(services.namecoinSettings, services.namecoinClients, nav)
electrumXClient = { Amethyst.instance.electrumXClient },
namecoinCoreRpcClient = { Amethyst.instance.namecoinCoreRpcClient },
nav,
)
} }
@OptIn(ExperimentalMaterial3Api::class) @OptIn(ExperimentalMaterial3Api::class)
@Composable @Composable
fun NamecoinSettingsScreen( fun NamecoinSettingsScreen(
namecoinPrefs: NamecoinSettingsStore, namecoinPrefs: NamecoinSettingsStore,
electrumXClient: () -> ElectrumXClient, clients: NamecoinClients,
namecoinCoreRpcClient: () -> NamecoinCoreRpcClient,
nav: INav, nav: INav,
) { ) {
val namecoinSettings by namecoinPrefs.settings.collectAsStateWithLifecycle() val namecoinSettings by namecoinPrefs.settings.collectAsStateWithLifecycle()
@@ -92,14 +86,13 @@ fun NamecoinSettingsScreen(
onReset = { onReset = {
scope.launch { namecoinPrefs.reset() } scope.launch { namecoinPrefs.reset() }
}, },
onTestServer = { server -> electrumXClient().testServer(server) }, onTestServer = { server -> clients.testElectrumxServer(server) },
onPinCert = { pem -> onPinCert = { pem ->
scope.launch { scope.launch {
namecoinPrefs.addPinnedCert(pem) namecoinPrefs.addPinnedCert(pem)
// Share the pinned PEM across both backends so the // Share the pinned PEM across both backends so the
// user only has to confirm a self-signed cert once. // user only has to confirm a self-signed cert once.
electrumXClient().addPinnedCert(pem) clients.addPinnedCert(pem)
namecoinCoreRpcClient().addPinnedCert(pem)
} }
}, },
onSetBackend = { backend -> onSetBackend = { backend ->
@@ -108,7 +101,7 @@ fun NamecoinSettingsScreen(
onSetCoreRpcConfig = { cfg -> onSetCoreRpcConfig = { cfg ->
scope.launch { scope.launch {
namecoinPrefs.setCoreRpcConfig(cfg) namecoinPrefs.setCoreRpcConfig(cfg)
namecoinCoreRpcClient().setConfig(cfg) clients.setCoreRpcConfig(cfg)
} }
}, },
onSetFallbackToCustomElectrumx = { enabled -> onSetFallbackToCustomElectrumx = { enabled ->
@@ -117,7 +110,7 @@ fun NamecoinSettingsScreen(
onSetFallbackToDefaultElectrumx = { enabled -> onSetFallbackToDefaultElectrumx = { enabled ->
scope.launch { namecoinPrefs.setFallbackToDefaultElectrumx(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 * 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. * 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.AnimatedVisibility
import androidx.compose.animation.expandVertically import androidx.compose.animation.expandVertically
import androidx.compose.animation.shrinkVertically import androidx.compose.animation.shrinkVertically
@@ -60,7 +59,6 @@ import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalSoftwareKeyboardController import androidx.compose.ui.platform.LocalSoftwareKeyboardController
import androidx.compose.ui.text.font.FontFamily import androidx.compose.ui.text.font.FontFamily
import androidx.compose.ui.text.font.FontWeight 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.text.style.TextOverflow
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp 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.Icon
import com.vitorpamplona.amethyst.commons.icons.symbols.MaterialSymbols import com.vitorpamplona.amethyst.commons.icons.symbols.MaterialSymbols
import com.vitorpamplona.amethyst.commons.model.nip05DnsIdentifiers.namecoin.NamecoinSettings 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.DateTimeStyle
import com.vitorpamplona.amethyst.commons.ui.note.formatDateTime import com.vitorpamplona.amethyst.commons.ui.note.formatDateTime
import com.vitorpamplona.amethyst.commons.ui.stringRes 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.DEFAULT_ELECTRUMX_SERVERS
import com.vitorpamplona.quartz.nip05DnsIdentifiers.namecoin.ElectrumxServer import com.vitorpamplona.quartz.nip05DnsIdentifiers.namecoin.ElectrumxServer
import com.vitorpamplona.quartz.nip05DnsIdentifiers.namecoin.NamecoinBackend import com.vitorpamplona.quartz.nip05DnsIdentifiers.namecoin.NamecoinBackend
import com.vitorpamplona.quartz.nip05DnsIdentifiers.namecoin.NamecoinCoreRpcConfig import com.vitorpamplona.quartz.nip05DnsIdentifiers.namecoin.NamecoinCoreRpcConfig
import com.vitorpamplona.quartz.nip05DnsIdentifiers.namecoin.RpcProbeResult import com.vitorpamplona.quartz.nip05DnsIdentifiers.namecoin.RpcProbeResult
import com.vitorpamplona.quartz.nip05DnsIdentifiers.namecoin.ServerTestResult import com.vitorpamplona.quartz.nip05DnsIdentifiers.namecoin.ServerTestResult
import com.vitorpamplona.quartz.utils.currentTimeMillis
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import java.util.Locale import kotlin.math.roundToLong
/** /**
* Complete settings section for Namecoin ElectrumX server configuration. * Complete settings section for Namecoin ElectrumX server configuration.
@@ -349,7 +350,7 @@ private fun TestConnectionSection(
) )
} }
} }
lastTestTimestamp = System.currentTimeMillis() lastTestTimestamp = currentTimeMillis()
isTesting = false isTesting = false
// Show confirmation dialog for each new cert // Show confirmation dialog for each new cert
if (newCerts.isNotEmpty()) { if (newCerts.isNotEmpty()) {
@@ -512,9 +513,8 @@ private fun DiagnosticCard(
// Last test timestamp // Last test timestamp
if (lastTestTimestamp != null) { if (lastTestTimestamp != null) {
val context = LocalContext.current
val formatted = val formatted =
remember(lastTestTimestamp, context) { remember(lastTestTimestamp) {
formatDateTime(lastTestTimestamp, DateTimeStyle.MEDIUM, DateTimeStyle.SHORT) formatDateTime(lastTestTimestamp, DateTimeStyle.MEDIUM, DateTimeStyle.SHORT)
} }
val successCount = testResults.count { it.success } val successCount = testResults.count { it.success }
@@ -535,7 +535,7 @@ private fun DiagnosticCard(
// Device info // Device info
DiagnosticRow( DiagnosticRow(
label = stringRes(Res.string.namecoin_device_info), 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)) Spacer(Modifier.height(4.dp))
@@ -1108,13 +1108,7 @@ private fun NamecoinCoreRpcSection(
!candidate.usePinnedTrustStore !candidate.usePinnedTrustStore
) )
) { ) {
val host = val host = OmniboxInput.hostOf(candidate.url) ?: candidate.url
try {
java.net.URI(candidate.url).host
?: candidate.url
} catch (_: Exception) {
candidate.url
}
pendingPin = pendingPin =
PendingCertPin( PendingCertPin(
serverHost = host, serverHost = host,
@@ -1167,7 +1161,7 @@ private fun NamecoinCoreRpcSection(
val chain = probe.chain ?: "?" val chain = probe.chain ?: "?"
val blocks = probe.blocks?.toString() ?: "?" val blocks = probe.blocks?.toString() ?: "?"
val pct = val pct =
probe.verificationProgress?.let { String.format(Locale.ROOT, "%.2f%%", it * 100) } probe.verificationProgress?.let { percentTwoDecimals(it) }
?: "?" ?: "?"
Text( Text(
"chain=$chain height=$blocks sync=$pct" + "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 * 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. * 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.Column
import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxSize
@@ -33,7 +33,6 @@ import androidx.compose.runtime.getValue
import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import com.vitorpamplona.amethyst.Amethyst
import com.vitorpamplona.amethyst.commons.model.preferences.OtsSettingsStore import com.vitorpamplona.amethyst.commons.model.preferences.OtsSettingsStore
import com.vitorpamplona.amethyst.commons.resources.Res import com.vitorpamplona.amethyst.commons.resources.Res
import com.vitorpamplona.amethyst.commons.resources.ots_explorer_settings 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.tor.TorType
import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav
import com.vitorpamplona.amethyst.commons.ui.navigation.topbars.TopBarWithBackButton import com.vitorpamplona.amethyst.commons.ui.navigation.topbars.TopBarWithBackButton
import com.vitorpamplona.amethyst.commons.ui.platform.LocalAppServices
import com.vitorpamplona.amethyst.commons.ui.stringRes import com.vitorpamplona.amethyst.commons.ui.stringRes
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
@OptIn(ExperimentalMaterial3Api::class) @OptIn(ExperimentalMaterial3Api::class)
@Composable @Composable
fun OtsSettingsScreen(nav: INav) { 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) @OptIn(ExperimentalMaterial3Api::class)
@@ -18,7 +18,7 @@
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION * 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. * 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.background
import androidx.compose.foundation.border 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.resources.save
import com.vitorpamplona.amethyst.commons.ui.stringRes import com.vitorpamplona.amethyst.commons.ui.stringRes
import com.vitorpamplona.amethyst.commons.ui.theme.BitcoinOrange 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. * Settings section for configuring the blockchain explorer used for OTS verification.
@@ -180,7 +180,7 @@ private fun ActiveExplorerDisplay(
val isCustom = settings.hasCustomExplorer val isCustom = settings.hasCustomExplorer
val activeUrl = val activeUrl =
settings.normalizedUrl() settings.normalizedUrl()
?: if (isTorActive) OkHttpBitcoinExplorer.MEMPOOL_API_URL else OkHttpBitcoinExplorer.BLOCKSTREAM_API_URL ?: if (isTorActive) BitcoinExplorerUrls.MEMPOOL else BitcoinExplorerUrls.BLOCKSTREAM
Column { Column {
Row( Row(
@@ -18,9 +18,8 @@
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION * 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. * 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.background
import androidx.compose.foundation.border import androidx.compose.foundation.border
import androidx.compose.foundation.clickable import androidx.compose.foundation.clickable
@@ -56,14 +55,12 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.text.font.FontFamily import androidx.compose.ui.text.font.FontFamily
import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.compose.collectAsStateWithLifecycle
import coil3.compose.AsyncImage import coil3.compose.AsyncImage
import com.vitorpamplona.amethyst.Amethyst
import com.vitorpamplona.amethyst.commons.connectedApps.nip46.Nip46ClientInfo import com.vitorpamplona.amethyst.commons.connectedApps.nip46.Nip46ClientInfo
import com.vitorpamplona.amethyst.commons.connectedApps.nip46.Nip46PermissionAuthorizer import com.vitorpamplona.amethyst.commons.connectedApps.nip46.Nip46PermissionAuthorizer
import com.vitorpamplona.amethyst.commons.connectedApps.signers.AppSignerPolicy 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_manage_apps
import com.vitorpamplona.amethyst.commons.resources.nip46_signer_reconnecting import com.vitorpamplona.amethyst.commons.resources.nip46_signer_reconnecting
import com.vitorpamplona.amethyst.commons.resources.nip46_signer_remote_app 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.navs.INav
import com.vitorpamplona.amethyst.commons.ui.navigation.topbars.TopBarWithBackButton 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.pluralStringRes
import com.vitorpamplona.amethyst.commons.ui.stringRes import com.vitorpamplona.amethyst.commons.ui.stringRes
import com.vitorpamplona.amethyst.commons.util.toTimeAgo
import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel
import com.vitorpamplona.quartz.nip01Core.core.HexKey import com.vitorpamplona.quartz.nip01Core.core.HexKey
import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl
@@ -125,7 +125,8 @@ fun Nip46ConnectedAppsScreen(
val reconnectingStr = stringRes(Res.string.nip46_signer_reconnecting) val reconnectingStr = stringRes(Res.string.nip46_signer_reconnecting)
val account = accountViewModel.account val account = accountViewModel.account
val signerPubKey = remember { account.signer.pubKey } 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 // 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. // 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) } var refreshKey by remember { mutableIntStateOf(0) }
LaunchedEffect(refreshKey) { LaunchedEffect(refreshKey) {
items = withContext(Dispatchers.Default) { loadNip46Apps(signerPubKey) } items = withContext(Dispatchers.Default) { loadNip46Apps(services, signerPubKey) }
} }
Scaffold( Scaffold(
@@ -187,7 +188,7 @@ fun Nip46ConnectedAppsScreen(
online = online, online = online,
onReconnect = { onReconnect = {
account.client.reconnect(ignoreRetryDelays = true) account.client.reconnect(ignoreRetryDelays = true)
Toast.makeText(context, reconnectingStr, Toast.LENGTH_SHORT).show() shortNotice.show(reconnectingStr)
}, },
onClick = { nav.nav(Route.ConnectedAppDetail(entry.coordinate)) }, onClick = { nav.nav(Route.ConnectedAppDetail(entry.coordinate)) },
) )
@@ -241,7 +242,7 @@ private fun Nip46AppCard(
val meta = val meta =
buildList { buildList {
if (relayCount > 0) add(pluralStringRes(Res.plurals.nip46_signer_app_relay_count, relayCount, relayCount)) 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(" · ") }.joinToString(" · ")
if (meta.isNotEmpty()) { if (meta.isNotEmpty()) {
Text( Text(
@@ -291,7 +292,7 @@ private val LiveGreen = Color(0xFF3DDC84)
/** A colored dot: green when the signer currently reaches the relay(s), muted otherwise. */ /** A colored dot: green when the signer currently reaches the relay(s), muted otherwise. */
@Composable @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)) 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. * pill inside a clickable card doesn't also open the card.
*/ */
@Composable @Composable
internal fun Nip46ReconnectPill(onClick: () -> Unit) { fun Nip46ReconnectPill(onClick: () -> Unit) {
Text( Text(
stringRes(Res.string.nip46_signer_app_reconnect), stringRes(Res.string.nip46_signer_app_reconnect),
style = MaterialTheme.typography.labelSmall, 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. */ /** A [Nip46StatusDot] + Connected/Offline label showing whether the signer currently reaches an app's relays. */
@Composable @Composable
internal fun Nip46LiveStatus(online: Boolean) { fun Nip46LiveStatus(online: Boolean) {
Row(verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(6.dp)) { Row(verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(6.dp)) {
Nip46StatusDot(online) Nip46StatusDot(online)
Text( 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 * `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]. * one of the app's relays is in [connectedRelays].
*/ */
internal fun nip46AppOnline( fun nip46AppOnline(
appRelays: Set<String>, appRelays: Set<String>,
inboxRelays: Set<NormalizedRelayUrl>, inboxRelays: Set<NormalizedRelayUrl>,
connectedRelays: Set<NormalizedRelayUrl>, connectedRelays: Set<NormalizedRelayUrl>,
@@ -352,7 +353,7 @@ internal fun nip46AppOnline(
* user's IP (see BrowserIconRegistry). * user's IP (see BrowserIconRegistry).
*/ */
@Composable @Composable
internal fun Nip46AppIcon( fun Nip46AppIcon(
image: String?, image: String?,
modifier: Modifier = Modifier, 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 * advertised one, otherwise its npub. Shared by the list card and the detail header so a row and
* the screen it opens always agree. * the screen it opens always agree.
*/ */
internal fun nip46ClientSubtitle( fun nip46ClientSubtitle(
url: String?, url: String?,
clientPubKey: HexKey, clientPubKey: HexKey,
): String { ): String {
@@ -402,9 +403,12 @@ internal fun nip46ClientSubtitle(
return host ?: runCatching { NPub.create(clientPubKey) }.getOrDefault(clientPubKey.take(12) + "…") return host ?: runCatching { NPub.create(clientPubKey) }.getOrDefault(clientPubKey.take(12) + "…")
} }
private suspend fun loadNip46Apps(signerPubKey: HexKey): List<Nip46AppEntry> { private suspend fun loadNip46Apps(
val store = Amethyst.instance.signerPermissionStore services: AppServices,
val clientStore = Amethyst.instance.nip46ClientStore 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 // allPolicies() already carries each app's policy — read it from the map instead of a second
// loadPolicy() call per app. // loadPolicy() call per app.
return store 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() 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 package com.vitorpamplona.quartz.nip03Timestamp.okhttp
import com.vitorpamplona.quartz.nip01Core.jackson.JacksonMapper 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.BitcoinExplorer
import com.vitorpamplona.quartz.nip03Timestamp.ots.BlockHeader import com.vitorpamplona.quartz.nip03Timestamp.ots.BlockHeader
import com.vitorpamplona.quartz.nip03Timestamp.ots.OtsBlockHeightCache import com.vitorpamplona.quartz.nip03Timestamp.ots.OtsBlockHeightCache
@@ -116,9 +117,9 @@ class OkHttpBitcoinExplorer(
companion object { companion object {
// doesn't accept Tor // doesn't accept Tor
const val BLOCKSTREAM_API_URL = "https://blockstream.info/api" const val BLOCKSTREAM_API_URL = BitcoinExplorerUrls.BLOCKSTREAM
// accepts Tor // 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. */ /** 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,
)