fixes the need for coroutine scopes

This commit is contained in:
Vitor Pamplona
2026-03-28 15:26:29 -04:00
parent c0212c09e5
commit 4db617aeef
23 changed files with 273 additions and 87 deletions
@@ -38,17 +38,19 @@ import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalClipboard
import androidx.compose.ui.text.AnnotatedString
import androidx.compose.ui.unit.dp
import com.vitorpamplona.amethyst.R
import com.vitorpamplona.amethyst.ui.components.toasts.ThrowableToastMsg
import com.vitorpamplona.amethyst.ui.components.toasts.ThrowableToastMsg2
import com.vitorpamplona.amethyst.ui.components.util.setText
import com.vitorpamplona.amethyst.ui.stringRes
import com.vitorpamplona.amethyst.ui.theme.Size16dp
import com.vitorpamplona.amethyst.ui.theme.StdHorzSpacer
import kotlinx.coroutines.launch
import java.io.PrintWriter
import java.io.StringWriter
@@ -120,8 +122,11 @@ fun InformationDialog(
) {
moreInfo?.let {
val clipboardManager = LocalClipboard.current
val scope = rememberCoroutineScope()
TextButton(onClick = {
clipboardManager.setText(AnnotatedString(it))
scope.launch {
clipboardManager.setText(it)
}
}) {
Text(stringRes(R.string.copy_stack_to_clipboard))
}
@@ -40,13 +40,13 @@ import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.produceState
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalClipboard
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.text.AnnotatedString
import androidx.compose.ui.text.style.TextDirection
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
@@ -58,6 +58,7 @@ import com.vitorpamplona.amethyst.commons.hashtags.CustomHashTagIcons
import com.vitorpamplona.amethyst.service.cashu.CachedCashuParser
import com.vitorpamplona.amethyst.service.cashu.CashuToken
import com.vitorpamplona.amethyst.ui.actions.CrossfadeIfEnabled
import com.vitorpamplona.amethyst.ui.components.util.setText
import com.vitorpamplona.amethyst.ui.note.CopyIcon
import com.vitorpamplona.amethyst.ui.note.OpenInNewIcon
import com.vitorpamplona.amethyst.ui.note.ZapIcon
@@ -72,6 +73,7 @@ import com.vitorpamplona.amethyst.ui.theme.ThemeComparisonColumn
import kotlinx.collections.immutable.ImmutableList
import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
@Composable
@@ -141,7 +143,6 @@ fun CashuPreviewNew(
toast: (String, String) -> Unit,
) {
val context = LocalContext.current
val clipboardManager = LocalClipboard.current
Card(
modifier = CashuCardBorders,
@@ -230,10 +231,15 @@ fun CashuPreviewNew(
Spacer(modifier = StdHorzSpacer)
val clipboardManager = LocalClipboard.current
val scope = rememberCoroutineScope()
FilledTonalButton(
onClick = {
scope.launch {
// Copying the token to clipboard
clipboardManager.setText(AnnotatedString(token.token))
clipboardManager.setText(token.token)
}
},
shape = SmallishBorder,
contentPadding = PaddingValues(0.dp),
@@ -25,12 +25,14 @@ import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalClipboard
import androidx.compose.ui.text.AnnotatedString
import androidx.compose.ui.text.style.TextOverflow
import com.vitorpamplona.amethyst.ui.components.util.setText
import com.vitorpamplona.amethyst.ui.navigation.navs.INav
import com.vitorpamplona.amethyst.ui.navigation.routes.Route
import kotlinx.coroutines.launch
@Composable
fun ClickableRelayUrl(
@@ -38,11 +40,16 @@ fun ClickableRelayUrl(
nav: INav,
) {
val clipboardManager = LocalClipboard.current
val scope = rememberCoroutineScope()
val clickableModifier =
remember(relayUrl) {
Modifier
.combinedClickable(
onLongClick = { clipboardManager.setText(AnnotatedString(relayUrl)) },
onLongClick = {
scope.launch {
clipboardManager.setText(relayUrl)
}
},
onClick = { nav.nav(Route.RelayInfo(relayUrl)) },
)
}
@@ -31,20 +31,22 @@ import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.platform.LocalClipboard
import androidx.compose.ui.platform.LocalUriHandler
import androidx.compose.ui.text.AnnotatedString
import androidx.compose.ui.text.style.TextOverflow
import coil3.compose.AsyncImage
import com.vitorpamplona.amethyst.R
import com.vitorpamplona.amethyst.commons.preview.UrlInfoItem
import com.vitorpamplona.amethyst.ui.components.util.setText
import com.vitorpamplona.amethyst.ui.stringRes
import com.vitorpamplona.amethyst.ui.theme.DoubleVertSpacer
import com.vitorpamplona.amethyst.ui.theme.MaxWidthWithHorzPadding
import com.vitorpamplona.amethyst.ui.theme.innerPostModifier
import com.vitorpamplona.amethyst.ui.theme.previewCardImageModifier
import kotlinx.coroutines.launch
@OptIn(ExperimentalFoundationApi::class)
@Composable
@@ -60,6 +62,7 @@ fun UrlPreviewCard(
if (popupExpanded.value) {
val clipboardManager = LocalClipboard.current
val scope = rememberCoroutineScope()
M3ActionDialog(
title = stringRes(R.string.link_actions_dialog_title),
onDismiss = { popupExpanded.value = false },
@@ -69,12 +72,14 @@ fun UrlPreviewCard(
icon = Icons.Outlined.ContentCopy,
text = stringRes(R.string.copy_url_to_clipboard),
) {
clipboardManager.setText(AnnotatedString(url))
scope.launch {
clipboardManager.setText(url)
popupExpanded.value = false
}
}
}
}
}
Column(
modifier =
@@ -54,6 +54,7 @@ import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
@@ -62,7 +63,6 @@ import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.platform.LocalClipboard
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalUriHandler
import androidx.compose.ui.text.AnnotatedString
import androidx.compose.ui.text.SpanStyle
import androidx.compose.ui.text.buildAnnotatedString
import androidx.compose.ui.text.style.TextOverflow
@@ -90,6 +90,7 @@ import com.vitorpamplona.amethyst.service.playback.composable.VideoView
import com.vitorpamplona.amethyst.service.uploads.blossom.bud10.openBlossomUriAsIntent
import com.vitorpamplona.amethyst.ui.actions.CrossfadeIfEnabled
import com.vitorpamplona.amethyst.ui.actions.InformationDialog
import com.vitorpamplona.amethyst.ui.components.util.setText
import com.vitorpamplona.amethyst.ui.note.BlankNote
import com.vitorpamplona.amethyst.ui.note.DownloadForOfflineIcon
import com.vitorpamplona.amethyst.ui.painterRes
@@ -772,19 +773,24 @@ fun ShareMediaAction(
onDismiss = { if (!isDownloadingVideo.value) onDismiss() },
) {
val clipboardManager = LocalClipboard.current
val scope = rememberCoroutineScope()
// Copy & Gallery section
if ((videoUri != null && !videoUri.startsWith("file")) || postNostrUri != null) {
M3ActionSection {
if (videoUri != null && !videoUri.startsWith("file")) {
M3ActionRow(icon = Icons.Outlined.Link, text = stringRes(R.string.copy_url_to_clipboard)) {
clipboardManager.setText(AnnotatedString(videoUri))
scope.launch {
clipboardManager.setText(videoUri)
}
onDismiss()
}
}
postNostrUri?.let {
M3ActionRow(icon = Icons.Outlined.ContentCopy, text = stringRes(R.string.copy_the_note_id_to_the_clipboard)) {
clipboardManager.setText(AnnotatedString(it))
scope.launch {
clipboardManager.setText(it)
}
onDismiss()
}
M3ActionRow(icon = Icons.Outlined.Collections, text = stringRes(R.string.add_media_to_gallery)) {
@@ -0,0 +1,36 @@
/*
* 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.components.util
import android.content.ClipData
import androidx.compose.ui.platform.ClipEntry
import androidx.compose.ui.platform.Clipboard
suspend fun Clipboard.setText(text: String) {
setClipEntry(ClipEntry(ClipData.newPlainText("", text)))
}
suspend fun Clipboard.getText(): String? =
getClipEntry()
?.clipData
?.getItemAt(0)
?.text
?.toString()
@@ -70,7 +70,6 @@ import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.platform.LocalClipboard
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.vectorResource
import androidx.compose.ui.text.AnnotatedString
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
@@ -80,6 +79,7 @@ import com.vitorpamplona.amethyst.R
import com.vitorpamplona.amethyst.model.AddressableNote
import com.vitorpamplona.amethyst.model.Note
import com.vitorpamplona.amethyst.model.User
import com.vitorpamplona.amethyst.ui.components.util.setText
import com.vitorpamplona.amethyst.ui.navigation.navs.INav
import com.vitorpamplona.amethyst.ui.navigation.routes.routeEditDraftTo
import com.vitorpamplona.amethyst.ui.painterRes
@@ -289,9 +289,11 @@ fun CardBody(
label = stringRes(R.string.quick_action_copy_text),
) {
accountViewModel.decrypt(note) {
clipboardManager.setText(AnnotatedString(it))
scope.launch {
clipboardManager.setText(it)
showToast(R.string.copied_note_text_to_clipboard)
}
}
onDismiss()
}
@@ -302,7 +304,7 @@ fun CardBody(
) {
note.author?.let {
scope.launch {
clipboardManager.setText(AnnotatedString(it.toNostrUri()))
clipboardManager.setText(it.toNostrUri())
showToast(R.string.copied_user_id_to_clipboard)
onDismiss()
}
@@ -314,7 +316,7 @@ fun CardBody(
stringRes(R.string.quick_action_copy_note_id),
) {
scope.launch {
clipboardManager.setText(AnnotatedString(note.toNostrUri()))
clipboardManager.setText(note.toNostrUri())
showToast(R.string.copied_note_id_to_clipboard)
onDismiss()
}
@@ -39,11 +39,11 @@ import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.platform.LocalClipboard
import androidx.compose.ui.text.AnnotatedString
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
@@ -54,6 +54,7 @@ import com.vitorpamplona.amethyst.model.Note
import com.vitorpamplona.amethyst.model.nip11RelayInfo.loadRelayInfo
import com.vitorpamplona.amethyst.ui.components.ClickableBox
import com.vitorpamplona.amethyst.ui.components.RobohashFallbackAsyncImage
import com.vitorpamplona.amethyst.ui.components.util.setText
import com.vitorpamplona.amethyst.ui.navigation.navs.INav
import com.vitorpamplona.amethyst.ui.navigation.routes.Route
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
@@ -71,6 +72,7 @@ import com.vitorpamplona.amethyst.ui.theme.relayIconModifier
import com.vitorpamplona.amethyst.ui.theme.ripple24dp
import com.vitorpamplona.amethyst.ui.theme.warningColorOnSecondSurface
import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl
import kotlinx.coroutines.launch
private const val DAMUS_RELAY_URL = "wss://relay.damus.io"
@@ -130,6 +132,7 @@ fun RenderRelay(
val relayInfo by loadRelayInfo(relay)
val clipboardManager = LocalClipboard.current
val scope = rememberCoroutineScope()
val clickableModifier =
remember(relay) {
Modifier
@@ -138,7 +141,9 @@ fun RenderRelay(
indication = ripple24dp,
interactionSource = MutableInteractionSource(),
onLongClick = {
clipboardManager.setText(AnnotatedString(relay.url))
scope.launch {
clipboardManager.setText(relay.url)
}
},
onClick = { nav.nav(Route.RelayInfo(relay.url)) },
)
@@ -84,6 +84,7 @@ import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
@@ -102,6 +103,7 @@ import androidx.lifecycle.viewmodel.compose.viewModel
import com.vitorpamplona.amethyst.R
import com.vitorpamplona.amethyst.ui.components.TextSpinner
import com.vitorpamplona.amethyst.ui.components.TitleExplainer
import com.vitorpamplona.amethyst.ui.components.util.getText
import com.vitorpamplona.amethyst.ui.painterRes
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
import com.vitorpamplona.amethyst.ui.screen.loggedIn.keyBackup.getFragmentActivity
@@ -120,6 +122,7 @@ import com.vitorpamplona.amethyst.ui.theme.ThemeComparisonRow
import com.vitorpamplona.amethyst.ui.theme.placeholderText
import com.vitorpamplona.quartz.nip57Zaps.LnZapEvent
import kotlinx.collections.immutable.toImmutableList
import kotlinx.coroutines.launch
@OptIn(ExperimentalLayoutApi::class)
@Composable
@@ -149,6 +152,7 @@ fun UpdateZapAmountContent(
val context = LocalContext.current
val clipboardManager = LocalClipboard.current
val uri = LocalUriHandler.current
val scope = rememberCoroutineScope()
val zapTypes =
listOf(
@@ -443,7 +447,8 @@ fun UpdateZapAmountContent(
// Paste from clipboard
IconButton(
onClick = {
val clipText = clipboardManager.getText()?.text
scope.launch {
val clipText = clipboardManager.getText()
try {
clipText?.let { postViewModel.copyFromClipboard(it) }
} catch (e: IllegalArgumentException) {
@@ -453,6 +458,7 @@ fun UpdateZapAmountContent(
clipText ?: "",
)
}
}
},
) {
Icon(
@@ -48,7 +48,6 @@ import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue
import androidx.compose.ui.platform.LocalClipboard
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.text.AnnotatedString
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.vitorpamplona.amethyst.R
import com.vitorpamplona.amethyst.model.AddressableNote
@@ -59,6 +58,7 @@ import com.vitorpamplona.amethyst.ui.components.GenericLoadable
import com.vitorpamplona.amethyst.ui.components.M3ActionDialog
import com.vitorpamplona.amethyst.ui.components.M3ActionRow
import com.vitorpamplona.amethyst.ui.components.M3ActionSection
import com.vitorpamplona.amethyst.ui.components.util.setText
import com.vitorpamplona.amethyst.ui.navigation.navs.INav
import com.vitorpamplona.amethyst.ui.navigation.routes.Route
import com.vitorpamplona.amethyst.ui.navigation.routes.routeEditDraftTo
@@ -196,20 +196,24 @@ fun NoteDropDownMenu(
M3ActionSection {
M3ActionRow(icon = Icons.Outlined.ContentCopy, text = stringRes(R.string.copy_text)) {
val lastNoteVersion = (editState?.value as? GenericLoadable.Loaded)?.loaded?.modificationToShow?.value ?: note
accountViewModel.decrypt(lastNoteVersion) { clipboardManager.setText(AnnotatedString(it)) }
accountViewModel.decrypt(lastNoteVersion) {
scope.launch {
clipboardManager.setText(it)
}
}
onDismiss()
}
M3ActionRow(icon = Icons.Outlined.ContentCopy, text = stringRes(R.string.copy_user_pubkey)) {
note.author?.let {
scope.launch(Dispatchers.IO) {
clipboardManager.setText(AnnotatedString("nostr:${it.pubkeyNpub()}"))
clipboardManager.setText("nostr:${it.pubkeyNpub()}")
onDismiss()
}
}
}
M3ActionRow(icon = Icons.Outlined.ContentCopy, text = stringRes(R.string.copy_note_id)) {
scope.launch(Dispatchers.IO) {
clipboardManager.setText(AnnotatedString(note.toNostrUri()))
clipboardManager.setText(note.toNostrUri())
onDismiss()
}
}
@@ -42,6 +42,7 @@ import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
@@ -49,7 +50,6 @@ import androidx.compose.ui.draw.clip
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.platform.LocalClipboard
import androidx.compose.ui.platform.LocalUriHandler
import androidx.compose.ui.text.AnnotatedString
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
@@ -63,6 +63,7 @@ import com.vitorpamplona.amethyst.ui.components.ClickableTextPrimary
import com.vitorpamplona.amethyst.ui.components.CreateTextWithEmoji
import com.vitorpamplona.amethyst.ui.components.TranslatableRichTextViewer
import com.vitorpamplona.amethyst.ui.components.ZoomableImageDialog
import com.vitorpamplona.amethyst.ui.components.util.setText
import com.vitorpamplona.amethyst.ui.navigation.navs.INav
import com.vitorpamplona.amethyst.ui.note.LinkIcon
import com.vitorpamplona.amethyst.ui.painterRes
@@ -74,6 +75,7 @@ import com.vitorpamplona.amethyst.ui.theme.placeholderText
import com.vitorpamplona.quartz.nip89AppHandlers.definition.AppDefinitionEvent
import com.vitorpamplona.quartz.nip89AppHandlers.definition.AppMetadata
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
@OptIn(ExperimentalFoundationApi::class)
@@ -94,6 +96,7 @@ fun RenderAppDefinition(
metadata?.let { theAppMetadata ->
Box {
val clipboardManager = LocalClipboard.current
val scope = rememberCoroutineScope()
val uri = LocalUriHandler.current
if (!theAppMetadata.banner.isNullOrBlank()) {
@@ -109,7 +112,13 @@ fun RenderAppDefinition(
.height(125.dp)
.combinedClickable(
onClick = {},
onLongClick = { clipboardManager.setText(AnnotatedString(theAppMetadata.banner!!)) },
onLongClick = {
theAppMetadata.banner?.let {
scope.launch {
clipboardManager.setText(it)
}
}
},
),
)
@@ -161,7 +170,11 @@ fun RenderAppDefinition(
.background(MaterialTheme.colorScheme.background)
.combinedClickable(
onClick = { zoomImageDialogOpen = true },
onLongClick = { clipboardManager.setText(AnnotatedString(picture)) },
onLongClick = {
scope.launch {
clipboardManager.setText(picture)
}
},
),
)
@@ -33,12 +33,12 @@ import androidx.compose.runtime.MutableState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalClipboard
import androidx.compose.ui.text.AnnotatedString
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.text.style.TextOverflow
@@ -49,6 +49,7 @@ import com.vitorpamplona.amethyst.model.Note
import com.vitorpamplona.amethyst.model.nip51Lists.relayLists.RelayListCard
import com.vitorpamplona.amethyst.service.relayClient.reqCommand.user.observeUserRelayIntoList
import com.vitorpamplona.amethyst.ui.components.ShowMoreButton
import com.vitorpamplona.amethyst.ui.components.util.setText
import com.vitorpamplona.amethyst.ui.navigation.navs.INav
import com.vitorpamplona.amethyst.ui.navigation.routes.Route
import com.vitorpamplona.amethyst.ui.note.AddRelayButton
@@ -63,6 +64,7 @@ import com.vitorpamplona.quartz.nip17Dm.settings.ChatMessageRelayListEvent
import com.vitorpamplona.quartz.nip51Lists.relaySets.RelaySetEvent
import com.vitorpamplona.quartz.nip65RelayList.AdvertisedRelayListEvent
import kotlinx.collections.immutable.toImmutableList
import kotlinx.coroutines.launch
@Composable
fun DisplayRelaySet(
@@ -411,12 +413,15 @@ private fun RelayOptionsAction(
) {
val isCurrentlyOnTheUsersList by observeUserRelayIntoList(relay, accountViewModel)
val clipboardManager = LocalClipboard.current
val scope = rememberCoroutineScope()
if (isCurrentlyOnTheUsersList) {
AddRelayButton {
clipboardManager.setText(AnnotatedString(relay.url))
scope.launch {
clipboardManager.setText(relay.url)
nav.nav(Route.EditRelays)
}
}
} else {
RemoveRelayButton {
nav.nav(Route.EditRelays)
@@ -43,7 +43,6 @@ import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue
import androidx.compose.ui.platform.LocalClipboard
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.text.AnnotatedString
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.vitorpamplona.amethyst.R
import com.vitorpamplona.amethyst.model.Note
@@ -53,6 +52,7 @@ import com.vitorpamplona.amethyst.ui.components.GenericLoadable
import com.vitorpamplona.amethyst.ui.components.M3ActionDialog
import com.vitorpamplona.amethyst.ui.components.M3ActionRow
import com.vitorpamplona.amethyst.ui.components.M3ActionSection
import com.vitorpamplona.amethyst.ui.components.util.setText
import com.vitorpamplona.amethyst.ui.navigation.navs.INav
import com.vitorpamplona.amethyst.ui.navigation.routes.Route
import com.vitorpamplona.amethyst.ui.navigation.routes.routeEditDraftTo
@@ -202,20 +202,24 @@ fun BookmarkGroupItemOptionsMenu(
M3ActionSection {
M3ActionRow(icon = Icons.Outlined.ContentCopy, text = stringRes(R.string.copy_text)) {
val lastNoteVersion = (editState?.value as? GenericLoadable.Loaded)?.loaded?.modificationToShow?.value ?: note
accountViewModel.decrypt(lastNoteVersion) { clipboardManager.setText(AnnotatedString(it)) }
accountViewModel.decrypt(lastNoteVersion) {
scope.launch {
clipboardManager.setText(it)
}
}
onDismiss()
}
M3ActionRow(icon = Icons.Outlined.ContentCopy, text = stringRes(R.string.copy_user_pubkey)) {
note.author?.let {
scope.launch(Dispatchers.IO) {
clipboardManager.setText(AnnotatedString("nostr:${it.pubkeyNpub()}"))
clipboardManager.setText("nostr:${it.pubkeyNpub()}")
onDismiss()
}
}
}
M3ActionRow(icon = Icons.Outlined.ContentCopy, text = stringRes(R.string.copy_note_id)) {
scope.launch(Dispatchers.IO) {
clipboardManager.setText(AnnotatedString(note.toNostrUri()))
clipboardManager.setText(note.toNostrUri())
onDismiss()
}
}
@@ -37,12 +37,12 @@ import androidx.compose.runtime.MutableState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalClipboard
import androidx.compose.ui.text.AnnotatedString
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
@@ -57,6 +57,7 @@ import com.vitorpamplona.amethyst.model.nip11RelayInfo.loadRelayInfo
import com.vitorpamplona.amethyst.ui.components.CreateTextWithEmoji
import com.vitorpamplona.amethyst.ui.components.RobohashFallbackAsyncImage
import com.vitorpamplona.amethyst.ui.components.TranslatableRichTextViewer
import com.vitorpamplona.amethyst.ui.components.util.setText
import com.vitorpamplona.amethyst.ui.navigation.navs.EmptyNav
import com.vitorpamplona.amethyst.ui.navigation.navs.INav
import com.vitorpamplona.amethyst.ui.navigation.routes.Route
@@ -74,6 +75,7 @@ import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl
import com.vitorpamplona.quartz.nip01Core.relay.normalizer.displayUrl
import com.vitorpamplona.quartz.nip28PublicChat.admin.ChannelCreateEvent
import com.vitorpamplona.quartz.nip28PublicChat.base.ChannelDataNorm
import kotlinx.coroutines.launch
@Composable
fun RenderCreateChannelNote(
@@ -238,12 +240,15 @@ fun RenderRelayLinePublicChat(
@Suppress("ProduceStateDoesNotAssignValue")
val relayInfo by loadRelayInfo(relay)
val scope = rememberCoroutineScope()
val clipboardManager = LocalClipboard.current
val clickableModifier =
remember(relay) {
Modifier.combinedClickable(
onLongClick = {
clipboardManager.setText(AnnotatedString(relay.url))
scope.launch {
clipboardManager.setText(relay.url)
}
},
onClick = { nav.nav(Route.RelayInfo(relay.url)) },
)
@@ -63,12 +63,11 @@ import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.autofill.ContentType
import androidx.compose.ui.platform.ClipboardManager
import androidx.compose.ui.platform.Clipboard
import androidx.compose.ui.platform.LocalClipboard
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.semantics.contentType
import androidx.compose.ui.semantics.semantics
import androidx.compose.ui.text.AnnotatedString
import androidx.compose.ui.text.input.ImeAction
import androidx.compose.ui.text.input.KeyboardType
import androidx.compose.ui.text.input.PasswordVisualTransformation
@@ -87,6 +86,7 @@ import com.halilibo.richtext.ui.material3.RichText
import com.halilibo.richtext.ui.resolveDefaults
import com.vitorpamplona.amethyst.R
import com.vitorpamplona.amethyst.model.Account
import com.vitorpamplona.amethyst.ui.components.util.setText
import com.vitorpamplona.amethyst.ui.navigation.navs.INav
import com.vitorpamplona.amethyst.ui.navigation.topbars.TopBarWithBackButton
import com.vitorpamplona.amethyst.ui.note.authenticate
@@ -366,11 +366,11 @@ private fun copyNSec(
context: Context,
scope: CoroutineScope,
account: Account,
clipboardManager: ClipboardManager,
clipboardManager: Clipboard,
) {
account.settings.keyPair.privKey?.let {
clipboardManager.setText(AnnotatedString(it.toNsec()))
scope.launch {
clipboardManager.setText(it.toNsec())
Toast
.makeText(
context,
@@ -386,7 +386,7 @@ private fun encryptCopyNSec(
context: Context,
scope: CoroutineScope,
accountViewModel: AccountViewModel,
clipboardManager: ClipboardManager,
clipboardManager: Clipboard,
) {
if (password.value.text.isBlank()) {
scope.launch {
@@ -401,8 +401,8 @@ private fun encryptCopyNSec(
accountViewModel.account.settings.keyPair.privKey?.let {
val key = runCatching { Nip49().encrypt(it.toHexKey(), password.value.text) }.getOrNull()
if (key != null) {
clipboardManager.setText(AnnotatedString(key))
scope.launch {
clipboardManager.setText(key)
Toast
.makeText(
context,
@@ -29,11 +29,11 @@ import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.platform.LocalClipboard
import androidx.compose.ui.text.AnnotatedString
import androidx.compose.ui.unit.dp
import coil3.compose.AsyncImage
import com.vitorpamplona.amethyst.R
@@ -41,9 +41,11 @@ import com.vitorpamplona.amethyst.commons.richtext.RichTextParser
import com.vitorpamplona.amethyst.model.User
import com.vitorpamplona.amethyst.service.relayClient.reqCommand.user.observeUserBanner
import com.vitorpamplona.amethyst.ui.components.ZoomableImageDialog
import com.vitorpamplona.amethyst.ui.components.util.setText
import com.vitorpamplona.amethyst.ui.painterRes
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
import com.vitorpamplona.amethyst.ui.stringRes
import kotlinx.coroutines.launch
@OptIn(ExperimentalFoundationApi::class)
@Composable
@@ -64,6 +66,7 @@ fun DrawBanner(
) {
if (!banner.isNullOrBlank()) {
val clipboardManager = LocalClipboard.current
val scope = rememberCoroutineScope()
var zoomImageDialogOpen by remember { mutableStateOf(false) }
AsyncImage(
@@ -78,7 +81,11 @@ fun DrawBanner(
.height(150.dp)
.combinedClickable(
onClick = { zoomImageDialogOpen = true },
onLongClick = { clipboardManager.setText(AnnotatedString(banner)) },
onLongClick = {
scope.launch {
clipboardManager.setText(banner)
}
},
),
)
@@ -28,18 +28,20 @@ import androidx.compose.material.icons.outlined.ContentCopy
import androidx.compose.material.icons.outlined.Report
import androidx.compose.material.icons.outlined.Share
import androidx.compose.runtime.Composable
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.platform.LocalClipboard
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.text.AnnotatedString
import com.vitorpamplona.amethyst.R
import com.vitorpamplona.amethyst.model.User
import com.vitorpamplona.amethyst.ui.components.M3ActionDialog
import com.vitorpamplona.amethyst.ui.components.M3ActionRow
import com.vitorpamplona.amethyst.ui.components.M3ActionSection
import com.vitorpamplona.amethyst.ui.components.util.setText
import com.vitorpamplona.amethyst.ui.note.externalLinkForUser
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
import com.vitorpamplona.amethyst.ui.stringRes
import com.vitorpamplona.quartz.nip56Reports.ReportType
import kotlinx.coroutines.launch
@Composable
fun UserProfileDropDownMenu(
@@ -55,6 +57,7 @@ fun UserProfileDropDownMenu(
onDismiss = onDismiss,
) {
val clipboardManager = LocalClipboard.current
val scope = rememberCoroutineScope()
val context = LocalContext.current
// Share section
@@ -63,9 +66,11 @@ fun UserProfileDropDownMenu(
icon = Icons.Outlined.ContentCopy,
text = stringRes(R.string.copy_user_id),
) {
clipboardManager.setText(AnnotatedString(user.pubkeyNpub()))
scope.launch {
clipboardManager.setText(user.pubkeyNpub())
onDismiss()
}
}
M3ActionRow(
icon = Icons.Outlined.Share,
text = stringRes(R.string.quick_action_share),
@@ -20,6 +20,7 @@
*/
package com.vitorpamplona.amethyst.ui.screen.loggedIn.profile.relays
import android.R.attr.onClick
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyColumn
@@ -28,18 +29,20 @@ import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.material3.HorizontalDivider
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalClipboard
import androidx.compose.ui.text.AnnotatedString
import androidx.compose.ui.unit.dp
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.vitorpamplona.amethyst.R
import com.vitorpamplona.amethyst.ui.components.util.setText
import com.vitorpamplona.amethyst.ui.navigation.navs.INav
import com.vitorpamplona.amethyst.ui.navigation.routes.Route
import com.vitorpamplona.amethyst.ui.note.RelayCompose
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.SettingsCategory
import com.vitorpamplona.amethyst.ui.theme.DividerThickness
import kotlinx.coroutines.launch
@Composable
fun RelayFeedView(
@@ -95,12 +98,15 @@ private fun RenderRelayRow(
nav: INav,
) {
val clipboardManager = LocalClipboard.current
val scope = rememberCoroutineScope()
RelayCompose(
relay,
accountViewModel = accountViewModel,
onAddRelay = {
clipboardManager.setText(AnnotatedString(relay.url.url))
scope.launch {
clipboardManager.setText(relay.url.url)
nav.nav(Route.EditRelays)
}
},
onRemoveRelay = {
nav.nav(Route.EditRelays)
@@ -32,13 +32,14 @@ import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalClipboard
import androidx.compose.ui.text.AnnotatedString
import com.vitorpamplona.amethyst.R
import com.vitorpamplona.amethyst.model.nip11RelayInfo.Nip11CachedRetriever
import com.vitorpamplona.amethyst.model.nip11RelayInfo.loadRelayInfo
import com.vitorpamplona.amethyst.ui.components.util.setText
import com.vitorpamplona.amethyst.ui.navigation.navs.INav
import com.vitorpamplona.amethyst.ui.note.RenderRelayIcon
import com.vitorpamplona.amethyst.ui.note.UserPicture
@@ -52,6 +53,7 @@ import com.vitorpamplona.amethyst.ui.theme.LargeRelayIconModifier
import com.vitorpamplona.amethyst.ui.theme.ReactionRowHeightChatMaxWidth
import com.vitorpamplona.amethyst.ui.theme.Size25dp
import com.vitorpamplona.quartz.nip01Core.relay.normalizer.displayUrl
import kotlinx.coroutines.launch
@OptIn(ExperimentalFoundationApi::class)
@Composable
@@ -68,13 +70,16 @@ fun BasicRelaySetupInfoClickableRow(
nav: INav,
) {
val clipboardManager = LocalClipboard.current
val scope = rememberCoroutineScope()
Column(
Modifier
.fillMaxWidth()
.combinedClickable(
onClick = onClick,
onLongClick = {
clipboardManager.setText(AnnotatedString(item.relay.url))
scope.launch {
clipboardManager.setText(item.relay.url)
}
},
),
) {
@@ -33,18 +33,20 @@ import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalClipboard
import androidx.compose.ui.text.AnnotatedString
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import com.vitorpamplona.amethyst.R
import com.vitorpamplona.amethyst.ui.components.util.setText
import com.vitorpamplona.amethyst.ui.painterRes
import com.vitorpamplona.amethyst.ui.stringRes
import com.vitorpamplona.amethyst.ui.theme.LightRedColor
import com.vitorpamplona.amethyst.ui.theme.allGoodColor
import com.vitorpamplona.quartz.nip01Core.relay.normalizer.displayUrl
import kotlinx.coroutines.launch
@OptIn(ExperimentalFoundationApi::class)
@Composable
@@ -55,6 +57,7 @@ fun RelayNameAndRemoveButton(
modifier: Modifier,
) {
val clipboardManager = LocalClipboard.current
val scope = rememberCoroutineScope()
Row(verticalAlignment = Alignment.CenterVertically, modifier = modifier) {
Row(Modifier.weight(1f), verticalAlignment = Alignment.CenterVertically) {
Text(
@@ -63,7 +66,9 @@ fun RelayNameAndRemoveButton(
Modifier.combinedClickable(
onClick = onClick,
onLongClick = {
clipboardManager.setText(AnnotatedString(item.relay.url))
scope.launch {
clipboardManager.setText(item.relay.url)
}
},
),
maxLines = 1,
@@ -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.desktop
import androidx.compose.ui.platform.ClipEntry
import androidx.compose.ui.platform.Clipboard
import java.awt.datatransfer.DataFlavor
import java.awt.datatransfer.StringSelection
import java.awt.datatransfer.Transferable
@OptIn(androidx.compose.ui.ExperimentalComposeUiApi::class)
suspend fun Clipboard.setText(text: String) {
setClipEntry(ClipEntry(StringSelection(text)))
}
@OptIn(androidx.compose.ui.ExperimentalComposeUiApi::class)
suspend fun Clipboard.getText(): String? {
val entry = getClipEntry() ?: return null
val transferable = entry.nativeClipEntry as? Transferable ?: return null
return try {
transferable.getTransferData(DataFlavor.stringFlavor) as? String
} catch (_: Exception) {
null
}
}
@@ -80,6 +80,7 @@ import com.vitorpamplona.amethyst.commons.ui.components.EmptyState
import com.vitorpamplona.amethyst.commons.ui.components.LoadingState
import com.vitorpamplona.amethyst.desktop.account.AccountState
import com.vitorpamplona.amethyst.desktop.cache.DesktopLocalCache
import com.vitorpamplona.amethyst.desktop.getText
import com.vitorpamplona.amethyst.desktop.network.DesktopRelayConnectionManager
import com.vitorpamplona.amethyst.desktop.service.highlights.DesktopHighlightStore
import com.vitorpamplona.amethyst.desktop.subscriptions.DesktopRelaySubscriptionsCoordinator
@@ -99,6 +100,7 @@ import com.vitorpamplona.quartz.nip47WalletConnect.Nip47WalletConnect
import com.vitorpamplona.quartz.nip51Lists.bookmarkList.BookmarkListEvent
import com.vitorpamplona.quartz.nip57Zaps.LnZapEvent
import kotlinx.coroutines.launch
import java.awt.SystemColor.text
import java.time.Instant
import java.time.ZoneId
import java.time.format.DateTimeFormatter
@@ -573,9 +575,10 @@ fun ArticleReaderScreen(
) {
HighlightContextMenuRepresentation(
delegate = defaultRepresentation,
clipboardManager = clipboardManager,
onHighlight = { text ->
onHighlight = {
scope.launch {
val text = clipboardManager.getText()
if (!text.isNullOrBlank()) {
highlightStore?.addHighlight(
articleAddressTag = addressTag,
text = text,
@@ -583,9 +586,15 @@ fun ArticleReaderScreen(
articleTitle = title,
)
}
}
},
onHighlightWithNote = { text ->
onHighlightWithNote = {
scope.launch {
val text = clipboardManager.getText()
if (!text.isNullOrBlank()) {
showAnnotationDialog = text
}
}
},
)
}
@@ -695,9 +704,8 @@ fun ArticleReaderScreen(
*/
private class HighlightContextMenuRepresentation(
private val delegate: ContextMenuRepresentation,
private val clipboardManager: androidx.compose.ui.platform.ClipboardManager,
private val onHighlight: (String) -> Unit,
private val onHighlightWithNote: (String) -> Unit,
private val onHighlight: () -> Unit,
private val onHighlightWithNote: () -> Unit,
) : ContextMenuRepresentation {
@Composable
override fun Representation(
@@ -713,17 +721,11 @@ private class HighlightContextMenuRepresentation(
listOf(
ContextMenuItem("Highlight") {
copyItem.onClick()
val text = clipboardManager.getText()?.text
if (!text.isNullOrBlank()) {
onHighlight(text)
}
onHighlight()
},
ContextMenuItem("Highlight with Note") {
copyItem.onClick()
val text = clipboardManager.getText()?.text
if (!text.isNullOrBlank()) {
onHighlightWithNote(text)
}
onHighlightWithNote()
},
)
} else {
@@ -50,7 +50,6 @@ import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.platform.LocalClipboard
import androidx.compose.ui.text.AnnotatedString
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.tooling.preview.Preview
@@ -63,6 +62,7 @@ import com.vitorpamplona.amethyst.commons.resources.login_card_title
import com.vitorpamplona.amethyst.commons.resources.login_generate_button
import com.vitorpamplona.amethyst.desktop.account.LoginProgress
import com.vitorpamplona.amethyst.desktop.account.validateBunkerUri
import com.vitorpamplona.amethyst.desktop.setText
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
@@ -318,7 +318,11 @@ private fun NostrConnectContent(
Spacer(Modifier.height(8.dp))
OutlinedButton(
onClick = { clipboardManager.setText(AnnotatedString(uri)) },
onClick = {
scope.launch {
clipboardManager.setText(uri)
}
},
modifier = Modifier.fillMaxWidth(),
) {
Text("Copy URI")