refactor: share relay exporters, zap amount text and composer nav seams

- FileSharer expect/actual (Android zips into the cache and shares via the
  FileProvider); RelayExporter/RelayZipExporter/RelayListCollection move to
  commonsUI on TextSharer/FileSharer.
- rememberExtendedNav moves to commonsUI; composer screens take INav.
- ZapAmountText and the NWC finder subscription move to commonsUI;
  QuickZapAmountRow becomes a NotePlatform slot.
- ChannelNewMessageViewModel mines PoW with a monotonic deadline (KMP).
- BlossomServerFinder port on AppServices for MarmotGroupIconDisplay.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01S7FuNBSKiyVecARSoE4B9P
This commit is contained in:
Claude
2026-09-30 00:07:02 +00:00
parent d8c4682f1f
commit d7a45732dc
52 changed files with 669 additions and 359 deletions
@@ -44,11 +44,11 @@ import com.vitorpamplona.amethyst.commons.resources.error_dialog_button_ok
import com.vitorpamplona.amethyst.commons.resources.notify_block_relay
import com.vitorpamplona.amethyst.commons.ui.components.TranslatableRichTextViewer
import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav
import com.vitorpamplona.amethyst.commons.ui.navigation.navs.rememberExtendedNav
import com.vitorpamplona.amethyst.commons.ui.stringRes
import com.vitorpamplona.amethyst.commons.ui.theme.Size16dp
import com.vitorpamplona.amethyst.commons.ui.theme.StdHorzSpacer
import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel
import com.vitorpamplona.amethyst.ui.navigation.navs.rememberExtendedNav
@Composable
fun NotifyRequestDialog(
@@ -24,8 +24,6 @@ import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.navigation.compose.rememberNavController
import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav
import com.vitorpamplona.amethyst.commons.ui.navigation.navs.ObservableNav
@Composable
fun rememberNav(): Nav {
@@ -37,14 +35,3 @@ fun rememberNav(): Nav {
Nav(navController, scope, ime)
}
}
@Composable
fun rememberExtendedNav(
nav: INav,
onBeforeNavigate: () -> Unit,
): INav {
val scope = rememberCoroutineScope()
return remember(nav, scope) {
ObservableNav(nav, scope, onBeforeNavigate)
}
}
@@ -66,7 +66,6 @@ import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.CardDefaults
import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.ElevatedCard
import androidx.compose.material3.LinearProgressIndicator
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.ProgressIndicatorDefaults
import androidx.compose.material3.Surface
@@ -77,7 +76,6 @@ import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.MutableState
import androidx.compose.runtime.SideEffect
import androidx.compose.runtime.State
import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableFloatStateOf
import androidx.compose.runtime.mutableLongStateOf
@@ -95,7 +93,6 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.alpha
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.StrokeCap
import androidx.compose.ui.platform.LocalClipboard
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalDensity
@@ -124,7 +121,6 @@ import com.vitorpamplona.amethyst.commons.model.Note
import com.vitorpamplona.amethyst.commons.model.ReactionRowAction
import com.vitorpamplona.amethyst.commons.model.ReactionRowItem
import com.vitorpamplona.amethyst.commons.model.User
import com.vitorpamplona.amethyst.commons.model.ZapraiserStatus
import com.vitorpamplona.amethyst.commons.model.navigation.Route
import com.vitorpamplona.amethyst.commons.model.nip29RelayGroups.RelayGroupChannel
import com.vitorpamplona.amethyst.commons.model.payments.PaymentTargetTypes
@@ -138,6 +134,7 @@ import com.vitorpamplona.amethyst.commons.relayClient.event.observeNoteReposts
import com.vitorpamplona.amethyst.commons.relayClient.event.observeNoteRepostsBy
import com.vitorpamplona.amethyst.commons.relayClient.event.observeNoteZaps
import com.vitorpamplona.amethyst.commons.relayClient.reqCommand.event.EventFinderFilterAssemblerSubscription
import com.vitorpamplona.amethyst.commons.relayClient.reqCommand.nwc.NWCFinderFilterAssemblerSubscription
import com.vitorpamplona.amethyst.commons.relayClient.user.observeUserInfo
import com.vitorpamplona.amethyst.commons.resources.Res
import com.vitorpamplona.amethyst.commons.resources.boost
@@ -164,7 +161,6 @@ import com.vitorpamplona.amethyst.commons.resources.quick_zap_amounts
import com.vitorpamplona.amethyst.commons.resources.quote
import com.vitorpamplona.amethyst.commons.resources.read_only_user
import com.vitorpamplona.amethyst.commons.resources.reload_mint_title
import com.vitorpamplona.amethyst.commons.resources.sats_to_complete
import com.vitorpamplona.amethyst.commons.service.ZapPaymentHandler
import com.vitorpamplona.amethyst.commons.ui.components.AnimatedBorderTextCornerRadius
import com.vitorpamplona.amethyst.commons.ui.components.ClickableBox
@@ -181,11 +177,13 @@ import com.vitorpamplona.amethyst.commons.ui.note.ExpandMoreIcon
import com.vitorpamplona.amethyst.commons.ui.note.LikeIcon
import com.vitorpamplona.amethyst.commons.ui.note.LikedIcon
import com.vitorpamplona.amethyst.commons.ui.note.LoadAddressableNote
import com.vitorpamplona.amethyst.commons.ui.note.ObserveZapAmountText
import com.vitorpamplona.amethyst.commons.ui.note.OutlinedZapIcon
import com.vitorpamplona.amethyst.commons.ui.note.RenderBoostGallery
import com.vitorpamplona.amethyst.commons.ui.note.RenderLikeGallery
import com.vitorpamplona.amethyst.commons.ui.note.RenderReaction
import com.vitorpamplona.amethyst.commons.ui.note.RenderZapGallery
import com.vitorpamplona.amethyst.commons.ui.note.RenderZapRaiser
import com.vitorpamplona.amethyst.commons.ui.note.RepostIcon
import com.vitorpamplona.amethyst.commons.ui.note.RepostedIcon
import com.vitorpamplona.amethyst.commons.ui.note.ShareIcon
@@ -203,10 +201,7 @@ import com.vitorpamplona.amethyst.commons.ui.theme.ButtonBorder
import com.vitorpamplona.amethyst.commons.ui.theme.Font14SP
import com.vitorpamplona.amethyst.commons.ui.theme.HalfDoubleVertSpacer
import com.vitorpamplona.amethyst.commons.ui.theme.HalfPadding
import com.vitorpamplona.amethyst.commons.ui.theme.Height24dpFilledModifier
import com.vitorpamplona.amethyst.commons.ui.theme.Height4dpFilledModifier
import com.vitorpamplona.amethyst.commons.ui.theme.ModifierWidth3dp
import com.vitorpamplona.amethyst.commons.ui.theme.NoSoTinyBorders
import com.vitorpamplona.amethyst.commons.ui.theme.ReactionRowExpandButton
import com.vitorpamplona.amethyst.commons.ui.theme.ReactionRowHeight
import com.vitorpamplona.amethyst.commons.ui.theme.ReactionRowHeightWithPadding
@@ -223,11 +218,9 @@ import com.vitorpamplona.amethyst.commons.ui.theme.Size22Modifier
import com.vitorpamplona.amethyst.commons.ui.theme.Size28Modifier
import com.vitorpamplona.amethyst.commons.ui.theme.SmallBorder
import com.vitorpamplona.amethyst.commons.ui.theme.ThemeComparisonColumn
import com.vitorpamplona.amethyst.commons.ui.theme.TinyBorders
import com.vitorpamplona.amethyst.commons.ui.theme.defaultTweenDuration
import com.vitorpamplona.amethyst.commons.ui.theme.defaultTweenFloatSpec
import com.vitorpamplona.amethyst.commons.ui.theme.defaultTweenIntOffsetSpec
import com.vitorpamplona.amethyst.commons.ui.theme.fundraiserProgressColor
import com.vitorpamplona.amethyst.commons.ui.theme.placeholderText
import com.vitorpamplona.amethyst.commons.ui.theme.reactionBox
import com.vitorpamplona.amethyst.commons.ui.theme.ripple24dp
@@ -239,7 +232,6 @@ import com.vitorpamplona.amethyst.model.zap.CashuRailStatus
import com.vitorpamplona.amethyst.model.zap.RailCapability
import com.vitorpamplona.amethyst.model.zap.RailCapabilityResolver
import com.vitorpamplona.amethyst.service.payments.PayToAppAvailability
import com.vitorpamplona.amethyst.service.relayClient.reqCommand.nwc.NWCFinderFilterAssemblerSubscription
import com.vitorpamplona.amethyst.ui.actions.uploads.MAX_VOICE_RECORD_SECONDS
import com.vitorpamplona.amethyst.ui.actions.uploads.RecordAudioBox
import com.vitorpamplona.amethyst.ui.screen.loggedIn.profile.header.PaymentTargetsDialog
@@ -268,7 +260,6 @@ import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import kotlinx.serialization.json.Json
import kotlin.math.roundToInt
import kotlin.uuid.ExperimentalUuidApi
import kotlin.uuid.Uuid
import androidx.compose.material3.Icon as Material3Icon
@@ -524,57 +515,6 @@ fun LoadAndDisplayZapraiser(
}
}
@Composable
fun RenderZapRaiser(
baseNote: Note,
zapraiserAmount: Long,
details: Boolean,
accountViewModel: AccountViewModel,
) {
val zapsState by observeNoteZaps(baseNote, accountViewModel)
var zapraiserStatus by remember { mutableStateOf(ZapraiserStatus(0F, "$zapraiserAmount")) }
LaunchedEffect(key1 = zapsState) {
zapsState?.note?.let {
val newStatus = accountViewModel.calculateZapraiser(baseNote)
if (zapraiserStatus != newStatus) {
zapraiserStatus = newStatus
}
}
}
LinearProgressIndicator(
modifier = if (details) Height24dpFilledModifier else Height4dpFilledModifier,
color = MaterialTheme.colorScheme.fundraiserProgressColor,
progress = { zapraiserStatus.progress },
gapSize = 0.dp,
strokeCap = StrokeCap.Square,
drawStopIndicator = {},
)
if (details) {
Box(
contentAlignment = Center,
modifier = TinyBorders,
) {
val totalPercentage by
remember(zapraiserStatus) {
derivedStateOf { "${(zapraiserStatus.progress * 100).roundToInt()}%" }
}
Text(
text =
stringRes(id = Res.string.sats_to_complete, totalPercentage, zapraiserStatus.left),
modifier = NoSoTinyBorders,
// color = MaterialTheme.colorScheme.placeholderText,
fontSize = Font14SP,
maxLines = 1,
)
}
}
}
@Composable
private fun WatchReactionsZapsBoostsAndDisplayIfExists(
baseNote: Note,
@@ -1668,45 +1608,6 @@ fun ObserveZapIcon(
inner(wasZappedByLoggedInUser)
}
@Composable
fun ObserveZapAmountText(
baseNote: Note,
accountViewModel: AccountViewModel,
inner: @Composable (String) -> Unit,
) {
val zapsState by observeNoteZaps(baseNote, accountViewModel)
if (zapsState?.note?.zapPayments?.isNotEmpty() == true) {
zapsState?.note?.zapPayments?.forEach {
if (it.value == null) {
NWCFinderFilterAssemblerSubscription(it.key, accountViewModel)
}
}
@Suppress("ProduceStateDoesNotAssignValue")
val zapAmountTxt by
produceState(initialValue = showAmount(baseNote.zapsAmount), key1 = zapsState) {
zapsState?.note?.let {
val newZapAmount = accountViewModel.calculateZapAmount(it)
if (value != newZapAmount) {
value = newZapAmount
}
}
}
inner(zapAmountTxt)
} else {
// Include the signed-in user's own pending onchain zaps so
// the counter reflects the optimistic value the gallery shows.
val ownPubKey = accountViewModel.account.userProfile().pubkeyHex
val note = zapsState?.note
val total =
(note?.zapsAmount ?: java.math.BigDecimal(0)) +
java.math.BigDecimal(note?.extraOwnPendingOnchainSats(ownPubKey) ?: 0L)
inner(showAmount(total))
}
}
@Composable
private fun BoostTypeChoicePopup(
baseNote: Note,
@@ -83,6 +83,7 @@ import com.vitorpamplona.amethyst.commons.ui.actions.uploads.SelectFromFiles
import com.vitorpamplona.amethyst.commons.ui.actions.uploads.SelectFromGallery
import com.vitorpamplona.amethyst.commons.ui.components.PlatformBackHandler
import com.vitorpamplona.amethyst.commons.ui.insets.imePaddingSafe
import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav
import com.vitorpamplona.amethyst.commons.ui.navigation.topbars.PostingTopBar
import com.vitorpamplona.amethyst.commons.ui.note.BaseUserPicture
import com.vitorpamplona.amethyst.commons.ui.note.NoteCompose
@@ -114,7 +115,6 @@ import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel
import com.vitorpamplona.amethyst.ui.actions.uploads.TakePictureButton
import com.vitorpamplona.amethyst.ui.actions.uploads.TakeVideoButton
import com.vitorpamplona.amethyst.ui.components.getActivity
import com.vitorpamplona.amethyst.ui.navigation.navs.Nav
import com.vitorpamplona.amethyst.ui.note.creators.emojiSuggestions.WatchAndLoadMyEmojiList
import com.vitorpamplona.amethyst.ui.note.creators.expiration.ExpirationDatePicker
import com.vitorpamplona.amethyst.ui.note.creators.invoice.InvoiceRequest
@@ -144,7 +144,7 @@ fun ReplyCommentPostScreen(
quoteId: HexKey? = null,
draftId: HexKey? = null,
accountViewModel: AccountViewModel,
nav: Nav,
nav: INav,
) {
val postViewModel: CommentPostViewModel = viewModel()
postViewModel.init(accountViewModel)
@@ -184,7 +184,7 @@ fun ReplyCommentPostScreen(
fun GenericCommentPostScreen(
postViewModel: CommentPostViewModel,
accountViewModel: AccountViewModel,
nav: Nav,
nav: INav,
) {
WatchAndLoadMyEmojiList(accountViewModel)
@@ -279,7 +279,7 @@ fun GenericCommentPostScreen(
private fun GenericCommentPostBody(
postViewModel: CommentPostViewModel,
accountViewModel: AccountViewModel,
nav: Nav,
nav: INav,
) {
val scrollState = rememberScrollState()
val audienceLists = rememberAudienceLists(accountViewModel)
@@ -83,6 +83,7 @@ import com.vitorpamplona.amethyst.ui.note.types.RenderNamedSiteEvent as AppRende
import com.vitorpamplona.amethyst.ui.note.types.RenderRootNappletEvent as AppRenderRootNappletEvent
import com.vitorpamplona.amethyst.ui.note.types.RenderRootSiteEvent as AppRenderRootSiteEvent
import com.vitorpamplona.amethyst.ui.note.types.RenderVoiceTrack as AppRenderVoiceTrack
import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.feed.QuickZapAmountRow as AppQuickZapAmountRow
/** Android's [NotePlatform]: the app's own media, map, audio, chess, git, napplet and zap composables. */
object AndroidNotePlatform : NotePlatform {
@@ -621,6 +622,15 @@ object AndroidNotePlatform : NotePlatform {
accountViewModel = accountViewModel,
)
@Composable
override fun QuickZapAmountRow(
note: Note,
onDismiss: () -> Unit,
onOnchainRequest: (Long?) -> Unit,
accountViewModel: AccountViewModel,
nav: INav,
) = AppQuickZapAmountRow(note, onDismiss, onOnchainRequest, accountViewModel, nav)
@Composable
override fun EditPostView(
onClose: () -> Unit,
@@ -27,6 +27,7 @@ import com.vitorpamplona.amethyst.commons.connectedApps.signers.NostrSignerPermi
import com.vitorpamplona.amethyst.commons.favorites.FavoriteAppsRegistry
import com.vitorpamplona.amethyst.commons.napplet.permissions.NappletPermissionLedger
import com.vitorpamplona.amethyst.commons.service.AppServices
import com.vitorpamplona.amethyst.commons.service.BlossomServerFinder
import com.vitorpamplona.amethyst.commons.tor.TorSettingsFlow
import com.vitorpamplona.quartz.nip05DnsIdentifiers.namecoin.NamecoinNameResolver
@@ -49,4 +50,19 @@ object AndroidAppServices : AppServices {
override val torSettings: TorSettingsFlow get() = Amethyst.instance.torPrefs.value
override val namecoinResolver: NamecoinNameResolver get() = Amethyst.instance.namecoinResolver
override val blossomServerFinder: BlossomServerFinder = AndroidBlossomServerFinder
}
/** [BlossomServerFinder] over the app's BUD-10 resolver, read lazily (main process only). */
private object AndroidBlossomServerFinder : BlossomServerFinder {
override fun cachedServerUrl(blossomUri: String): String? =
Amethyst.instance.blossomResolver
.cachedFindServer(blossomUri)
?.serverUrl
override suspend fun findServerUrl(blossomUri: String): String? =
Amethyst.instance.blossomResolver
.findServers(blossomUri)
?.serverUrl
}
@@ -49,7 +49,6 @@ import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
@@ -59,7 +58,6 @@ import com.vitorpamplona.amethyst.commons.icons.symbols.Icon
import com.vitorpamplona.amethyst.commons.icons.symbols.MaterialSymbol
import com.vitorpamplona.amethyst.commons.icons.symbols.MaterialSymbols
import com.vitorpamplona.amethyst.commons.model.Note
import com.vitorpamplona.amethyst.commons.model.User
import com.vitorpamplona.amethyst.commons.model.concord.ConcordChannel
import com.vitorpamplona.amethyst.commons.model.navigation.Route
import com.vitorpamplona.amethyst.commons.model.nip29RelayGroups.RelayGroupChannel
@@ -69,16 +67,13 @@ import com.vitorpamplona.amethyst.commons.resources.buzz_edit_message
import com.vitorpamplona.amethyst.commons.resources.chat_delivery_details_title
import com.vitorpamplona.amethyst.commons.resources.edit_draft
import com.vitorpamplona.amethyst.commons.resources.edit_message
import com.vitorpamplona.amethyst.commons.resources.error_dialog_zap_error
import com.vitorpamplona.amethyst.commons.resources.more_options
import com.vitorpamplona.amethyst.commons.resources.no_wallet_found
import com.vitorpamplona.amethyst.commons.resources.quick_action_delete_dialog_btn
import com.vitorpamplona.amethyst.commons.resources.quick_action_request_deletion_alert_title
import com.vitorpamplona.amethyst.commons.resources.relay_group_pin_message
import com.vitorpamplona.amethyst.commons.resources.relay_group_unpin_message
import com.vitorpamplona.amethyst.commons.resources.reply_description
import com.vitorpamplona.amethyst.commons.resources.show_less
import com.vitorpamplona.amethyst.commons.service.ZapPaymentHandler
import com.vitorpamplona.amethyst.commons.ui.components.ClickableBox
import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav
import com.vitorpamplona.amethyst.commons.ui.note.ChangeReactionIcon
@@ -93,9 +88,9 @@ import com.vitorpamplona.amethyst.commons.ui.note.elements.ShareOptionsBottomShe
import com.vitorpamplona.amethyst.commons.ui.note.elements.noteActionSections
import com.vitorpamplona.amethyst.commons.ui.note.elements.observeBookmarksFollowsAndAccount
import com.vitorpamplona.amethyst.commons.ui.note.platform.EditPostView
import com.vitorpamplona.amethyst.commons.ui.note.platform.QuickZapAmountRow
import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.report.ReportNoteDialog
import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.wallet.OnchainZapSendDialog
import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.wallet.navigateToReloadMint
import com.vitorpamplona.amethyst.commons.ui.stringRes
import com.vitorpamplona.amethyst.commons.ui.theme.DividerThickness
import com.vitorpamplona.amethyst.commons.ui.theme.Size28Modifier
@@ -104,17 +99,11 @@ import com.vitorpamplona.amethyst.commons.ui.theme.placeholderText
import com.vitorpamplona.amethyst.commons.ui.theme.reactionBox
import com.vitorpamplona.amethyst.commons.ui.theme.selectedReactionBoxModifier
import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel
import com.vitorpamplona.amethyst.ui.note.ZapAmountChoiceGrid
import com.vitorpamplona.amethyst.ui.note.observeZapRailCapability
import com.vitorpamplona.amethyst.ui.note.payViaIntentOrManualSplit
import com.vitorpamplona.quartz.buzz.stream.StreamMessageV2Event
import com.vitorpamplona.quartz.nip01Core.core.Event
import com.vitorpamplona.quartz.nip22Comments.CommentEvent
import com.vitorpamplona.quartz.nipC7Chats.ChatEvent
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.toImmutableList
import kotlinx.collections.immutable.toImmutableSet
import kotlin.uuid.ExperimentalUuidApi
// null amount = open the on-chain dialog with no prefill.
@Immutable
@@ -622,98 +611,3 @@ private fun QuickReactionRow(
}
}
}
/**
* The user's zap presets unpacked as rail-aware amount chips (same grid the zap
* amount popup shows), firing directly from the sheet. Lightning/cashu zaps
* dismiss immediately — errors surface as toasts and the receipt lands on the
* bubble's sats chip; on-chain amounts hand off to the dialog hosted by the sheet.
*/
@OptIn(ExperimentalUuidApi::class)
@Composable
private fun QuickZapAmountRow(
note: Note,
onDismiss: () -> Unit,
onOnchainRequest: (Long?) -> Unit,
accountViewModel: AccountViewModel,
nav: INav,
) {
val noWalletFoundStr = stringRes(Res.string.no_wallet_found)
val zapAmountChoices by
accountViewModel.account.settings.syncedSettings.zaps.zapAmountChoices
.collectAsStateWithLifecycle()
val amountChoices = remember(zapAmountChoices) { zapAmountChoices.distinct().toImmutableList() }
if (amountChoices.isEmpty()) return
val railCapability =
observeZapRailCapability(
baseNote = note,
accountViewModel = accountViewModel,
// Onchain zap events are public and would e-tag the rumor id.
onchainSupported = !note.isPrivateRumor(),
)
val context = LocalContext.current
val onError = { _: String, message: String, user: User? ->
// Payment failed — drop the optimistic "zapping" indicator on the bubble.
accountViewModel.endZapInFlight(note.idHex)
accountViewModel.toastManager.toast(Res.string.error_dialog_zap_error, message, user)
}
val onPayViaIntent = { payables: ImmutableList<ZapPaymentHandler.Payable> ->
// Handoff to an external wallet: we can't observe whether it completes, so clear
// the optimistic indicator rather than leave it spinning forever.
accountViewModel.endZapInFlight(note.idHex)
payViaIntentOrManualSplit(payables, context, noWalletFoundStr, accountViewModel, nav)
}
Row(
modifier = Modifier.fillMaxWidth().padding(horizontal = 16.dp),
horizontalArrangement = Arrangement.Center,
) {
ZapAmountChoiceGrid(
amountChoices = amountChoices,
railCapability = railCapability,
onLightningZap = { amountInSats ->
// Show a pending zap chip on the bubble immediately; it settles into the
// real sats chip once the receipt lands (or clears on error/timeout).
accountViewModel.markZapInFlight(note.idHex)
accountViewModel.zap(
note,
amountInSats * 1000,
null,
"",
true,
onError,
{ },
onPayViaIntent,
)
onDismiss()
},
onNutzap = { amountInSats ->
accountViewModel.markZapInFlight(note.idHex)
accountViewModel.sendNutzap(
baseNote = note,
amountSats = amountInSats,
message = "",
onError = onError,
onProgress = { },
)
onDismiss()
},
onOnchainAmount = onOnchainRequest,
onReloadNutzap = { amount ->
navigateToReloadMint(accountViewModel, nav, note, amount)
onDismiss()
},
onChangeAmount = {
nav.nav(Route.UpdateZapAmount())
onDismiss()
},
// Hands off to another app; the sheet must not stay stacked behind it.
onHandedOff = onDismiss,
)
}
}
@@ -56,6 +56,7 @@ import com.vitorpamplona.amethyst.commons.model.navigation.routeFor
import com.vitorpamplona.amethyst.commons.ui.components.LocalInlineQuoteRenderer
import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav
import com.vitorpamplona.amethyst.commons.ui.navigation.routes.routeFor
import com.vitorpamplona.amethyst.commons.ui.note.RenderZapRaiser
import com.vitorpamplona.amethyst.commons.ui.note.WatchBlockAndReport
import com.vitorpamplona.amethyst.commons.ui.note.WatchNoteEvent
import com.vitorpamplona.amethyst.commons.ui.note.creators.zapsplits.DisplayZapSplits
@@ -66,7 +67,6 @@ import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.chats.feed.types.Re
import com.vitorpamplona.amethyst.commons.ui.theme.ReactionRowZapraiser
import com.vitorpamplona.amethyst.commons.ui.theme.StdVertSpacer
import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel
import com.vitorpamplona.amethyst.ui.note.RenderZapRaiser
import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.feed.types.RenderBuzzActivityRow
import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.feed.types.RenderBuzzDiff
import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.feed.types.RenderBuzzEditedNote
@@ -66,6 +66,7 @@ import com.vitorpamplona.amethyst.commons.resources.chat_minichat_reply_count
import com.vitorpamplona.amethyst.commons.ui.components.AnimatedBorderTextCornerRadius
import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav
import com.vitorpamplona.amethyst.commons.ui.note.LikedIcon
import com.vitorpamplona.amethyst.commons.ui.note.ObserveZapAmountText
import com.vitorpamplona.amethyst.commons.ui.note.ZappedIcon
import com.vitorpamplona.amethyst.commons.ui.pluralStringRes
import com.vitorpamplona.amethyst.commons.ui.richtext.InLineIconRenderer
@@ -76,7 +77,6 @@ import com.vitorpamplona.amethyst.commons.ui.theme.bitcoinColor
import com.vitorpamplona.amethyst.commons.ui.theme.grayText
import com.vitorpamplona.amethyst.commons.ui.theme.subtleBorder
import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel
import com.vitorpamplona.amethyst.ui.note.ObserveZapAmountText
import com.vitorpamplona.quartz.nip30CustomEmoji.CustomEmoji
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.persistentListOf
@@ -0,0 +1,145 @@
/*
* Copyright (c) 2025 Vitor Pamplona
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
* the Software without restriction, including without limitation the rights to use,
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
* Software, and to permit persons to whom the Software is furnished to do so,
* subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
package com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.feed
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.unit.dp
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.vitorpamplona.amethyst.commons.model.Note
import com.vitorpamplona.amethyst.commons.model.User
import com.vitorpamplona.amethyst.commons.model.navigation.Route
import com.vitorpamplona.amethyst.commons.resources.Res
import com.vitorpamplona.amethyst.commons.resources.error_dialog_zap_error
import com.vitorpamplona.amethyst.commons.resources.no_wallet_found
import com.vitorpamplona.amethyst.commons.service.ZapPaymentHandler
import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav
import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.wallet.navigateToReloadMint
import com.vitorpamplona.amethyst.commons.ui.stringRes
import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel
import com.vitorpamplona.amethyst.ui.note.ZapAmountChoiceGrid
import com.vitorpamplona.amethyst.ui.note.observeZapRailCapability
import com.vitorpamplona.amethyst.ui.note.payViaIntentOrManualSplit
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.toImmutableList
import kotlin.uuid.ExperimentalUuidApi
/**
* The user's zap presets unpacked as rail-aware amount chips (same grid the zap
* amount popup shows), firing directly from the sheet. Lightning/cashu zaps
* dismiss immediately — errors surface as toasts and the receipt lands on the
* bubble's sats chip; on-chain amounts hand off to the dialog hosted by the sheet.
*/
@OptIn(ExperimentalUuidApi::class)
@Composable
fun QuickZapAmountRow(
note: Note,
onDismiss: () -> Unit,
onOnchainRequest: (Long?) -> Unit,
accountViewModel: AccountViewModel,
nav: INav,
) {
val noWalletFoundStr = stringRes(Res.string.no_wallet_found)
val zapAmountChoices by
accountViewModel.account.settings.syncedSettings.zaps.zapAmountChoices
.collectAsStateWithLifecycle()
val amountChoices = remember(zapAmountChoices) { zapAmountChoices.distinct().toImmutableList() }
if (amountChoices.isEmpty()) return
val railCapability =
observeZapRailCapability(
baseNote = note,
accountViewModel = accountViewModel,
// Onchain zap events are public and would e-tag the rumor id.
onchainSupported = !note.isPrivateRumor(),
)
val context = LocalContext.current
val onError = { _: String, message: String, user: User? ->
// Payment failed — drop the optimistic "zapping" indicator on the bubble.
accountViewModel.endZapInFlight(note.idHex)
accountViewModel.toastManager.toast(Res.string.error_dialog_zap_error, message, user)
}
val onPayViaIntent = { payables: ImmutableList<ZapPaymentHandler.Payable> ->
// Handoff to an external wallet: we can't observe whether it completes, so clear
// the optimistic indicator rather than leave it spinning forever.
accountViewModel.endZapInFlight(note.idHex)
payViaIntentOrManualSplit(payables, context, noWalletFoundStr, accountViewModel, nav)
}
Row(
modifier = Modifier.fillMaxWidth().padding(horizontal = 16.dp),
horizontalArrangement = Arrangement.Center,
) {
ZapAmountChoiceGrid(
amountChoices = amountChoices,
railCapability = railCapability,
onLightningZap = { amountInSats ->
// Show a pending zap chip on the bubble immediately; it settles into the
// real sats chip once the receipt lands (or clears on error/timeout).
accountViewModel.markZapInFlight(note.idHex)
accountViewModel.zap(
note,
amountInSats * 1000,
null,
"",
true,
onError,
{ },
onPayViaIntent,
)
onDismiss()
},
onNutzap = { amountInSats ->
accountViewModel.markZapInFlight(note.idHex)
accountViewModel.sendNutzap(
baseNote = note,
amountSats = amountInSats,
message = "",
onError = onError,
onProgress = { },
)
onDismiss()
},
onOnchainAmount = onOnchainRequest,
onReloadNutzap = { amount ->
navigateToReloadMint(accountViewModel, nav, note, amount)
onDismiss()
},
onChangeAmount = {
nav.nav(Route.UpdateZapAmount())
onDismiss()
},
// Hands off to another app; the sheet must not stay stacked behind it.
onHandedOff = onDismiss,
)
}
}
@@ -24,9 +24,10 @@ import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.produceState
import androidx.compose.runtime.remember
import com.vitorpamplona.amethyst.Amethyst
import com.vitorpamplona.amethyst.commons.model.marmotGroups.MarmotGroupImage
import com.vitorpamplona.amethyst.commons.model.nip11RelayInfo.loadRelayInfo
import com.vitorpamplona.amethyst.commons.service.BlossomServerFinder
import com.vitorpamplona.amethyst.commons.ui.platform.LocalAppServices
import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel
import com.vitorpamplona.quartz.marmot.appComponents.GroupAvatarUrlV1
import com.vitorpamplona.quartz.marmot.appComponents.MarmotWebUrl
@@ -43,7 +44,7 @@ import com.vitorpamplona.quartz.nipB7Blossom.BlossomUri
*
* The blob is content-addressed on Blossom by [MarmotGroupImage.hash]; the canonical
* scheme stores only the hash, and the icon usually lives on the *uploader's* Blossom
* server rather than the viewer's. So we resolve it through [Amethyst.blossomResolver],
* server rather than the viewer's. So we resolve it through the app's BUD-10 resolver ([BlossomServerFinder]),
* which probes the viewer's default server (passed as a first-try `xs` hint) and the
* group admins' configured servers (via their pubkeys as `as` authors, BUD-03). While
* that async probe runs, we optimistically load from the viewer's default server so the
@@ -77,9 +78,9 @@ fun rememberMarmotGroupIconUrl(
).toUriString()
}
val resolver = Amethyst.instance.blossomResolver
val url by produceState(resolver.cachedFindServer(blossomUri)?.serverUrl ?: fallbackUrl, blossomUri) {
value = resolver.findServers(blossomUri)?.serverUrl ?: fallbackUrl
val finder = LocalAppServices.current.blossomServerFinder
val url by produceState(finder.cachedServerUrl(blossomUri) ?: fallbackUrl, blossomUri) {
value = finder.findServerUrl(blossomUri) ?: fallbackUrl
}
val cipher = remember(image) { MarmotGroupImageCipher(image.key, image.nonce) }
@@ -125,7 +125,6 @@ import com.vitorpamplona.amethyst.ui.actions.UrlUserTagOutputTransformation
import com.vitorpamplona.amethyst.ui.actions.uploads.TakePictureButton
import com.vitorpamplona.amethyst.ui.actions.uploads.TakeVideoButton
import com.vitorpamplona.amethyst.ui.components.ThinPaddingTextField
import com.vitorpamplona.amethyst.ui.navigation.navs.Nav
import com.vitorpamplona.amethyst.ui.note.creators.emojiSuggestions.WatchAndLoadMyEmojiList
import com.vitorpamplona.amethyst.ui.note.creators.expiration.ExpirationDatePicker
import com.vitorpamplona.amethyst.ui.note.creators.invoice.NewPostInvoiceRequest
@@ -148,7 +147,7 @@ fun NewGroupDMScreen(
message: String? = null,
attachment: String? = null,
accountViewModel: AccountViewModel,
nav: Nav,
nav: INav,
) {
NewGroupDMScreen(message, attachment?.ifBlank { null }?.toUri(), accountViewModel, nav)
}
@@ -159,7 +158,7 @@ fun NewGroupDMScreen(
message: String? = null,
attachment: Uri? = null,
accountViewModel: AccountViewModel,
nav: Nav,
nav: INav,
) {
val postViewModel: ChatNewMessageViewModel = viewModel()
postViewModel.init(accountViewModel)
@@ -136,6 +136,8 @@ import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import kotlin.coroutines.cancellation.CancellationException
import kotlin.time.Duration.Companion.seconds
import kotlin.time.TimeSource
@Stable
open class ChannelNewMessageViewModel :
@@ -465,10 +467,9 @@ open class ChannelNewMessageViewModel :
val mined =
withContext(Dispatchers.Default) {
// Bitchat mines 8 bits by default; cap the effort so a slow device still sends.
val deadline = System.nanoTime() + 2_000_000_000L
val threads = Runtime.getRuntime().availableProcessors().coerceAtLeast(1)
val started = TimeSource.Monotonic.markNow()
runCatching {
PoWMiner.mine(template, pubKeyHex, 8, threads, isActive = { System.nanoTime() < deadline })
PoWMiner.mine(template, pubKeyHex, 8, account.powMinerWorkers(), isActive = { started.elapsedNow() < 2.seconds })
}.getOrDefault(template)
}
@@ -1051,7 +1052,7 @@ open class ChannelNewMessageViewModel :
}
override fun onCleared() {
Log.d("Init") { "OnCleared: ${this.javaClass.simpleName}" }
Log.d("Init") { "OnCleared: ${this::class.simpleName}" }
}
fun updateZapPercentage(
@@ -107,6 +107,7 @@ import com.vitorpamplona.amethyst.commons.ui.actions.uploads.SelectFromGallery
import com.vitorpamplona.amethyst.commons.ui.components.MyAsyncImage
import com.vitorpamplona.amethyst.commons.ui.components.PlatformBackHandler
import com.vitorpamplona.amethyst.commons.ui.insets.imePaddingSafe
import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav
import com.vitorpamplona.amethyst.commons.ui.navigation.topbars.PostingTopBar
import com.vitorpamplona.amethyst.commons.ui.note.creators.contentWarning.ContentSensitivityExplainer
import com.vitorpamplona.amethyst.commons.ui.note.creators.contentWarning.MarkAsSensitiveButton
@@ -133,7 +134,6 @@ import com.vitorpamplona.amethyst.ui.actions.uploads.TakePictureButton
import com.vitorpamplona.amethyst.ui.actions.uploads.TakeVideoButton
import com.vitorpamplona.amethyst.ui.components.ThinPaddingTextField
import com.vitorpamplona.amethyst.ui.components.markdown.RenderContentAsMarkdown
import com.vitorpamplona.amethyst.ui.navigation.navs.Nav
import com.vitorpamplona.amethyst.ui.note.creators.emojiSuggestions.WatchAndLoadMyEmojiList
import com.vitorpamplona.amethyst.ui.note.creators.expiration.ExpirationDatePicker
import com.vitorpamplona.amethyst.ui.note.creators.invoice.InvoiceRequest
@@ -150,7 +150,7 @@ fun LongFormPostScreen(
draftId: HexKey? = null,
versionId: HexKey? = null,
accountViewModel: AccountViewModel,
nav: Nav,
nav: INav,
) {
val postViewModel: LongFormPostViewModel = viewModel()
postViewModel.init(accountViewModel)
@@ -214,7 +214,7 @@ fun LongFormPostScreen(
private fun MarkdownPostScreenBody(
postViewModel: LongFormPostViewModel,
accountViewModel: AccountViewModel,
nav: Nav,
nav: INav,
) {
val scrollState = rememberScrollState()
Column(
@@ -80,7 +80,6 @@ import com.vitorpamplona.amethyst.commons.ui.theme.SuggestionListDefaultHeightPa
import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel
import com.vitorpamplona.amethyst.ui.actions.uploads.TakePictureButton
import com.vitorpamplona.amethyst.ui.actions.uploads.TakeVideoButton
import com.vitorpamplona.amethyst.ui.navigation.navs.Nav
import com.vitorpamplona.amethyst.ui.note.creators.emojiSuggestions.WatchAndLoadMyEmojiList
import com.vitorpamplona.amethyst.ui.note.creators.expiration.ExpirationDatePicker
import com.vitorpamplona.amethyst.ui.note.creators.invoice.InvoiceRequest
@@ -103,7 +102,7 @@ fun NewProductScreen(
quoteId: HexKey? = null,
draftId: HexKey? = null,
accountViewModel: AccountViewModel,
nav: Nav,
nav: INav,
) {
val postViewModel: NewProductViewModel = viewModel()
postViewModel.init(accountViewModel)
@@ -28,8 +28,8 @@ import androidx.core.net.toUri
import androidx.lifecycle.viewmodel.compose.viewModel
import com.vitorpamplona.amethyst.commons.model.cache.LocalCache
import com.vitorpamplona.amethyst.commons.service.uploads.SelectedMedia
import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav
import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel
import com.vitorpamplona.amethyst.ui.navigation.navs.Nav
import com.vitorpamplona.amethyst.ui.note.nip22Comments.CommentPostViewModel
import com.vitorpamplona.amethyst.ui.note.nip22Comments.GenericCommentPostScreen
import com.vitorpamplona.quartz.nip01Core.core.HexKey
@@ -47,7 +47,7 @@ fun GeoHashPostScreen(
quoteId: HexKey? = null,
draftId: HexKey? = null,
accountViewModel: AccountViewModel,
nav: Nav,
nav: INav,
) {
val postViewModel: CommentPostViewModel = viewModel()
postViewModel.init(accountViewModel)
@@ -28,8 +28,8 @@ import androidx.core.net.toUri
import androidx.lifecycle.viewmodel.compose.viewModel
import com.vitorpamplona.amethyst.commons.model.cache.LocalCache
import com.vitorpamplona.amethyst.commons.service.uploads.SelectedMedia
import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav
import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel
import com.vitorpamplona.amethyst.ui.navigation.navs.Nav
import com.vitorpamplona.amethyst.ui.note.nip22Comments.CommentPostViewModel
import com.vitorpamplona.amethyst.ui.note.nip22Comments.GenericCommentPostScreen
import com.vitorpamplona.quartz.nip01Core.core.HexKey
@@ -47,7 +47,7 @@ fun HashtagPostScreen(
quoteId: HexKey? = null,
draftId: HexKey? = null,
accountViewModel: AccountViewModel,
nav: Nav,
nav: INav,
) {
val postViewModel: CommentPostViewModel = viewModel()
postViewModel.init(accountViewModel)
@@ -72,6 +72,7 @@ import com.vitorpamplona.amethyst.commons.resources.new_highlight_passage_placeh
import com.vitorpamplona.amethyst.commons.resources.new_highlight_source_label
import com.vitorpamplona.amethyst.commons.resources.new_highlight_title
import com.vitorpamplona.amethyst.commons.ui.insets.imePaddingSafe
import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav
import com.vitorpamplona.amethyst.commons.ui.navigation.topbars.PostingTopBar
import com.vitorpamplona.amethyst.commons.ui.note.NoteCompose
import com.vitorpamplona.amethyst.commons.ui.note.creators.userSuggestions.ShowUserSuggestionList
@@ -80,7 +81,6 @@ import com.vitorpamplona.amethyst.commons.ui.stringRes
import com.vitorpamplona.amethyst.commons.ui.theme.SuggestionListDefaultHeightPage
import com.vitorpamplona.amethyst.commons.ui.theme.replyModifier
import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel
import com.vitorpamplona.amethyst.ui.navigation.navs.Nav
import com.vitorpamplona.amethyst.ui.note.creators.emojiSuggestions.WatchAndLoadMyEmojiList
import com.vitorpamplona.amethyst.ui.note.creators.messagefield.MessageField
@@ -111,7 +111,7 @@ fun NewHighlightScreen(
sourceEventId: String? = null,
author: String? = null,
accountViewModel: AccountViewModel,
nav: Nav,
nav: INav,
) {
val postViewModel: NewHighlightPostViewModel = viewModel()
postViewModel.init(accountViewModel)
@@ -114,6 +114,7 @@ import com.vitorpamplona.amethyst.commons.ui.actions.uploads.SelectFromFiles
import com.vitorpamplona.amethyst.commons.ui.actions.uploads.SelectFromGallery
import com.vitorpamplona.amethyst.commons.ui.components.PlatformBackHandler
import com.vitorpamplona.amethyst.commons.ui.insets.imePaddingSafe
import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav
import com.vitorpamplona.amethyst.commons.ui.navigation.topbars.PostingTopBar
import com.vitorpamplona.amethyst.commons.ui.note.BaseUserPicture
import com.vitorpamplona.amethyst.commons.ui.note.NoteCompose
@@ -156,7 +157,6 @@ import com.vitorpamplona.amethyst.ui.actions.uploads.VoiceAnonymizationSection
import com.vitorpamplona.amethyst.ui.actions.uploads.VoiceMessagePreview
import com.vitorpamplona.amethyst.ui.components.ThinPaddingTextField
import com.vitorpamplona.amethyst.ui.components.getActivity
import com.vitorpamplona.amethyst.ui.navigation.navs.Nav
import com.vitorpamplona.amethyst.ui.note.creators.emojiSuggestions.WatchAndLoadMyEmojiList
import com.vitorpamplona.amethyst.ui.note.creators.expiration.ExpirationDatePicker
import com.vitorpamplona.amethyst.ui.note.creators.invoice.InvoiceRequest
@@ -195,7 +195,7 @@ fun ShortNotePostScreen(
groupThreadId: HexKey? = null,
groupThreadRelayUrl: String? = null,
accountViewModel: AccountViewModel,
nav: Nav,
nav: INav,
) {
val postViewModel: ShortNotePostViewModel = viewModel()
postViewModel.init(accountViewModel)
@@ -269,7 +269,7 @@ fun ShortNotePostScreen(
internal fun NewPostScreenInner(
postViewModel: ShortNotePostViewModel,
accountViewModel: AccountViewModel,
nav: Nav,
nav: INav,
) {
WatchAndLoadMyEmojiList(accountViewModel)
@@ -324,7 +324,7 @@ internal fun NewPostScreenInner(
private fun NewPostScreenBody(
postViewModel: ShortNotePostViewModel,
accountViewModel: AccountViewModel,
nav: Nav,
nav: INav,
) {
val scrollState = rememberScrollState()
val audienceLists = rememberAudienceLists(accountViewModel)
@@ -40,6 +40,7 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import androidx.lifecycle.viewmodel.compose.viewModel
import com.vitorpamplona.amethyst.commons.ui.components.PlatformBackHandler
import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav
import com.vitorpamplona.amethyst.commons.ui.navigation.topbars.PostingTopBar
import com.vitorpamplona.amethyst.commons.ui.note.NoteCompose
import com.vitorpamplona.amethyst.commons.ui.note.types.ReplyRenderType
@@ -51,7 +52,6 @@ import com.vitorpamplona.amethyst.ui.actions.mediaServers.FileServerSelectionRow
import com.vitorpamplona.amethyst.ui.actions.uploads.UploadProgressIndicator
import com.vitorpamplona.amethyst.ui.actions.uploads.VoiceAnonymizationSection
import com.vitorpamplona.amethyst.ui.actions.uploads.VoiceMessagePreview
import com.vitorpamplona.amethyst.ui.navigation.navs.Nav
@OptIn(ExperimentalMaterial3Api::class)
@Composable
@@ -62,7 +62,7 @@ fun VoiceReplyScreen(
duration: Int,
amplitudesJson: String,
accountViewModel: AccountViewModel,
nav: Nav,
nav: INav,
) {
val viewModel: VoiceReplyViewModel = viewModel()
@@ -107,7 +107,7 @@ fun VoiceReplyScreen(
private fun VoiceReplyScreenBody(
viewModel: VoiceReplyViewModel,
accountViewModel: AccountViewModel,
nav: Nav,
nav: INav,
) {
val scrollState = rememberScrollState()
@@ -64,13 +64,12 @@ import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav
import com.vitorpamplona.amethyst.commons.ui.navigation.topbars.PostingTopBar
import com.vitorpamplona.amethyst.commons.ui.stringRes
import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel
import com.vitorpamplona.amethyst.ui.navigation.navs.Nav
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun NewGoalScreen(
accountViewModel: AccountViewModel,
nav: Nav,
nav: INav,
) {
val goalViewModel: NewGoalViewModel = viewModel()
goalViewModel.init(accountViewModel)
@@ -94,7 +94,6 @@ import com.vitorpamplona.amethyst.ui.actions.UrlUserTagOutputTransformation
import com.vitorpamplona.amethyst.ui.actions.uploads.TakePictureButton
import com.vitorpamplona.amethyst.ui.actions.uploads.TakeVideoButton
import com.vitorpamplona.amethyst.ui.components.ThinPaddingTextField
import com.vitorpamplona.amethyst.ui.navigation.navs.Nav
import com.vitorpamplona.amethyst.ui.note.creators.emojiSuggestions.WatchAndLoadMyEmojiList
import com.vitorpamplona.amethyst.ui.note.creators.expiration.ExpirationDatePicker
import com.vitorpamplona.amethyst.ui.note.creators.invoice.NewPostInvoiceRequest
@@ -118,7 +117,7 @@ fun NewPublicMessageScreen(
replyId: HexKey? = null,
draftId: HexKey? = null,
accountViewModel: AccountViewModel,
nav: Nav,
nav: INav,
) {
val postViewModel: NewPublicMessageViewModel = viewModel()
postViewModel.init(accountViewModel)
@@ -26,8 +26,8 @@ import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.lifecycle.viewmodel.compose.viewModel
import com.vitorpamplona.amethyst.commons.model.cache.LocalCache
import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav
import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel
import com.vitorpamplona.amethyst.ui.navigation.navs.Nav
import com.vitorpamplona.amethyst.ui.screen.loggedIn.home.NewPostScreenInner
import com.vitorpamplona.amethyst.ui.screen.loggedIn.home.ShortNotePostViewModel
import com.vitorpamplona.quartz.nip01Core.core.HexKey
@@ -39,7 +39,7 @@ fun PollPostScreen(
message: String? = null,
draftId: HexKey? = null,
accountViewModel: AccountViewModel,
nav: Nav,
nav: INav,
) {
val postViewModel: ShortNotePostViewModel = viewModel()
postViewModel.init(accountViewModel)
@@ -40,7 +40,6 @@ import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.lifecycle.viewmodel.compose.viewModel
import com.vitorpamplona.amethyst.commons.defaults.DefaultDMRelayList
@@ -87,9 +86,14 @@ import com.vitorpamplona.amethyst.commons.resources.trusted_section_explainer
import com.vitorpamplona.amethyst.commons.ui.components.M3ActionDialog
import com.vitorpamplona.amethyst.commons.ui.components.M3ActionRow
import com.vitorpamplona.amethyst.commons.ui.components.M3ActionSection
import com.vitorpamplona.amethyst.commons.ui.components.rememberFileSharer
import com.vitorpamplona.amethyst.commons.ui.components.rememberTextSharer
import com.vitorpamplona.amethyst.commons.ui.insets.imePaddingSafe
import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav
import com.vitorpamplona.amethyst.commons.ui.navigation.topbars.SavingTopBar
import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.relays.common.RelayExporter
import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.relays.common.RelayListCollection
import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.relays.common.RelayZipExporter
import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.relays.common.relaySetupInfoBuilder
import com.vitorpamplona.amethyst.commons.ui.stringRes
import com.vitorpamplona.amethyst.commons.ui.theme.FeedPadding
@@ -102,9 +106,6 @@ import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.blocked.BlockedRelay
import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.blocked.renderBlockedItems
import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.broadcast.BroadcastRelayListViewModel
import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.broadcast.renderBroadcastItems
import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.common.RelayExporter
import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.common.RelayListCollection
import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.common.RelayZipExporter
import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.connected.ConnectedRelayListViewModel
import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.connected.renderConnectedItems
import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.dm.DMRelayListViewModel
@@ -621,7 +622,8 @@ fun SettingsCategoryWithButton(
@Composable
fun ExportDropdownMenu(collection: () -> RelayListCollection) {
var expanded by remember { mutableStateOf(false) }
val context = LocalContext.current
val textSharer = rememberTextSharer()
val fileSharer = rememberFileSharer()
val scope = rememberCoroutineScope()
IconButton(onClick = { expanded = true }) {
@@ -642,14 +644,14 @@ fun ExportDropdownMenu(collection: () -> RelayListCollection) {
text = stringRes(Res.string.export_as_text),
) {
expanded = false
scope.launch { RelayExporter(context).export(collection()) }
scope.launch { RelayExporter(textSharer).export(collection()) }
}
M3ActionRow(
icon = MaterialSymbols.FolderZip,
text = stringRes(Res.string.export_as_zip),
) {
expanded = false
scope.launch { RelayZipExporter(context).export(collection()) }
scope.launch { RelayZipExporter(fileSharer).export(collection()) }
}
}
}
@@ -33,6 +33,7 @@ import com.vitorpamplona.amethyst.commons.model.cache.LocalCache
import com.vitorpamplona.amethyst.commons.relays.ui.RelayDragState
import com.vitorpamplona.amethyst.commons.relays.ui.rememberRelayDragState
import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav
import com.vitorpamplona.amethyst.commons.ui.navigation.navs.rememberExtendedNav
import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.relays.common.BasicRelaySetupInfo
import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.relays.common.RelayUrlEditField
import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.relays.common.relaySetupInfoBuilder
@@ -41,7 +42,6 @@ import com.vitorpamplona.amethyst.commons.ui.theme.HorzHalfVertPadding
import com.vitorpamplona.amethyst.commons.ui.theme.HorzPadding
import com.vitorpamplona.amethyst.commons.ui.theme.StdVertSpacer
import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel
import com.vitorpamplona.amethyst.ui.navigation.navs.rememberExtendedNav
import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.common.BasicRelaySetupInfoDialog
@Composable
@@ -33,6 +33,7 @@ import com.vitorpamplona.amethyst.commons.model.cache.LocalCache
import com.vitorpamplona.amethyst.commons.relays.ui.RelayDragState
import com.vitorpamplona.amethyst.commons.relays.ui.rememberRelayDragState
import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav
import com.vitorpamplona.amethyst.commons.ui.navigation.navs.rememberExtendedNav
import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.relays.common.BasicRelaySetupInfo
import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.relays.common.RelayUrlEditField
import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.relays.common.relaySetupInfoBuilder
@@ -41,7 +42,6 @@ import com.vitorpamplona.amethyst.commons.ui.theme.HorzHalfVertPadding
import com.vitorpamplona.amethyst.commons.ui.theme.HorzPadding
import com.vitorpamplona.amethyst.commons.ui.theme.StdVertSpacer
import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel
import com.vitorpamplona.amethyst.ui.navigation.navs.rememberExtendedNav
import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.common.BasicRelaySetupInfoDialog
@Composable
@@ -41,12 +41,12 @@ import com.vitorpamplona.amethyst.commons.model.navigation.Route
import com.vitorpamplona.amethyst.commons.resources.Res
import com.vitorpamplona.amethyst.commons.resources.active_subs_title
import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav
import com.vitorpamplona.amethyst.commons.ui.navigation.navs.rememberExtendedNav
import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.relays.common.BasicRelaySetupInfo
import com.vitorpamplona.amethyst.commons.ui.stringRes
import com.vitorpamplona.amethyst.commons.ui.theme.FeedPadding
import com.vitorpamplona.amethyst.commons.ui.theme.HorzHalfVertPadding
import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel
import com.vitorpamplona.amethyst.ui.navigation.navs.rememberExtendedNav
import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.common.BasicRelaySetupInfoDialog
@Composable
@@ -34,6 +34,7 @@ import com.vitorpamplona.amethyst.commons.relays.ui.RelayCountResult
import com.vitorpamplona.amethyst.commons.relays.ui.RelayDragState
import com.vitorpamplona.amethyst.commons.relays.ui.rememberRelayDragState
import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav
import com.vitorpamplona.amethyst.commons.ui.navigation.navs.rememberExtendedNav
import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.relays.common.BasicRelaySetupInfo
import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.relays.common.RelayUrlEditField
import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.relays.common.relaySetupInfoBuilder
@@ -42,7 +43,6 @@ import com.vitorpamplona.amethyst.commons.ui.theme.HorzHalfVertPadding
import com.vitorpamplona.amethyst.commons.ui.theme.HorzPadding
import com.vitorpamplona.amethyst.commons.ui.theme.StdVertSpacer
import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel
import com.vitorpamplona.amethyst.ui.navigation.navs.rememberExtendedNav
import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.common.BasicRelaySetupInfoDialog
import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl
@@ -33,6 +33,7 @@ import com.vitorpamplona.amethyst.commons.model.cache.LocalCache
import com.vitorpamplona.amethyst.commons.relays.ui.RelayDragState
import com.vitorpamplona.amethyst.commons.relays.ui.rememberRelayDragState
import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav
import com.vitorpamplona.amethyst.commons.ui.navigation.navs.rememberExtendedNav
import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.relays.common.BasicRelaySetupInfo
import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.relays.common.RelayUrlEditField
import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.relays.common.relaySetupInfoBuilder
@@ -41,7 +42,6 @@ import com.vitorpamplona.amethyst.commons.ui.theme.HorzHalfVertPadding
import com.vitorpamplona.amethyst.commons.ui.theme.HorzPadding
import com.vitorpamplona.amethyst.commons.ui.theme.StdVertSpacer
import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel
import com.vitorpamplona.amethyst.ui.navigation.navs.rememberExtendedNav
import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.common.BasicRelaySetupInfoDialog
@Composable
@@ -34,6 +34,7 @@ import com.vitorpamplona.amethyst.commons.relays.ui.RelayCountResult
import com.vitorpamplona.amethyst.commons.relays.ui.RelayDragState
import com.vitorpamplona.amethyst.commons.relays.ui.rememberRelayDragState
import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav
import com.vitorpamplona.amethyst.commons.ui.navigation.navs.rememberExtendedNav
import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.relays.common.BasicRelaySetupInfo
import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.relays.common.RelayUrlEditField
import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.relays.common.relaySetupInfoBuilder
@@ -42,7 +43,6 @@ import com.vitorpamplona.amethyst.commons.ui.theme.HorzHalfVertPadding
import com.vitorpamplona.amethyst.commons.ui.theme.HorzPadding
import com.vitorpamplona.amethyst.commons.ui.theme.StdVertSpacer
import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel
import com.vitorpamplona.amethyst.ui.navigation.navs.rememberExtendedNav
import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.common.BasicRelaySetupInfoDialog
import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl
@@ -34,6 +34,7 @@ import com.vitorpamplona.amethyst.commons.relays.ui.RelayCountResult
import com.vitorpamplona.amethyst.commons.relays.ui.RelayDragState
import com.vitorpamplona.amethyst.commons.relays.ui.rememberRelayDragState
import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav
import com.vitorpamplona.amethyst.commons.ui.navigation.navs.rememberExtendedNav
import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.relays.common.BasicRelaySetupInfo
import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.relays.common.RelayUrlEditField
import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.relays.common.relaySetupInfoBuilder
@@ -42,7 +43,6 @@ import com.vitorpamplona.amethyst.commons.ui.theme.HorzHalfVertPadding
import com.vitorpamplona.amethyst.commons.ui.theme.HorzPadding
import com.vitorpamplona.amethyst.commons.ui.theme.StdVertSpacer
import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel
import com.vitorpamplona.amethyst.ui.navigation.navs.rememberExtendedNav
import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.common.BasicRelaySetupInfoDialog
import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl
@@ -37,6 +37,7 @@ import com.vitorpamplona.amethyst.commons.resources.Res
import com.vitorpamplona.amethyst.commons.resources.send_kind0_to_local_relay_description
import com.vitorpamplona.amethyst.commons.resources.send_kind0_to_local_relay_title
import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav
import com.vitorpamplona.amethyst.commons.ui.navigation.navs.rememberExtendedNav
import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.relays.common.BasicRelaySetupInfo
import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.relays.common.RelayUrlEditField
import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.relays.common.relaySetupInfoBuilder
@@ -47,7 +48,6 @@ import com.vitorpamplona.amethyst.commons.ui.theme.HorzPadding
import com.vitorpamplona.amethyst.commons.ui.theme.SettingsCategorySpacingWithHorzBorderModifier
import com.vitorpamplona.amethyst.commons.ui.theme.StdVertSpacer
import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel
import com.vitorpamplona.amethyst.ui.navigation.navs.rememberExtendedNav
import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.common.BasicRelaySetupInfoDialog
@Composable
@@ -34,6 +34,7 @@ import com.vitorpamplona.amethyst.commons.relays.ui.RelayCountResult
import com.vitorpamplona.amethyst.commons.relays.ui.RelayDragState
import com.vitorpamplona.amethyst.commons.relays.ui.rememberRelayDragState
import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav
import com.vitorpamplona.amethyst.commons.ui.navigation.navs.rememberExtendedNav
import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.relays.common.BasicRelaySetupInfo
import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.relays.common.RelayUrlEditField
import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.relays.common.relaySetupInfoBuilder
@@ -42,7 +43,6 @@ import com.vitorpamplona.amethyst.commons.ui.theme.HorzHalfVertPadding
import com.vitorpamplona.amethyst.commons.ui.theme.HorzPadding
import com.vitorpamplona.amethyst.commons.ui.theme.StdVertSpacer
import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel
import com.vitorpamplona.amethyst.ui.navigation.navs.rememberExtendedNav
import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.common.BasicRelaySetupInfoDialog
import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl
@@ -34,6 +34,7 @@ import com.vitorpamplona.amethyst.commons.relays.ui.RelayCountResult
import com.vitorpamplona.amethyst.commons.relays.ui.RelayDragState
import com.vitorpamplona.amethyst.commons.relays.ui.rememberRelayDragState
import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav
import com.vitorpamplona.amethyst.commons.ui.navigation.navs.rememberExtendedNav
import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.relays.common.BasicRelaySetupInfo
import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.relays.common.RelayUrlEditField
import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.relays.common.relaySetupInfoBuilder
@@ -42,7 +43,6 @@ import com.vitorpamplona.amethyst.commons.ui.theme.HorzHalfVertPadding
import com.vitorpamplona.amethyst.commons.ui.theme.HorzPadding
import com.vitorpamplona.amethyst.commons.ui.theme.StdVertSpacer
import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel
import com.vitorpamplona.amethyst.ui.navigation.navs.rememberExtendedNav
import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.common.BasicRelaySetupInfoDialog
import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl
@@ -34,6 +34,7 @@ import com.vitorpamplona.amethyst.commons.relays.ui.RelayCountResult
import com.vitorpamplona.amethyst.commons.relays.ui.RelayDragState
import com.vitorpamplona.amethyst.commons.relays.ui.rememberRelayDragState
import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav
import com.vitorpamplona.amethyst.commons.ui.navigation.navs.rememberExtendedNav
import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.relays.common.BasicRelaySetupInfo
import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.relays.common.RelayUrlEditField
import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.relays.common.relaySetupInfoBuilder
@@ -42,7 +43,6 @@ import com.vitorpamplona.amethyst.commons.ui.theme.HorzHalfVertPadding
import com.vitorpamplona.amethyst.commons.ui.theme.HorzPadding
import com.vitorpamplona.amethyst.commons.ui.theme.StdVertSpacer
import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel
import com.vitorpamplona.amethyst.ui.navigation.navs.rememberExtendedNav
import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.common.BasicRelaySetupInfoDialog
import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl
@@ -34,6 +34,7 @@ import com.vitorpamplona.amethyst.commons.relays.ui.RelayCountResult
import com.vitorpamplona.amethyst.commons.relays.ui.RelayDragState
import com.vitorpamplona.amethyst.commons.relays.ui.rememberRelayDragState
import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav
import com.vitorpamplona.amethyst.commons.ui.navigation.navs.rememberExtendedNav
import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.relays.common.BasicRelaySetupInfo
import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.relays.common.RelayUrlEditField
import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.relays.common.relaySetupInfoBuilder
@@ -42,7 +43,6 @@ import com.vitorpamplona.amethyst.commons.ui.theme.HorzHalfVertPadding
import com.vitorpamplona.amethyst.commons.ui.theme.HorzPadding
import com.vitorpamplona.amethyst.commons.ui.theme.StdVertSpacer
import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel
import com.vitorpamplona.amethyst.ui.navigation.navs.rememberExtendedNav
import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.common.BasicRelaySetupInfoDialog
import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl
@@ -33,6 +33,7 @@ import com.vitorpamplona.amethyst.commons.model.cache.LocalCache
import com.vitorpamplona.amethyst.commons.relays.ui.RelayDragState
import com.vitorpamplona.amethyst.commons.relays.ui.rememberRelayDragState
import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav
import com.vitorpamplona.amethyst.commons.ui.navigation.navs.rememberExtendedNav
import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.relays.common.BasicRelaySetupInfo
import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.relays.common.RelayUrlEditField
import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.relays.common.relaySetupInfoBuilder
@@ -41,7 +42,6 @@ import com.vitorpamplona.amethyst.commons.ui.theme.HorzHalfVertPadding
import com.vitorpamplona.amethyst.commons.ui.theme.HorzPadding
import com.vitorpamplona.amethyst.commons.ui.theme.StdVertSpacer
import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel
import com.vitorpamplona.amethyst.ui.navigation.navs.rememberExtendedNav
import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.common.BasicRelaySetupInfoDialog
@Composable
@@ -28,8 +28,8 @@ import androidx.core.net.toUri
import androidx.lifecycle.viewmodel.compose.viewModel
import com.vitorpamplona.amethyst.commons.model.cache.LocalCache
import com.vitorpamplona.amethyst.commons.service.uploads.SelectedMedia
import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav
import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel
import com.vitorpamplona.amethyst.ui.navigation.navs.Nav
import com.vitorpamplona.amethyst.ui.note.nip22Comments.CommentPostViewModel
import com.vitorpamplona.amethyst.ui.note.nip22Comments.GenericCommentPostScreen
import com.vitorpamplona.quartz.nip01Core.core.HexKey
@@ -47,7 +47,7 @@ fun UrlPostScreen(
quoteId: HexKey? = null,
draftId: HexKey? = null,
accountViewModel: AccountViewModel,
nav: Nav,
nav: INav,
) {
val postViewModel: CommentPostViewModel = viewModel()
postViewModel.init(accountViewModel)
@@ -53,4 +53,7 @@ interface AppServices {
/** Resolves `.bit` names and `d/`/`id/` identifiers over the configured ElectrumX servers. */
val namecoinResolver: NamecoinNameResolver
/** Finds the server behind a `blossom:` URI (BUD-10). */
val blossomServerFinder: BlossomServerFinder get() = BlossomServerFinder.None
}
@@ -0,0 +1,40 @@
/*
* 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
/**
* Resolves a BUD-10 `blossom:` URI to the URL of a server that holds the blob, probing the
* author's (and the viewer's) Blossom servers.
*/
interface BlossomServerFinder {
/** The server URL already resolved for [blossomUri], without probing. */
fun cachedServerUrl(blossomUri: String): String?
/** Probes until a server answers for [blossomUri]; null when none does. */
suspend fun findServerUrl(blossomUri: String): String?
/** Resolves nothing: front ends without a Blossom client yet. */
object None : BlossomServerFinder {
override fun cachedServerUrl(blossomUri: String): String? = null
override suspend fun findServerUrl(blossomUri: String): String? = null
}
}
@@ -0,0 +1,61 @@
/*
* Copyright (c) 2025 Vitor Pamplona
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
* the Software without restriction, including without limitation the rights to use,
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
* Software, and to permit persons to whom the Software is furnished to do so,
* subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
package com.vitorpamplona.amethyst.commons.ui.components
import android.content.Intent
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.platform.LocalContext
import androidx.core.content.FileProvider
import java.io.File
import java.io.FileOutputStream
import java.util.zip.ZipEntry
import java.util.zip.ZipOutputStream
@Composable
actual fun rememberFileSharer(): FileSharer {
val context = LocalContext.current
return remember(context) {
FileSharer { zipName, files, title ->
val zipFile = File(context.cacheDir, zipName)
ZipOutputStream(FileOutputStream(zipFile)).use { zip ->
files.forEach { (name, content) ->
zip.putNextEntry(ZipEntry(name))
zip.write(content.toByteArray())
zip.closeEntry()
}
}
val uri = FileProvider.getUriForFile(context, "${context.packageName}.provider", zipFile)
val sendIntent =
Intent().apply {
action = Intent.ACTION_SEND
type = "application/zip"
putExtra(Intent.EXTRA_STREAM, uri)
putExtra(Intent.EXTRA_TITLE, title)
addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
}
context.startActivity(Intent.createChooser(sendIntent, title))
}
}
}
@@ -18,7 +18,7 @@
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
package com.vitorpamplona.amethyst.service.relayClient.reqCommand.nwc
package com.vitorpamplona.amethyst.commons.relayClient.reqCommand.nwc
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
@@ -0,0 +1,43 @@
/*
* Copyright (c) 2025 Vitor Pamplona
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
* the Software without restriction, including without limitation the rights to use,
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
* Software, and to permit persons to whom the Software is furnished to do so,
* subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
package com.vitorpamplona.amethyst.commons.ui.components
import androidx.compose.runtime.Composable
/** Hands generated files to the platform's share sheet. */
fun interface FileSharer {
/**
* Packs [files] (name to text content) into a zip named [zipName] and shares it, behind a
* chooser titled [title] where the platform has one.
*/
fun shareTextFilesAsZip(
zipName: String,
files: List<Pair<String, String>>,
title: String,
)
}
/**
* The platform's file sharing: Android writes the zip to its cache and shares it through the
* app's `FileProvider`. Desktop and iOS have no share sheet wired yet and do nothing.
*/
@Composable
expect fun rememberFileSharer(): FileSharer
@@ -0,0 +1,37 @@
/*
* Copyright (c) 2025 Vitor Pamplona
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
* the Software without restriction, including without limitation the rights to use,
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
* Software, and to permit persons to whom the Software is furnished to do so,
* subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
package com.vitorpamplona.amethyst.commons.ui.navigation.navs
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
/** [nav], calling [onBeforeNavigate] first on every navigation (to close a sheet or dialog). */
@Composable
fun rememberExtendedNav(
nav: INav,
onBeforeNavigate: () -> Unit,
): INav {
val scope = rememberCoroutineScope()
return remember(nav, scope) {
ObservableNav(nav, scope, onBeforeNavigate)
}
}
@@ -0,0 +1,146 @@
/*
* Copyright (c) 2025 Vitor Pamplona
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
* the Software without restriction, including without limitation the rights to use,
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
* Software, and to permit persons to whom the Software is furnished to do so,
* subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
package com.vitorpamplona.amethyst.commons.ui.note
import androidx.compose.foundation.layout.Box
import androidx.compose.material3.LinearProgressIndicator
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.produceState
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment.Companion.Center
import androidx.compose.ui.graphics.StrokeCap
import androidx.compose.ui.unit.dp
import com.vitorpamplona.amethyst.commons.model.Note
import com.vitorpamplona.amethyst.commons.model.ZapraiserStatus
import com.vitorpamplona.amethyst.commons.relayClient.event.observeNoteZaps
import com.vitorpamplona.amethyst.commons.relayClient.reqCommand.nwc.NWCFinderFilterAssemblerSubscription
import com.vitorpamplona.amethyst.commons.resources.Res
import com.vitorpamplona.amethyst.commons.resources.sats_to_complete
import com.vitorpamplona.amethyst.commons.ui.stringRes
import com.vitorpamplona.amethyst.commons.ui.theme.Font14SP
import com.vitorpamplona.amethyst.commons.ui.theme.Height24dpFilledModifier
import com.vitorpamplona.amethyst.commons.ui.theme.Height4dpFilledModifier
import com.vitorpamplona.amethyst.commons.ui.theme.NoSoTinyBorders
import com.vitorpamplona.amethyst.commons.ui.theme.TinyBorders
import com.vitorpamplona.amethyst.commons.ui.theme.fundraiserProgressColor
import com.vitorpamplona.amethyst.commons.util.showAmount
import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel
import com.vitorpamplona.quartz.nip57Zaps.zapraiser.zapraiserAmount
import com.vitorpamplona.quartz.utils.BigDecimal
import com.vitorpamplona.quartz.utils.plus
import kotlin.math.roundToInt
@Composable
fun ObserveZapAmountText(
baseNote: Note,
accountViewModel: AccountViewModel,
inner: @Composable (String) -> Unit,
) {
val zapsState by observeNoteZaps(baseNote, accountViewModel)
if (zapsState?.note?.zapPayments?.isNotEmpty() == true) {
zapsState?.note?.zapPayments?.forEach {
if (it.value == null) {
NWCFinderFilterAssemblerSubscription(it.key, accountViewModel)
}
}
@Suppress("ProduceStateDoesNotAssignValue")
val zapAmountTxt by
produceState(initialValue = showAmount(baseNote.zapsAmount), key1 = zapsState) {
zapsState?.note?.let {
val newZapAmount = accountViewModel.calculateZapAmount(it)
if (value != newZapAmount) {
value = newZapAmount
}
}
}
inner(zapAmountTxt)
} else {
// Include the signed-in user's own pending onchain zaps so
// the counter reflects the optimistic value the gallery shows.
val ownPubKey = accountViewModel.account.userProfile().pubkeyHex
val note = zapsState?.note
val total =
(note?.zapsAmount ?: BigDecimal(0)) +
BigDecimal(note?.extraOwnPendingOnchainSats(ownPubKey) ?: 0L)
inner(showAmount(total))
}
}
@Composable
fun RenderZapRaiser(
baseNote: Note,
zapraiserAmount: Long,
details: Boolean,
accountViewModel: AccountViewModel,
) {
val zapsState by observeNoteZaps(baseNote, accountViewModel)
var zapraiserStatus by remember { mutableStateOf(ZapraiserStatus(0F, "$zapraiserAmount")) }
LaunchedEffect(key1 = zapsState) {
zapsState?.note?.let {
val newStatus = accountViewModel.calculateZapraiser(baseNote)
if (zapraiserStatus != newStatus) {
zapraiserStatus = newStatus
}
}
}
LinearProgressIndicator(
modifier = if (details) Height24dpFilledModifier else Height4dpFilledModifier,
color = MaterialTheme.colorScheme.fundraiserProgressColor,
progress = { zapraiserStatus.progress },
gapSize = 0.dp,
strokeCap = StrokeCap.Square,
drawStopIndicator = {},
)
if (details) {
Box(
contentAlignment = Center,
modifier = TinyBorders,
) {
val totalPercentage by
remember(zapraiserStatus) {
derivedStateOf { "${(zapraiserStatus.progress * 100).roundToInt()}%" }
}
Text(
text =
stringRes(id = Res.string.sats_to_complete, totalPercentage, zapraiserStatus.left),
modifier = NoSoTinyBorders,
// color = MaterialTheme.colorScheme.placeholderText,
fontSize = Font14SP,
maxLines = 1,
)
}
}
}
@@ -426,6 +426,19 @@ interface NotePlatform {
callbacks: ZapButtonCallbacks,
) {}
/**
* The user's zap presets as rail-aware amount chips that zap [note] straight away (the chat
* long-press sheet). [onOnchainRequest] hands an on-chain amount to the caller's dialog.
*/
@Composable
fun QuickZapAmountRow(
note: Note,
onDismiss: () -> Unit,
onOnchainRequest: (Long?) -> Unit,
accountViewModel: AccountViewModel,
nav: INav,
) {}
/** The editor for a new version of [edit], shown while [versionLookingAt] is on screen. */
@Composable
fun EditPostView(
@@ -559,6 +559,15 @@ fun ReusableZapButton(
callbacks: ZapButtonCallbacks = ZapButtonCallbacks(),
) = LocalNotePlatform.current.ReusableZapButton(baseNote, accountViewModel, nav, config, callbacks)
@Composable
fun QuickZapAmountRow(
note: Note,
onDismiss: () -> Unit,
onOnchainRequest: (Long?) -> Unit,
accountViewModel: AccountViewModel,
nav: INav,
) = LocalNotePlatform.current.QuickZapAmountRow(note, onDismiss, onOnchainRequest, accountViewModel, nav)
@Composable
fun EditPostView(
onClose: () -> Unit,
@@ -18,35 +18,20 @@
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
package com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.common
package com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.relays.common
import android.content.Context
import android.content.Intent
import com.vitorpamplona.amethyst.commons.resources.Res
import com.vitorpamplona.amethyst.commons.resources.export_relay_settings
import com.vitorpamplona.amethyst.commons.resources.relay_settings
import com.vitorpamplona.amethyst.commons.ui.components.TextSharer
import com.vitorpamplona.amethyst.commons.ui.loadStringRes
class RelayExporter(
val context: Context,
val sharer: TextSharer,
) {
suspend fun export(collection: RelayListCollection) {
val text = buildExportText(collection)
val sendIntent =
Intent().apply {
action = Intent.ACTION_SEND
type = "text/plain"
putExtra(Intent.EXTRA_TEXT, text)
putExtra(Intent.EXTRA_TITLE, loadStringRes(Res.string.export_relay_settings))
}
val shareIntent =
Intent.createChooser(
sendIntent,
loadStringRes(Res.string.export_relay_settings),
)
context.startActivity(shareIntent)
val title = loadStringRes(Res.string.export_relay_settings)
sharer.share(buildExportText(collection), title, title)
}
suspend fun buildExportText(collection: RelayListCollection): String {
@@ -18,7 +18,7 @@
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
package com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.common
package com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.relays.common
import com.vitorpamplona.amethyst.commons.resources.Res
import com.vitorpamplona.amethyst.commons.resources.blocked_section
@@ -45,7 +45,6 @@ import com.vitorpamplona.amethyst.commons.resources.search_section
import com.vitorpamplona.amethyst.commons.resources.search_section_explainer
import com.vitorpamplona.amethyst.commons.resources.trusted_section
import com.vitorpamplona.amethyst.commons.resources.trusted_section_explainer
import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.relays.common.BasicRelaySetupInfo
import org.jetbrains.compose.resources.StringResource
data class RelayListCollection(
@@ -18,67 +18,30 @@
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
package com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.common
package com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.relays.common
import android.content.Context
import android.content.Intent
import androidx.core.content.FileProvider
import com.vitorpamplona.amethyst.commons.resources.Res
import com.vitorpamplona.amethyst.commons.resources.export_relay_settings
import com.vitorpamplona.amethyst.commons.ui.components.FileSharer
import com.vitorpamplona.amethyst.commons.ui.loadStringRes
import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.relays.common.BasicRelaySetupInfo
import java.io.File
import java.io.FileOutputStream
import java.util.zip.ZipEntry
import java.util.zip.ZipOutputStream
/** Exports each non-empty relay section as `<section>.json` (an array of URLs) in one zip. */
class RelayZipExporter(
val context: Context,
val sharer: FileSharer,
) {
suspend fun export(collection: RelayListCollection) {
val zipFile = buildZipFile(collection)
val uri =
FileProvider.getUriForFile(
context,
"${context.packageName}.provider",
zipFile,
)
val sendIntent =
Intent().apply {
action = Intent.ACTION_SEND
type = "application/zip"
putExtra(Intent.EXTRA_STREAM, uri)
putExtra(Intent.EXTRA_TITLE, loadStringRes(Res.string.export_relay_settings))
addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
}
val shareIntent =
Intent.createChooser(
sendIntent,
loadStringRes(Res.string.export_relay_settings),
)
context.startActivity(shareIntent)
sharer.shareTextFilesAsZip(
zipName = "relay_settings.zip",
files = buildFiles(collection),
title = loadStringRes(Res.string.export_relay_settings),
)
}
fun buildZipFile(collection: RelayListCollection): File {
val zipFile = File(context.cacheDir, "relay_settings.zip")
ZipOutputStream(FileOutputStream(zipFile)).use { zip ->
collection.sections().forEach { section ->
if (section.relays.isNotEmpty()) {
val json = buildJsonArray(section.relays)
zip.putNextEntry(ZipEntry("${section.fileName}.json"))
zip.write(json.toByteArray())
zip.closeEntry()
}
}
fun buildFiles(collection: RelayListCollection): List<Pair<String, String>> =
collection.sections().mapNotNull { section ->
if (section.relays.isEmpty()) null else "${section.fileName}.json" to buildJsonArray(section.relays)
}
return zipFile
}
private fun buildJsonArray(relays: List<BasicRelaySetupInfo>): String {
val builder = StringBuilder()
builder.appendLine("[")
@@ -0,0 +1,29 @@
/*
* Copyright (c) 2025 Vitor Pamplona
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
* the Software without restriction, including without limitation the rights to use,
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
* Software, and to permit persons to whom the Software is furnished to do so,
* subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
package com.vitorpamplona.amethyst.commons.ui.components
import androidx.compose.runtime.Composable
private val NoFileSharer = FileSharer { _, _, _ -> }
/** No share sheet wired yet. */
@Composable
actual fun rememberFileSharer(): FileSharer = NoFileSharer
@@ -0,0 +1,29 @@
/*
* Copyright (c) 2025 Vitor Pamplona
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
* the Software without restriction, including without limitation the rights to use,
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
* Software, and to permit persons to whom the Software is furnished to do so,
* subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
package com.vitorpamplona.amethyst.commons.ui.components
import androidx.compose.runtime.Composable
private val NoFileSharer = FileSharer { _, _, _ -> }
/** No share sheet wired yet. */
@Composable
actual fun rememberFileSharer(): FileSharer = NoFileSharer