From 9f68f0227e220d223e0d6f6ffeac7823f686ebf2 Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Sun, 1 Oct 2023 11:06:05 -0400 Subject: [PATCH] Moves many Toasts to better designed Information Dialogs. --- .../vitorpamplona/amethyst/model/Account.kt | 4 +- .../amethyst/service/CashuProcessor.kt | 28 +++- .../amethyst/service/ZapPaymentHandler.kt | 31 ++-- .../service/lnurl/LightningAddressResolver.kt | 105 +++++++++--- .../amethyst/ui/actions/InformationDialog.kt | 54 +++++++ .../amethyst/ui/actions/NewPostView.kt | 3 + .../amethyst/ui/actions/NewRelayListView.kt | 13 +- .../ui/actions/RelaySelectionDialog.kt | 36 ++--- .../amethyst/ui/components/CashuRedeem.kt | 33 ++-- .../ui/components/ClickableWithdrawal.kt | 31 ++-- .../amethyst/ui/components/InvoicePreview.kt | 31 ++-- .../amethyst/ui/components/InvoiceRequest.kt | 19 +-- .../ui/components/ZoomableContentView.kt | 31 ++-- .../amethyst/ui/navigation/DrawerContent.kt | 8 +- .../amethyst/ui/note/NoteCompose.kt | 10 +- .../amethyst/ui/note/NoteQuickActionMenu.kt | 12 +- .../amethyst/ui/note/PollNote.kt | 95 +++++------ .../amethyst/ui/note/ReactionsRow.kt | 153 +++++------------- .../amethyst/ui/note/RelayListRow.kt | 15 +- .../amethyst/ui/note/UpdateZapAmountDialog.kt | 73 +++++---- .../amethyst/ui/note/UserProfilePicture.kt | 6 +- .../amethyst/ui/note/ZapCustomDialog.kt | 20 +-- .../amethyst/ui/note/ZapNoteCompose.kt | 47 ++---- .../ui/screen/loggedIn/AccountBackupDialog.kt | 21 +-- .../ui/screen/loggedIn/AccountViewModel.kt | 95 ++++++++++- .../ui/screen/loggedIn/ConnectOrbotDialog.kt | 16 +- .../ui/screen/loggedIn/GeoHashScreen.kt | 47 ++---- .../ui/screen/loggedIn/HashtagScreen.kt | 50 ++---- .../ui/screen/loggedIn/HiddenUsersScreen.kt | 48 ++---- .../amethyst/ui/screen/loggedIn/MainScreen.kt | 28 ++++ .../ui/screen/loggedIn/ProfileScreen.kt | 148 ++++++++--------- .../ui/screen/loggedOff/LoginScreen.kt | 9 ++ app/src/main/res/values/strings.xml | 58 ++++++- 33 files changed, 722 insertions(+), 656 deletions(-) create mode 100644 app/src/main/java/com/vitorpamplona/amethyst/ui/actions/InformationDialog.kt diff --git a/app/src/main/java/com/vitorpamplona/amethyst/model/Account.kt b/app/src/main/java/com/vitorpamplona/amethyst/model/Account.kt index 397a6cf11..843e44747 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/model/Account.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/model/Account.kt @@ -895,7 +895,7 @@ class Account( return returningContactList } - fun follow(user: User) { + suspend fun follow(user: User) { if (!isWriteable() && !loginWithExternalSigner) return val contactList = migrateCommunitiesAndChannelsIfNeeded(userProfile().latestContactList) @@ -1064,7 +1064,7 @@ class Account( LocalCache.consume(event) } - fun unfollow(user: User) { + suspend fun unfollow(user: User) { if (!isWriteable() && !loginWithExternalSigner) return val contactList = migrateCommunitiesAndChannelsIfNeeded(userProfile().latestContactList) diff --git a/app/src/main/java/com/vitorpamplona/amethyst/service/CashuProcessor.kt b/app/src/main/java/com/vitorpamplona/amethyst/service/CashuProcessor.kt index 0773e70a8..9c2e2637c 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/service/CashuProcessor.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/service/CashuProcessor.kt @@ -1,8 +1,10 @@ package com.vitorpamplona.amethyst.service +import android.content.Context import androidx.compose.runtime.Immutable import com.fasterxml.jackson.databind.JsonNode import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper +import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.service.lnurl.LightningAddressResolver import com.vitorpamplona.amethyst.ui.components.GenericLoadable import com.vitorpamplona.quartz.events.Event @@ -45,7 +47,7 @@ class CashuProcessor { } } - suspend fun melt(token: CashuToken, lud16: String, onSuccess: (String) -> Unit, onError: (String) -> Unit) { + suspend fun melt(token: CashuToken, lud16: String, onSuccess: (String, String) -> Unit, onError: (String, String) -> Unit, context: Context) { checkNotInMainThread() runCatching { @@ -54,16 +56,17 @@ class CashuProcessor { milliSats = token.redeemInvoiceAmount * 1000, // Make invoice and leave room for fees message = "Redeem Cashu", onSuccess = { invoice -> - meltInvoice(token, invoice, onSuccess, onError) + meltInvoice(token, invoice, onSuccess, onError, context) }, onProgress = { }, - onError = onError + onError = onError, + context = context ) } } - private fun meltInvoice(token: CashuToken, invoice: String, onSuccess: (String) -> Unit, onError: (String) -> Unit) { + private fun meltInvoice(token: CashuToken, invoice: String, onSuccess: (String, String) -> Unit, onError: (String, String) -> Unit, context: Context) { try { val client = HttpClient.getHttpClient() val url = token.mint + "/melt" // Melt cashu tokens at Mint @@ -88,13 +91,24 @@ class CashuProcessor { val successful = tree?.get("paid")?.asText() == "true" if (successful) { - onSuccess("Redeemed ${token.totalAmount} Sats" + " (Fees: ${token.fees} Sats)") + onSuccess( + context.getString(R.string.cashu_sucessful_redemption), + context.getString(R.string.cashu_sucessful_redemption_explainer, token.totalAmount.toString(), token.fees.toString()) + ) } else { - onError(tree?.get("detail")?.asText()?.split('.')?.getOrNull(0) ?: "Cashu: Tokens already spent.") + val msg = tree?.get("detail")?.asText()?.split('.')?.getOrNull(0)?.ifBlank { null } + onError( + context.getString(R.string.cashu_failed_redemption), + if (msg != null) { + context.getString(R.string.cashu_failed_redemption_explainer_error_msg, msg) + } else { + context.getString(R.string.cashu_failed_redemption_explainer_error_msg) + } + ) } } } catch (e: Exception) { - onError("Token melt failure: " + e.message) + onError(context.getString(R.string.cashu_sucessful_redemption), context.getString(R.string.cashu_failed_redemption_explainer_error_msg, e.message)) } } } diff --git a/app/src/main/java/com/vitorpamplona/amethyst/service/ZapPaymentHandler.kt b/app/src/main/java/com/vitorpamplona/amethyst/service/ZapPaymentHandler.kt index b58fefc98..7ce89d610 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/service/ZapPaymentHandler.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/service/ZapPaymentHandler.kt @@ -35,7 +35,7 @@ class ZapPaymentHandler(val account: Account) { pollOption: Int?, message: String, context: Context, - onError: (String) -> Unit, + onError: (String, String) -> Unit, onProgress: (percent: Float) -> Unit, onPayViaIntent: (ImmutableList) -> Unit, zapType: LnZapEvent.ZapType @@ -48,7 +48,10 @@ class ZapPaymentHandler(val account: Account) { val lud16 = note.author?.info?.lud16?.trim() ?: note.author?.info?.lud06?.trim() if (lud16.isNullOrBlank()) { - onError(context.getString(R.string.user_does_not_have_a_lightning_address_setup_to_receive_sats)) + onError( + context.getString(R.string.missing_lud16), + context.getString(R.string.user_does_not_have_a_lightning_address_setup_to_receive_sats) + ) return@withContext } @@ -121,6 +124,9 @@ class ZapPaymentHandler(val account: Account) { ) } else { onError( + context.getString( + R.string.missing_lud16 + ), context.getString( R.string.user_x_does_not_have_a_lightning_address_setup_to_receive_sats, user?.toBestDisplayName() ?: value.lnAddressOrPubKeyHex @@ -149,7 +155,7 @@ class ZapPaymentHandler(val account: Account) { pollOption: Int?, message: String, context: Context, - onError: (String) -> Unit, + onError: (String, String) -> Unit, onProgress: (percent: Float) -> Unit, onPayInvoiceThroughIntent: (String) -> Unit, zapType: LnZapEvent.ZapType, @@ -181,9 +187,13 @@ class ZapPaymentHandler(val account: Account) { if (response is PayInvoiceErrorResponse) { onProgress(0.0f) onError( - response.error?.message - ?: response.error?.code?.toString() - ?: "Error parsing error message" + context.getString(R.string.error_dialog_pay_invoice_error), + context.getString( + R.string.wallet_connect_pay_invoice_error_error, + response.error?.message + ?: response.error?.code?.toString() + ?: "Error parsing error message" + ) ) } else { onProgress(1f) @@ -192,16 +202,13 @@ class ZapPaymentHandler(val account: Account) { ) onProgress(0.8f) } else { - try { - onPayInvoiceThroughIntent(it) - } catch (e: Exception) { - onError(context.getString(R.string.lightning_wallets_not_found2)) - } + onPayInvoiceThroughIntent(it) onProgress(0f) } }, onError = onError, - onProgress = onProgress + onProgress = onProgress, + context = context ) } } diff --git a/app/src/main/java/com/vitorpamplona/amethyst/service/lnurl/LightningAddressResolver.kt b/app/src/main/java/com/vitorpamplona/amethyst/service/lnurl/LightningAddressResolver.kt index f443d56c2..a89e22b84 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/service/lnurl/LightningAddressResolver.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/service/lnurl/LightningAddressResolver.kt @@ -1,7 +1,9 @@ package com.vitorpamplona.amethyst.service.lnurl +import android.content.Context import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper import com.vitorpamplona.amethyst.BuildConfig +import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.service.HttpClient import com.vitorpamplona.amethyst.service.checkNotInMainThread import com.vitorpamplona.quartz.encoders.LnInvoiceUtil @@ -31,13 +33,24 @@ class LightningAddressResolver() { return null } - private suspend fun fetchLightningAddressJson(lnaddress: String, onSuccess: suspend (String) -> Unit, onError: (String) -> Unit) = withContext(Dispatchers.IO) { + private suspend fun fetchLightningAddressJson( + lnaddress: String, + onSuccess: suspend (String) -> Unit, + onError: (String, String) -> Unit, + context: Context + ) = withContext(Dispatchers.IO) { checkNotInMainThread() val url = assembleUrl(lnaddress) if (url == null) { - onError("Could not assemble LNUrl from Lightning Address \"${lnaddress}\". Check the user's setup") + onError( + context.getString(R.string.error_unable_to_fetch_invoice), + context.getString( + R.string.could_not_assemble_lnurl_from_lightning_address_check_the_user_s_setup, + lnaddress + ) + ) return@withContext } @@ -51,16 +64,39 @@ class LightningAddressResolver() { if (it.isSuccessful) { onSuccess(it.body.string()) } else { - onError("The receiver's lightning service at $url is not available. It was calculated from the lightning address \"${lnaddress}\". Error: ${it.code}. Check if the server is up and if the lightning address is correct") + onError( + context.getString(R.string.error_unable_to_fetch_invoice), + context.getString( + R.string.the_receiver_s_lightning_service_at_is_not_available_it_was_calculated_from_the_lightning_address_error_check_if_the_server_is_up_and_if_the_lightning_address_is_correct, + url, + lnaddress, + it.code.toString() + ) + ) } } } catch (e: Exception) { e.printStackTrace() - onError("Could not resolve $url. Check if you are connected, if the server is up and if the lightning address $lnaddress is correct") + onError( + context.getString(R.string.error_unable_to_fetch_invoice), + context.getString( + R.string.could_not_resolve_check_if_you_are_connected_if_the_server_is_up_and_if_the_lightning_address_is_correct, + url, + lnaddress + ) + ) } } - suspend fun fetchLightningInvoice(lnCallback: String, milliSats: Long, message: String, nostrRequest: String? = null, onSuccess: suspend (String) -> Unit, onError: (String) -> Unit) = withContext(Dispatchers.IO) { + suspend fun fetchLightningInvoice( + lnCallback: String, + milliSats: Long, + message: String, + nostrRequest: String? = null, + onSuccess: suspend (String) -> Unit, + onError: (String, String) -> Unit, + context: Context + ) = withContext(Dispatchers.IO) { val encodedMessage = URLEncoder.encode(message, "utf-8") val urlBinder = if (lnCallback.contains("?")) "&" else "?" @@ -80,18 +116,22 @@ class LightningAddressResolver() { if (it.isSuccessful) { onSuccess(it.body.string()) } else { - onError("Could not fetch invoice from $lnCallback") + onError( + context.getString(R.string.error_unable_to_fetch_invoice), + context.getString(R.string.could_not_fetch_invoice_from, lnCallback) + ) } } } - suspend fun lnAddressToLnUrl(lnaddress: String, onSuccess: (String) -> Unit, onError: (String) -> Unit) { + suspend fun lnAddressToLnUrl(lnaddress: String, onSuccess: (String) -> Unit, onError: (String, String) -> Unit, context: Context) { fetchLightningAddressJson( lnaddress, onSuccess = { onSuccess(it.toByteArray().toLnUrl()) }, - onError = onError + onError = onError, + context = context ) } @@ -101,8 +141,9 @@ class LightningAddressResolver() { message: String, nostrRequest: String? = null, onSuccess: suspend (String) -> Unit, - onError: (String) -> Unit, - onProgress: (percent: Float) -> Unit + onError: (String, String) -> Unit, + onProgress: (percent: Float) -> Unit, + context: Context ) { val mapper = jacksonObjectMapper() @@ -114,14 +155,20 @@ class LightningAddressResolver() { val lnurlp = try { mapper.readTree(lnAddressJson) } catch (t: Throwable) { - onError("Error Parsing JSON from Lightning Address. Check the user's lightning setup") + onError( + context.getString(R.string.error_unable_to_fetch_invoice), + context.getString(R.string.error_parsing_json_from_lightning_address_check_the_user_s_lightning_setup) + ) null } val callback = lnurlp?.get("callback")?.asText() if (callback == null) { - onError("Callback URL not found in the User's lightning address server configuration") + onError( + context.getString(R.string.error_unable_to_fetch_invoice), + context.getString(R.string.callback_url_not_found_in_the_user_s_lightning_address_server_configuration) + ) } val allowsNostr = lnurlp?.get("allowsNostr")?.asBoolean() ?: false @@ -138,7 +185,10 @@ class LightningAddressResolver() { val lnInvoice = try { mapper.readTree(it) } catch (t: Throwable) { - onError("Error Parsing JSON from Lightning Address's invoice fetch. Check the user's lightning setup") + onError( + context.getString(R.string.error_unable_to_fetch_invoice), + context.getString(R.string.error_parsing_json_from_lightning_address_s_invoice_fetch_check_the_user_s_lightning_setup) + ) null } @@ -151,21 +201,40 @@ class LightningAddressResolver() { onSuccess(pr) } else { onProgress(0.0f) - onError("Incorrect invoice amount (${invoiceAmount.toLong()} sats) from $lnaddress. It should have been $expectedAmountInSats") + onError( + context.getString(R.string.error_unable_to_fetch_invoice), + context.getString( + R.string.incorrect_invoice_amount_sats_from_it_should_have_been, + invoiceAmount.toLong().toString(), + lnaddress, + expectedAmountInSats.toString() + ) + ) } } ?: lnInvoice?.get("reason")?.asText()?.ifBlank { null }?.let { reason -> onProgress(0.0f) - onError("Unable to create a lightning invoice before sending the zap. The receiver's lightning wallet sent the following error: $reason") + onError( + context.getString(R.string.error_unable_to_fetch_invoice), + context.getString( + R.string.unable_to_create_a_lightning_invoice_before_sending_the_zap_the_receiver_s_lightning_wallet_sent_the_following_error, + reason + ) + ) } ?: run { onProgress(0.0f) - onError("nable to create a lightning invoice before sending the zap. Element pr not found in the resulting JSON.") + onError( + context.getString(R.string.error_unable_to_fetch_invoice), + context.getString(R.string.unable_to_create_a_lightning_invoice_before_sending_the_zap_element_pr_not_found_in_the_resulting_json) + ) } }, - onError = onError + onError = onError, + context ) } }, - onError = onError + onError = onError, + context ) } } diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/InformationDialog.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/InformationDialog.kt new file mode 100644 index 000000000..a8323fd36 --- /dev/null +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/InformationDialog.kt @@ -0,0 +1,54 @@ +package com.vitorpamplona.amethyst.ui.actions + +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.text.selection.SelectionContainer +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.outlined.Done +import androidx.compose.material3.AlertDialog +import androidx.compose.material3.Button +import androidx.compose.material3.ButtonColors +import androidx.compose.material3.ButtonDefaults +import androidx.compose.material3.Icon +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.res.stringResource +import com.vitorpamplona.amethyst.R +import com.vitorpamplona.amethyst.ui.theme.Size16dp +import com.vitorpamplona.amethyst.ui.theme.StdHorzSpacer + +@Composable +fun InformationDialog( + title: String, + textContent: String, + buttonColors: ButtonColors = ButtonDefaults.buttonColors(), + onDismiss: () -> Unit +) { + AlertDialog( + onDismissRequest = onDismiss, + title = { + Text(title) + }, + text = { + SelectionContainer { + Text(textContent) + } + }, + confirmButton = { + Button(onClick = onDismiss, colors = buttonColors, contentPadding = PaddingValues(horizontal = Size16dp)) { + Row( + verticalAlignment = Alignment.CenterVertically + ) { + Icon( + imageVector = Icons.Outlined.Done, + contentDescription = null + ) + Spacer(StdHorzSpacer) + Text(stringResource(R.string.error_dialog_button_ok)) + } + } + } + ) +} diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewPostView.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewPostView.kt index 31bc92bb0..4e2dc9f6e 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewPostView.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewPostView.kt @@ -380,6 +380,9 @@ fun NewPostView( }, onClose = { postViewModel.wantsInvoice = false + }, + onError = { title, message -> + accountViewModel.toast(title, message) } ) } diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewRelayListView.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewRelayListView.kt index b1ba73ed7..b3033ec3e 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewRelayListView.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewRelayListView.kt @@ -350,15 +350,10 @@ fun ServerConfig( Nip11Retriever.ErrorCode.FAIL_WITH_HTTP_STATUS -> context.getString(R.string.relay_information_document_error_assemble_url, url, exceptionMessage) } - scope.launch { - Toast - .makeText( - context, - msg, - Toast.LENGTH_SHORT - ) - .show() - } + accountViewModel.toast( + context.getString(R.string.unable_to_download_relay_document), + msg + ) } ) } diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/RelaySelectionDialog.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/RelaySelectionDialog.kt index 28e971e44..04592354f 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/RelaySelectionDialog.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/RelaySelectionDialog.kt @@ -1,6 +1,5 @@ package com.vitorpamplona.amethyst.ui.actions -import android.widget.Toast import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.combinedClickable import androidx.compose.foundation.layout.Arrangement @@ -16,15 +15,14 @@ import androidx.compose.material3.Surface import androidx.compose.material3.Switch import androidx.compose.material3.Text import androidx.compose.runtime.Composable +import androidx.compose.runtime.derivedStateOf 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.platform.LocalContext -import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp import androidx.compose.ui.window.Dialog import androidx.compose.ui.window.DialogProperties @@ -34,7 +32,6 @@ import com.vitorpamplona.amethyst.model.RelayInformation import com.vitorpamplona.amethyst.service.Nip11Retriever import com.vitorpamplona.amethyst.service.relays.Relay import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel -import kotlinx.coroutines.launch data class RelayList( val relay: Relay, @@ -55,7 +52,6 @@ fun RelaySelectionDialog( accountViewModel: AccountViewModel, nav: (String) -> Unit ) { - val scope = rememberCoroutineScope() val context = LocalContext.current var relays by remember { @@ -69,6 +65,13 @@ fun RelaySelectionDialog( } ) } + + val hasSelectedRelay by remember { + derivedStateOf { + relays.any { it.isSelected } + } + } + var relayInfo: RelayInfoDialog? by remember { mutableStateOf(null) } relayInfo?.let { @@ -121,21 +124,15 @@ fun RelaySelectionDialog( SaveButton( onPost = { val selectedRelays = relays.filter { it.isSelected } - if (selectedRelays.isEmpty()) { - scope.launch { - Toast.makeText(context, context.getString(R.string.select_a_relay_to_continue), Toast.LENGTH_SHORT).show() - } - return@SaveButton - } onPost(selectedRelays.map { it.relay }) onClose() }, - isActive = true + isActive = hasSelectedRelay ) } RelaySwitch( - text = stringResource(R.string.select_deselect_all), + text = context.getString(R.string.select_deselect_all), checked = selected, onClick = { selected = !selected @@ -181,15 +178,10 @@ fun RelaySelectionDialog( Nip11Retriever.ErrorCode.FAIL_WITH_HTTP_STATUS -> context.getString(R.string.relay_information_document_error_assemble_url, url, exceptionMessage) } - scope.launch { - Toast - .makeText( - context, - msg, - Toast.LENGTH_SHORT - ) - .show() - } + accountViewModel.toast( + context.getString(R.string.unable_to_download_relay_document), + msg + ) } ) } diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/components/CashuRedeem.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/components/CashuRedeem.kt index 1b61fbb79..dedcdd47e 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/components/CashuRedeem.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/components/CashuRedeem.kt @@ -2,7 +2,6 @@ package com.vitorpamplona.amethyst.ui.components import android.content.Intent import android.net.Uri -import android.widget.Toast import androidx.compose.animation.Crossfade import androidx.compose.foundation.border import androidx.compose.foundation.layout.Column @@ -133,26 +132,20 @@ fun CashuPreview(token: CashuToken, accountViewModel: AccountViewModel) { CashuProcessor().melt( token, lud16, - onSuccess = { - scope.launch { - Toast.makeText(context, it, Toast.LENGTH_SHORT).show() - } + onSuccess = { title, message -> + accountViewModel.toast(title, message) }, - onError = { - scope.launch { - Toast.makeText(context, it, Toast.LENGTH_SHORT).show() - } - } + onError = { title, message -> + accountViewModel.toast(title, message) + }, + context ) } } else { - scope.launch { - Toast.makeText( - context, - context.getString(R.string.no_lightning_address_set), - Toast.LENGTH_SHORT - ).show() - } + accountViewModel.toast( + context.getString(R.string.no_lightning_address_set), + context.getString(R.string.user_x_does_not_have_a_lightning_address_setup_to_receive_sats, accountViewModel.account.userProfile().toBestDisplayName()) + ) } }, shape = QuoteBorder, @@ -177,11 +170,7 @@ fun CashuPreview(token: CashuToken, accountViewModel: AccountViewModel) { startActivity(context, intent, null) } else { // Copying the token to clipboard for now - var orignaltoken = token.token - clipboardManager.setText(AnnotatedString("$orignaltoken")) - scope.launch { - Toast.makeText(context, context.getString(R.string.copied_token_to_clipboard), Toast.LENGTH_SHORT).show() - } + clipboardManager.setText(AnnotatedString(token.token)) } }, shape = QuoteBorder, diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/components/ClickableWithdrawal.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/components/ClickableWithdrawal.kt index 97b69c3e6..621a7a9fb 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/components/ClickableWithdrawal.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/components/ClickableWithdrawal.kt @@ -1,8 +1,5 @@ package com.vitorpamplona.amethyst.ui.components -import android.content.Intent -import android.net.Uri -import android.widget.Toast import androidx.compose.animation.Crossfade import androidx.compose.foundation.text.ClickableText import androidx.compose.material3.LocalTextStyle @@ -12,8 +9,9 @@ import androidx.compose.runtime.* import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.text.AnnotatedString import androidx.compose.ui.text.style.TextDirection -import androidx.core.content.ContextCompat import com.vitorpamplona.amethyst.R +import com.vitorpamplona.amethyst.ui.note.ErrorMessageDialog +import com.vitorpamplona.amethyst.ui.note.payViaIntent import com.vitorpamplona.quartz.encoders.LnWithdrawalUtil import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch @@ -43,27 +41,26 @@ fun MayBeWithdrawal(lnurlWord: String) { @Composable fun ClickableWithdrawal(withdrawalString: String) { val context = LocalContext.current - val scope = rememberCoroutineScope() val withdraw = remember(withdrawalString) { AnnotatedString("$withdrawalString ") } + var showErrorMessageDialog by remember { mutableStateOf(null) } + + if (showErrorMessageDialog != null) { + ErrorMessageDialog( + title = context.getString(R.string.error_dialog_pay_withdraw_error), + textContent = showErrorMessageDialog ?: "", + onDismiss = { showErrorMessageDialog = null } + ) + } + ClickableText( text = withdraw, onClick = { - try { - val intent = Intent(Intent.ACTION_VIEW, Uri.parse("lightning:$withdrawalString")) - intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK - ContextCompat.startActivity(context, intent, null) - } catch (e: Exception) { - scope.launch { - Toast.makeText( - context, - context.getString(R.string.lightning_wallets_not_found), - Toast.LENGTH_LONG - ).show() - } + payViaIntent(withdrawalString, context) { + showErrorMessageDialog = it } }, style = LocalTextStyle.current.copy(color = MaterialTheme.colorScheme.primary) diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/components/InvoicePreview.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/components/InvoicePreview.kt index a50827a66..a909abd23 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/components/InvoicePreview.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/components/InvoicePreview.kt @@ -1,8 +1,5 @@ package com.vitorpamplona.amethyst.ui.components -import android.content.Intent -import android.net.Uri -import android.widget.Toast import androidx.compose.animation.Crossfade import androidx.compose.foundation.border import androidx.compose.foundation.layout.Column @@ -23,8 +20,9 @@ import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextDirection import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp -import androidx.core.content.ContextCompat.startActivity import com.vitorpamplona.amethyst.R +import com.vitorpamplona.amethyst.ui.note.ErrorMessageDialog +import com.vitorpamplona.amethyst.ui.note.payViaIntent import com.vitorpamplona.amethyst.ui.theme.QuoteBorder import com.vitorpamplona.amethyst.ui.theme.subtleBorder import com.vitorpamplona.quartz.encoders.LnInvoiceUtil @@ -67,7 +65,16 @@ fun MayBeInvoicePreview(lnbcWord: String) { @Composable fun InvoicePreview(lnInvoice: String, amount: String?) { val context = LocalContext.current - val scope = rememberCoroutineScope() + + var showErrorMessageDialog by remember { mutableStateOf(null) } + + if (showErrorMessageDialog != null) { + ErrorMessageDialog( + title = context.getString(R.string.error_dialog_pay_invoice_error), + textContent = showErrorMessageDialog ?: "", + onDismiss = { showErrorMessageDialog = null } + ) + } Column( modifier = Modifier @@ -120,18 +127,8 @@ fun InvoicePreview(lnInvoice: String, amount: String?) { .fillMaxWidth() .padding(vertical = 10.dp), onClick = { - try { - val intent = Intent(Intent.ACTION_VIEW, Uri.parse("lightning:$lnInvoice")) - intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK - startActivity(context, intent, null) - } catch (e: Exception) { - scope.launch { - Toast.makeText( - context, - context.getString(R.string.lightning_wallets_not_found), - Toast.LENGTH_LONG - ).show() - } + payViaIntent(lnInvoice, context) { + showErrorMessageDialog = it } }, shape = QuoteBorder, diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/components/InvoiceRequest.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/components/InvoiceRequest.kt index 942bab372..801f495a0 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/components/InvoiceRequest.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/components/InvoiceRequest.kt @@ -1,6 +1,5 @@ package com.vitorpamplona.amethyst.ui.components -import android.widget.Toast import androidx.compose.foundation.border import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row @@ -50,7 +49,8 @@ fun InvoiceRequestCard( titleText: String? = null, buttonText: String? = null, onSuccess: (String) -> Unit, - onClose: () -> Unit + onClose: () -> Unit, + onError: (String, String) -> Unit ) { Column( modifier = Modifier @@ -64,7 +64,7 @@ fun InvoiceRequestCard( .fillMaxWidth() .padding(30.dp) ) { - InvoiceRequest(lud16, toUserPubKeyHex, account, titleText, buttonText, onSuccess, onClose) + InvoiceRequest(lud16, toUserPubKeyHex, account, titleText, buttonText, onSuccess, onClose, onError) } } } @@ -77,7 +77,8 @@ fun InvoiceRequest( titleText: String? = null, buttonText: String? = null, onSuccess: (String) -> Unit, - onClose: () -> Unit + onClose: () -> Unit, + onError: (String, String) -> Unit ) { val context = LocalContext.current val scope = rememberCoroutineScope() @@ -162,14 +163,10 @@ fun InvoiceRequest( message, zapRequest?.toJson(), onSuccess = onSuccess, - onError = { - scope.launch { - Toast.makeText(context, it, Toast.LENGTH_SHORT).show() - onClose() - } - }, + onError = onError, onProgress = { - } + }, + context = context ) } }, diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/components/ZoomableContentView.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/components/ZoomableContentView.kt index 97c4bfe67..52e1c0353 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/components/ZoomableContentView.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/components/ZoomableContentView.kt @@ -6,7 +6,6 @@ import android.content.ContextWrapper import android.os.Build import android.util.Log import android.view.Window -import android.widget.Toast import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.clickable import androidx.compose.foundation.combinedClickable @@ -49,7 +48,6 @@ 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 @@ -78,6 +76,7 @@ import com.vitorpamplona.amethyst.model.ConnectivityType import com.vitorpamplona.amethyst.service.BlurHashRequester import com.vitorpamplona.amethyst.service.connectivitystatus.ConnectivityStatus import com.vitorpamplona.amethyst.ui.actions.CloseButton +import com.vitorpamplona.amethyst.ui.actions.InformationDialog import com.vitorpamplona.amethyst.ui.actions.LoadingAnimation import com.vitorpamplona.amethyst.ui.actions.SaveToGallery import com.vitorpamplona.amethyst.ui.note.BlankNote @@ -808,7 +807,17 @@ private fun verifyHash(content: ZoomableUrlContent, context: Context): Boolean? @Composable private fun HashVerificationSymbol(verifiedHash: Boolean, modifier: Modifier) { val localContext = LocalContext.current - val scope = rememberCoroutineScope() + + val openDialogMsg = remember { mutableStateOf(null) } + + openDialogMsg.value?.let { + InformationDialog( + title = localContext.getString(R.string.hash_verification_info_title), + textContent = it + ) { + openDialogMsg.value = null + } + } Box( modifier @@ -819,13 +828,7 @@ private fun HashVerificationSymbol(verifiedHash: Boolean, modifier: Modifier) { if (verifiedHash) { IconButton( onClick = { - scope.launch { - Toast.makeText( - localContext, - localContext.getString(R.string.hash_verification_passed), - Toast.LENGTH_LONG - ).show() - } + openDialogMsg.value = localContext.getString(R.string.hash_verification_passed) } ) { HashCheckIcon(Size30dp) @@ -833,13 +836,7 @@ private fun HashVerificationSymbol(verifiedHash: Boolean, modifier: Modifier) { } else { IconButton( onClick = { - scope.launch { - Toast.makeText( - localContext, - localContext.getString(R.string.hash_verification_failed), - Toast.LENGTH_LONG - ).show() - } + openDialogMsg.value = localContext.getString(R.string.hash_verification_failed) } ) { HashCheckFailedIcon(Size30dp) diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/navigation/DrawerContent.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/navigation/DrawerContent.kt index a0328648f..f6be5f8ab 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/navigation/DrawerContent.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/navigation/DrawerContent.kt @@ -566,7 +566,7 @@ fun ListContent( NewRelayListView({ wantsToEditRelays = false }, accountViewModel, nav = nav) } if (backupDialogOpen) { - AccountBackupDialog(accountViewModel.account, onClose = { backupDialogOpen = false }) + AccountBackupDialog(accountViewModel, onClose = { backupDialogOpen = false }) } if (conectOrbotDialogOpen) { ConnectOrbotDialog( @@ -577,6 +577,12 @@ fun ListContent( checked = true enableTor(accountViewModel.account, true, proxyPort, context, coroutineScope) }, + onError = { + accountViewModel.toast( + context.getString(R.string.could_not_connect_to_tor), + it + ) + }, proxyPort ) } diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/NoteCompose.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/NoteCompose.kt index 3176e64f4..933edeacb 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/NoteCompose.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/NoteCompose.kt @@ -88,7 +88,6 @@ import com.vitorpamplona.amethyst.model.ConnectivityType import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.model.RelayBriefInfo import com.vitorpamplona.amethyst.model.User -import com.vitorpamplona.amethyst.service.OnlineChecker import com.vitorpamplona.amethyst.service.ReverseGeoLocationUtil import com.vitorpamplona.amethyst.service.connectivitystatus.ConnectivityStatus import com.vitorpamplona.amethyst.ui.actions.NewRelayListView @@ -162,6 +161,7 @@ import com.vitorpamplona.amethyst.ui.theme.replyBackground import com.vitorpamplona.amethyst.ui.theme.replyModifier import com.vitorpamplona.amethyst.ui.theme.subtleBorder import com.vitorpamplona.quartz.encoders.ATag +import com.vitorpamplona.quartz.encoders.HexKey import com.vitorpamplona.quartz.encoders.toNpub import com.vitorpamplona.quartz.events.AppDefinitionEvent import com.vitorpamplona.quartz.events.AudioHeaderEvent @@ -1286,8 +1286,8 @@ fun routeFor(note: Note, loggedIn: User): String? { return null } -fun routeToMessage(user: User, draftMessage: String?, accountViewModel: AccountViewModel): String { - val withKey = ChatroomKey(persistentSetOf(user.pubkeyHex)) +fun routeToMessage(user: HexKey, draftMessage: String?, accountViewModel: AccountViewModel): String { + val withKey = ChatroomKey(persistentSetOf(user)) accountViewModel.account.userProfile().createChatroom(withKey) return if (draftMessage != null) { "Room/${withKey.hashCode()}?message=$draftMessage" @@ -1296,6 +1296,10 @@ fun routeToMessage(user: User, draftMessage: String?, accountViewModel: AccountV } } +fun routeToMessage(user: User, draftMessage: String?, accountViewModel: AccountViewModel): String { + return routeToMessage(user.pubkeyHex, draftMessage, accountViewModel) +} + fun routeFor(note: Channel): String { return "Channel/${note.idHex}" } diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/NoteQuickActionMenu.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/NoteQuickActionMenu.kt index 536616507..8fcecbf25 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/NoteQuickActionMenu.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/NoteQuickActionMenu.kt @@ -291,20 +291,16 @@ private fun RenderMainPopup( Icons.Default.PersonRemove, stringResource(R.string.quick_action_unfollow) ) { - scope.launch(Dispatchers.IO) { - accountViewModel.unfollow(note.author!!) - onDismiss() - } + accountViewModel.unfollow(note.author!!) + onDismiss() } } else { NoteQuickActionItem( Icons.Default.PersonAdd, stringResource(R.string.quick_action_follow) ) { - scope.launch(Dispatchers.IO) { - accountViewModel.follow(note.author!!) - onDismiss() - } + accountViewModel.follow(note.author!!) + onDismiss() } } diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/PollNote.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/PollNote.kt index 1f5eb03cb..d9f5b7064 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/PollNote.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/PollNote.kt @@ -1,11 +1,12 @@ package com.vitorpamplona.amethyst.ui.note -import android.widget.Toast import androidx.compose.foundation.* +import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.layout.* import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.Bolt import androidx.compose.material.icons.outlined.Bolt +import androidx.compose.material.ripple.rememberRipple import androidx.compose.material3.* import androidx.compose.runtime.* import androidx.compose.runtime.livedata.observeAsState @@ -27,6 +28,7 @@ import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.service.ZapPaymentHandler import com.vitorpamplona.amethyst.ui.components.TranslatableRichTextViewer import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel +import com.vitorpamplona.amethyst.ui.screen.loggedIn.StringToastMsg import com.vitorpamplona.amethyst.ui.theme.BitcoinOrange import com.vitorpamplona.amethyst.ui.theme.ButtonBorder import com.vitorpamplona.amethyst.ui.theme.Font14SP @@ -294,7 +296,7 @@ fun ZapVote( } var zappingProgress by remember { mutableStateOf(0f) } - var showErrorMessageDialog by remember { mutableStateOf(null) } + var showErrorMessageDialog by remember { mutableStateOf(null) } val context = LocalContext.current val scope = rememberCoroutineScope() @@ -305,50 +307,30 @@ fun ZapVote( verticalAlignment = Alignment.CenterVertically, modifier = Modifier.combinedClickable( role = Role.Button, - // interactionSource = remember { MutableInteractionSource() }, - // indication = rememberRipple(bounded = false, radius = 24.dp), + interactionSource = remember { MutableInteractionSource() }, + indication = rememberRipple(bounded = false, radius = 24.dp), onClick = { if (!accountViewModel.isWriteable() && !accountViewModel.loggedInWithExternalSigner()) { - scope.launch { - Toast - .makeText( - context, - context.getString(R.string.login_with_a_private_key_to_be_able_to_send_zaps), - Toast.LENGTH_SHORT - ) - .show() - } + accountViewModel.toast( + R.string.read_only_user, + R.string.login_with_a_private_key_to_be_able_to_send_zaps + ) } else if (pollViewModel.isPollClosed()) { - scope.launch { - Toast - .makeText( - context, - context.getString(R.string.poll_is_closed), - Toast.LENGTH_SHORT - ) - .show() - } + accountViewModel.toast( + R.string.poll_unable_to_vote, + R.string.poll_is_closed_explainer + ) } else if (isLoggedUser) { - scope.launch { - Toast - .makeText( - context, - context.getString(R.string.poll_author_no_vote), - Toast.LENGTH_SHORT - ) - .show() - } + accountViewModel.toast( + R.string.poll_unable_to_vote, + R.string.poll_author_no_vote + ) } else if (pollViewModel.isVoteAmountAtomic() && poolOption.zappedByLoggedIn) { // only allow one vote per option when min==max, i.e. atomic vote amount specified - scope.launch { - Toast - .makeText( - context, - R.string.one_vote_per_user_on_atomic_votes, - Toast.LENGTH_SHORT - ) - .show() - } + accountViewModel.toast( + R.string.poll_unable_to_vote, + R.string.one_vote_per_user_on_atomic_votes + ) return@combinedClickable } else if (accountViewModel.account.zapAmountChoices.size == 1 && pollViewModel.isValidInputVoteAmount(accountViewModel.account.zapAmountChoices.first()) @@ -359,13 +341,9 @@ fun ZapVote( poolOption.option, "", context, - onError = { - scope.launch { - zappingProgress = 0f - Toast - .makeText(context, it, Toast.LENGTH_SHORT) - .show() - } + onError = { title, message -> + zappingProgress = 0f + showErrorMessageDialog = StringToastMsg(title, message) }, onProgress = { scope.launch(Dispatchers.Main) { @@ -395,11 +373,9 @@ fun ZapVote( onChangeAmount = { wantsToZap = false }, - onError = { - scope.launch { - zappingProgress = 0f - showErrorMessageDialog = it - } + onError = { title, message -> + showErrorMessageDialog = StringToastMsg(title, message) + zappingProgress = 0f }, onProgress = { scope.launch(Dispatchers.Main) { @@ -423,19 +399,22 @@ fun ZapVote( wantsToPay = persistentListOf() scope.launch { zappingProgress = 0f - showErrorMessageDialog = it + showErrorMessageDialog = StringToastMsg( + context.getString(R.string.error_dialog_zap_error), + it + ) } } ) } - if (showErrorMessageDialog != null) { + showErrorMessageDialog?.let { toast -> ErrorMessageDialog( - title = stringResource(id = R.string.error_dialog_zap_error), - textContent = showErrorMessageDialog ?: "", + title = toast.title, + textContent = toast.msg, onClickStartMessage = { baseNote.author?.let { - nav(routeToMessage(it, showErrorMessageDialog, accountViewModel)) + nav(routeToMessage(it, toast.msg, accountViewModel)) } }, onDismiss = { showErrorMessageDialog = null } @@ -494,7 +473,7 @@ fun FilteredZapAmountChoicePopup( pollOption: Int, onDismiss: () -> Unit, onChangeAmount: () -> Unit, - onError: (text: String) -> Unit, + onError: (title: String, text: String) -> Unit, onProgress: (percent: Float) -> Unit, onPayViaIntent: (ImmutableList) -> Unit ) { diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/ReactionsRow.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/ReactionsRow.kt index 34e28fb4f..68bbf43be 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/ReactionsRow.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/ReactionsRow.kt @@ -2,7 +2,6 @@ package com.vitorpamplona.amethyst.ui.note import android.content.Context import android.util.Log -import android.widget.Toast import androidx.compose.animation.AnimatedContent import androidx.compose.animation.AnimatedContentTransitionScope import androidx.compose.animation.ContentTransform @@ -110,7 +109,6 @@ import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.persistentListOf import kotlinx.collections.immutable.toImmutableList import kotlinx.collections.immutable.toImmutableMap -import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch import java.math.BigDecimal @@ -540,9 +538,6 @@ fun ReplyReaction( iconSize: Dp = Size17dp, onPress: () -> Unit ) { - val context = LocalContext.current - val scope = rememberCoroutineScope() - IconButton( modifier = remember { Modifier.size(iconSize) @@ -554,13 +549,10 @@ fun ReplyReaction( if (accountViewModel.loggedInWithExternalSigner()) { onPress() } else { - scope.launch { - Toast.makeText( - context, - context.getString(R.string.login_with_a_private_key_to_be_able_to_reply), - Toast.LENGTH_SHORT - ).show() - } + accountViewModel.toast( + R.string.read_only_user, + R.string.login_with_a_private_key_to_be_able_to_reply + ) } } } @@ -643,9 +635,6 @@ fun BoostReaction( iconSize: Dp = 20.dp, onQuotePress: () -> Unit ) { - val context = LocalContext.current - val scope = rememberCoroutineScope() - var wantsToBoost by remember { mutableStateOf(false) } val iconButtonModifier = remember { @@ -657,29 +646,22 @@ fun BoostReaction( onClick = { if (accountViewModel.isWriteable()) { if (accountViewModel.hasBoosted(baseNote)) { - scope.launch(Dispatchers.IO) { - accountViewModel.deleteBoostsTo(baseNote) - } + accountViewModel.deleteBoostsTo(baseNote) } else { wantsToBoost = true } } else { if (accountViewModel.loggedInWithExternalSigner()) { if (accountViewModel.hasBoosted(baseNote)) { - scope.launch(Dispatchers.IO) { - accountViewModel.deleteBoostsTo(baseNote) - } + accountViewModel.deleteBoostsTo(baseNote) } else { wantsToBoost = true } } else { - scope.launch { - Toast.makeText( - context, - context.getString(R.string.login_with_a_private_key_to_be_able_to_boost_posts), - Toast.LENGTH_SHORT - ).show() - } + accountViewModel.toast( + R.string.read_only_user, + R.string.login_with_a_private_key_to_be_able_to_boost_posts + ) } } } @@ -699,9 +681,7 @@ fun BoostReaction( onQuotePress() }, onRepost = { - scope.launch(Dispatchers.IO) { - accountViewModel.boost(baseNote) - } + accountViewModel.boost(baseNote) } ) } @@ -741,9 +721,6 @@ fun LikeReaction( heartSize: Dp = 16.dp, iconFontSize: TextUnit = Font14SP ) { - val context = LocalContext.current - val scope = rememberCoroutineScope() - val iconButtonModifier = remember { Modifier.size(iconSize) } @@ -761,21 +738,12 @@ fun LikeReaction( likeClick( baseNote, accountViewModel, - scope, - context, onMultipleChoices = { wantsToReact = true }, onWantsToSignReaction = { if (accountViewModel.account.reactionChoices.size == 1) { - scope.launch(Dispatchers.IO) { - val reaction = accountViewModel.account.reactionChoices.first() - if (accountViewModel.hasReactedTo(baseNote, reaction)) { - accountViewModel.deleteReactionTo(baseNote, reaction) - } else { - accountViewModel.reactTo(baseNote, reaction) - } - } + accountViewModel.reactToOrDelete(baseNote) } else if (accountViewModel.account.reactionChoices.size > 1) { wantsToReact = true } @@ -896,44 +864,25 @@ fun LikeText(baseNote: Note, grayTint: Color) { private fun likeClick( baseNote: Note, accountViewModel: AccountViewModel, - scope: CoroutineScope, - context: Context, onMultipleChoices: () -> Unit, onWantsToSignReaction: () -> Unit ) { if (accountViewModel.account.reactionChoices.isEmpty()) { - scope.launch { - Toast - .makeText( - context, - context.getString(R.string.no_reaction_type_setup_long_press_to_change), - Toast.LENGTH_SHORT - ) - .show() - } + accountViewModel.toast( + R.string.no_reactions_setup, + R.string.no_reaction_type_setup_long_press_to_change + ) } else if (!accountViewModel.isWriteable()) { if (accountViewModel.loggedInWithExternalSigner()) { onWantsToSignReaction() } else { - scope.launch { - Toast - .makeText( - context, - context.getString(R.string.login_with_a_private_key_to_like_posts), - Toast.LENGTH_SHORT - ) - .show() - } + accountViewModel.toast( + R.string.read_only_user, + R.string.login_with_a_private_key_to_like_posts + ) } } else if (accountViewModel.account.reactionChoices.size == 1) { - scope.launch(Dispatchers.IO) { - val reaction = accountViewModel.account.reactionChoices.first() - if (accountViewModel.hasReactedTo(baseNote, reaction)) { - accountViewModel.deleteReactionTo(baseNote, reaction) - } else { - accountViewModel.reactTo(baseNote, reaction) - } - } + accountViewModel.reactToOrDelete(baseNote) } else if (accountViewModel.account.reactionChoices.size > 1) { onMultipleChoices() } @@ -976,18 +925,19 @@ fun ZapReaction( zapClick( baseNote, accountViewModel, - scope, context, onZappingProgress = { progress: Float -> - zappingProgress = progress + scope.launch { + zappingProgress = progress + } }, onMultipleChoices = { wantsToZap = true }, - onError = { + onError = { title, message -> scope.launch { zappingProgress = 0f - showErrorMessageDialog = it + showErrorMessageDialog = message } }, onPayViaIntent = { @@ -1015,10 +965,10 @@ fun ZapReaction( wantsToZap = false wantsToChangeZapAmount = true }, - onError = { + onError = { title, message -> scope.launch { zappingProgress = 0f - showErrorMessageDialog = it + showErrorMessageDialog = message } }, onProgress = { @@ -1075,10 +1025,10 @@ fun ZapReaction( if (wantsToSetCustomZap) { ZapCustomDialog( onClose = { wantsToSetCustomZap = false }, - onError = { + onError = { title, message -> scope.launch { zappingProgress = 0f - showErrorMessageDialog = it + showErrorMessageDialog = message } }, onProgress = { @@ -1121,33 +1071,22 @@ fun ZapReaction( private fun zapClick( baseNote: Note, accountViewModel: AccountViewModel, - scope: CoroutineScope, context: Context, onZappingProgress: (Float) -> Unit, onMultipleChoices: () -> Unit, - onError: (String) -> Unit, + onError: (String, String) -> Unit, onPayViaIntent: (ImmutableList) -> Unit ) { if (accountViewModel.account.zapAmountChoices.isEmpty()) { - scope.launch { - Toast - .makeText( - context, - context.getString(R.string.no_zap_amount_setup_long_press_to_change), - Toast.LENGTH_SHORT - ) - .show() - } + accountViewModel.toast( + context.getString(R.string.error_dialog_zap_error), + context.getString(R.string.no_zap_amount_setup_long_press_to_change) + ) } else if (!accountViewModel.isWriteable() && !accountViewModel.loggedInWithExternalSigner()) { - scope.launch { - Toast - .makeText( - context, - context.getString(R.string.login_with_a_private_key_to_be_able_to_send_zaps), - Toast.LENGTH_SHORT - ) - .show() - } + accountViewModel.toast( + context.getString(R.string.error_dialog_zap_error), + context.getString(R.string.login_with_a_private_key_to_be_able_to_send_zaps) + ) } else if (accountViewModel.account.zapAmountChoices.size == 1) { accountViewModel.zap( baseNote, @@ -1157,9 +1096,7 @@ private fun zapClick( context, onError = onError, onProgress = { - scope.launch(Dispatchers.Main) { - onZappingProgress(it) - } + onZappingProgress(it) }, zapType = accountViewModel.account.defaultZapType, onPayViaIntent = onPayViaIntent @@ -1289,15 +1226,12 @@ private fun BoostTypeChoicePopup(baseNote: Note, iconSize: Dp, accountViewModel: onDismissRequest = { onDismiss() } ) { FlowRow { - val scope = rememberCoroutineScope() Button( modifier = Modifier.padding(horizontal = 3.dp), onClick = { if (accountViewModel.isWriteable()) { - scope.launch(Dispatchers.IO) { - accountViewModel.boost(baseNote) - onDismiss() - } + accountViewModel.boost(baseNote) + onDismiss() } else { onRepost() onDismiss() @@ -1470,12 +1404,11 @@ fun ZapAmountChoicePopup( accountViewModel: AccountViewModel, onDismiss: () -> Unit, onChangeAmount: () -> Unit, - onError: (text: String) -> Unit, + onError: (title: String, text: String) -> Unit, onProgress: (percent: Float) -> Unit, onPayViaIntent: (ImmutableList) -> Unit ) { val context = LocalContext.current - val scope = rememberCoroutineScope() val accountState by accountViewModel.accountLiveData.observeAsState() val account = accountState?.account ?: return val zapMessage = "" diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/RelayListRow.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/RelayListRow.kt index da119f789..d224e01c3 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/RelayListRow.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/RelayListRow.kt @@ -1,6 +1,5 @@ package com.vitorpamplona.amethyst.ui.note -import android.widget.Toast import androidx.compose.foundation.background import androidx.compose.foundation.clickable import androidx.compose.foundation.interaction.MutableInteractionSource @@ -49,7 +48,6 @@ import com.vitorpamplona.amethyst.ui.theme.Size15dp import com.vitorpamplona.amethyst.ui.theme.StdStartPadding import com.vitorpamplona.amethyst.ui.theme.placeholderText import kotlinx.collections.immutable.toImmutableList -import kotlinx.coroutines.launch @Composable public fun RelayBadgesHorizontal(baseNote: Note, accountViewModel: AccountViewModel, nav: (String) -> Unit) { @@ -159,15 +157,10 @@ fun RenderRelay(relay: RelayBriefInfo, accountViewModel: AccountViewModel, nav: Nip11Retriever.ErrorCode.FAIL_WITH_HTTP_STATUS -> context.getString(R.string.relay_information_document_error_assemble_url, url, exceptionMessage) } - scope.launch { - Toast - .makeText( - context, - msg, - Toast.LENGTH_SHORT - ) - .show() - } + accountViewModel.toast( + context.getString(R.string.unable_to_download_relay_document), + msg + ) } ) } diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/UpdateZapAmountDialog.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/UpdateZapAmountDialog.kt index 6c2a6a09f..3a5ef0317 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/UpdateZapAmountDialog.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/UpdateZapAmountDialog.kt @@ -5,7 +5,6 @@ import android.app.KeyguardManager import android.content.Context import android.content.Intent import android.os.Build -import android.widget.Toast import androidx.activity.compose.ManagedActivityResultLauncher import androidx.activity.compose.rememberLauncherForActivityResult import androidx.activity.result.ActivityResult @@ -82,7 +81,6 @@ import com.vitorpamplona.quartz.encoders.decodePublicKey import com.vitorpamplona.quartz.encoders.toHexKey import com.vitorpamplona.quartz.events.LnZapEvent import kotlinx.collections.immutable.toImmutableList -import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.launch import androidx.compose.runtime.rememberCoroutineScope as rememberCoroutineScope @@ -228,9 +226,16 @@ fun UpdateZapAmountDialog( try { postViewModel.updateNIP47(nip47uri) } catch (e: IllegalArgumentException) { - scope.launch { - Toast.makeText(context, e.message, Toast.LENGTH_SHORT) - .show() + if (e.message != null) { + accountViewModel.toast( + context.getString(R.string.error_parsing_nip47_title), + context.getString(R.string.error_parsing_nip47, nip47uri, e.message!!) + ) + } else { + accountViewModel.toast( + context.getString(R.string.error_parsing_nip47_title), + context.getString(R.string.error_parsing_nip47_no_error, nip47uri) + ) } } } @@ -444,9 +449,16 @@ fun UpdateZapAmountDialog( try { postViewModel.updateNIP47(it) } catch (e: IllegalArgumentException) { - scope.launch { - Toast.makeText(context, e.message, Toast.LENGTH_SHORT) - .show() + if (e.message != null) { + accountViewModel.toast( + context.getString(R.string.error_parsing_nip47_title), + context.getString(R.string.error_parsing_nip47, it, e.message!!) + ) + } else { + accountViewModel.toast( + context.getString(R.string.error_parsing_nip47_title), + context.getString(R.string.error_parsing_nip47_no_error, it) + ) } } } @@ -505,7 +517,6 @@ fun UpdateZapAmountDialog( mutableStateOf(false) } - val scope = rememberCoroutineScope() val context = LocalContext.current val keyguardLauncher = @@ -544,13 +555,16 @@ fun UpdateZapAmountDialog( IconButton(onClick = { if (!showPassword) { authenticate( - authTitle, - context, - scope, - keyguardLauncher - ) { - showPassword = true - } + title = authTitle, + context = context, + keyguardLauncher = keyguardLauncher, + onApproved = { + showPassword = true + }, + onError = { title, message -> + accountViewModel.toast(title, message) + } + ) } else { showPassword = false } @@ -580,9 +594,9 @@ fun UpdateZapAmountDialog( fun authenticate( title: String, context: Context, - scope: CoroutineScope, keyguardLauncher: ManagedActivityResultLauncher, - onApproved: () -> Unit + onApproved: () -> Unit, + onError: (String, String) -> Unit ) { val fragmentContext = context.getFragmentActivity()!! val keyguardManager = @@ -626,26 +640,19 @@ fun authenticate( when (errorCode) { BiometricPrompt.ERROR_NEGATIVE_BUTTON -> keyguardPrompt() BiometricPrompt.ERROR_LOCKOUT -> keyguardPrompt() - else -> - scope.launch { - Toast.makeText( - context, - "${context.getString(R.string.biometric_error)}: $errString", - Toast.LENGTH_SHORT - ).show() - } + else -> onError( + context.getString(R.string.biometric_authentication_failed), + context.getString(R.string.biometric_authentication_failed_explainer_with_error, errString) + ) } } override fun onAuthenticationFailed() { super.onAuthenticationFailed() - scope.launch { - Toast.makeText( - context, - context.getString(R.string.biometric_authentication_failed), - Toast.LENGTH_SHORT - ).show() - } + onError( + context.getString(R.string.biometric_authentication_failed), + context.getString(R.string.biometric_authentication_failed_explainer) + ) } override fun onAuthenticationSucceeded(result: BiometricPrompt.AuthenticationResult) { diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/UserProfilePicture.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/UserProfilePicture.kt index 3b3879f2f..4cfb09f90 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/UserProfilePicture.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/UserProfilePicture.kt @@ -435,10 +435,8 @@ fun NoteDropDownMenu(note: Note, popupExpanded: MutableState, accountVi }, onClick = { val author = note.author ?: return@DropdownMenuItem - scope.launch(Dispatchers.IO) { - accountViewModel.follow(author) - onDismiss() - } + accountViewModel.follow(author) + onDismiss() } ) Divider() diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/ZapCustomDialog.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/ZapCustomDialog.kt index de22b4d00..c72da9f03 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/ZapCustomDialog.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/ZapCustomDialog.kt @@ -91,7 +91,7 @@ class ZapOptionstViewModel : ViewModel() { @Composable fun ZapCustomDialog( onClose: () -> Unit, - onError: (text: String) -> Unit, + onError: (title: String, text: String) -> Unit, onProgress: (percent: Float) -> Unit, onPayViaIntent: (ImmutableList) -> Unit, accountViewModel: AccountViewModel, @@ -254,7 +254,7 @@ fun ErrorMessageDialog( title: String, textContent: String, buttonColors: ButtonColors = ButtonDefaults.buttonColors(), - onClickStartMessage: () -> Unit, + onClickStartMessage: (() -> Unit)? = null, onDismiss: () -> Unit ) { AlertDialog( @@ -274,13 +274,15 @@ fun ErrorMessageDialog( .fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween ) { - TextButton(onClick = onClickStartMessage) { - Icon( - painter = painterResource(R.drawable.ic_dm), - contentDescription = null - ) - Spacer(StdHorzSpacer) - Text(stringResource(R.string.error_dialog_talk_to_user)) + onClickStartMessage?.let { + TextButton(onClick = onClickStartMessage) { + Icon( + painter = painterResource(R.drawable.ic_dm), + contentDescription = null + ) + Spacer(StdHorzSpacer) + Text(stringResource(R.string.error_dialog_talk_to_user)) + } } Button(onClick = onDismiss, colors = buttonColors, contentPadding = PaddingValues(horizontal = Size16dp)) { Row( diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/ZapNoteCompose.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/ZapNoteCompose.kt index f2b34a2d5..6a7a3664b 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/ZapNoteCompose.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/ZapNoteCompose.kt @@ -1,6 +1,5 @@ package com.vitorpamplona.amethyst.ui.note -import android.widget.Toast import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column @@ -20,7 +19,6 @@ import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp @@ -182,9 +180,6 @@ fun ShowFollowingOrUnfollowingButton( baseAuthor: User, accountViewModel: AccountViewModel ) { - val scope = rememberCoroutineScope() - val context = LocalContext.current - var isFollowing by remember { mutableStateOf(false) } val accountFollowsState by accountViewModel.account.userProfile().live().follows.observeAsState() @@ -203,48 +198,30 @@ fun ShowFollowingOrUnfollowingButton( UnfollowButton { if (!accountViewModel.isWriteable()) { if (accountViewModel.loggedInWithExternalSigner()) { - scope.launch(Dispatchers.IO) { - accountViewModel.unfollow(baseAuthor) - } + accountViewModel.unfollow(baseAuthor) } else { - scope.launch { - Toast - .makeText( - context, - context.getString(R.string.login_with_a_private_key_to_be_able_to_unfollow), - Toast.LENGTH_SHORT - ) - .show() - } + accountViewModel.toast( + R.string.read_only_user, + R.string.login_with_a_private_key_to_be_able_to_unfollow + ) } } else { - scope.launch(Dispatchers.IO) { - accountViewModel.unfollow(baseAuthor) - } + accountViewModel.unfollow(baseAuthor) } } } else { FollowButton { if (!accountViewModel.isWriteable()) { if (accountViewModel.loggedInWithExternalSigner()) { - scope.launch(Dispatchers.IO) { - accountViewModel.account.follow(baseAuthor) - } + accountViewModel.follow(baseAuthor) } else { - scope.launch { - Toast - .makeText( - context, - context.getString(R.string.login_with_a_private_key_to_be_able_to_follow), - Toast.LENGTH_SHORT - ) - .show() - } + accountViewModel.toast( + R.string.read_only_user, + R.string.login_with_a_private_key_to_be_able_to_follow + ) } } else { - scope.launch(Dispatchers.IO) { - accountViewModel.follow(baseAuthor) - } + accountViewModel.follow(baseAuthor) } } } diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/AccountBackupDialog.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/AccountBackupDialog.kt index c9d580917..2350fe388 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/AccountBackupDialog.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/AccountBackupDialog.kt @@ -52,7 +52,7 @@ import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.launch @Composable -fun AccountBackupDialog(account: Account, onClose: () -> Unit) { +fun AccountBackupDialog(accountViewModel: AccountViewModel, onClose: () -> Unit) { Dialog( onDismissRequest = onClose, properties = DialogProperties(usePlatformDefaultWidth = false) @@ -90,7 +90,7 @@ fun AccountBackupDialog(account: Account, onClose: () -> Unit) { Spacer(modifier = Modifier.height(30.dp)) - NSecCopyButton(account) + NSecCopyButton(accountViewModel) } } } @@ -99,7 +99,7 @@ fun AccountBackupDialog(account: Account, onClose: () -> Unit) { @Composable private fun NSecCopyButton( - account: Account + accountViewModel: AccountViewModel ) { val clipboardManager = LocalClipboardManager.current val context = LocalContext.current @@ -108,7 +108,7 @@ private fun NSecCopyButton( val keyguardLauncher = rememberLauncherForActivityResult(ActivityResultContracts.StartActivityForResult()) { result: ActivityResult -> if (result.resultCode == Activity.RESULT_OK) { - copyNSec(context, scope, account, clipboardManager) + copyNSec(context, scope, accountViewModel.account, clipboardManager) } } @@ -118,11 +118,14 @@ private fun NSecCopyButton( authenticate( title = context.getString(R.string.copy_my_secret_key), context = context, - scope = scope, - keyguardLauncher = keyguardLauncher - ) { - copyNSec(context, scope, account, clipboardManager) - } + keyguardLauncher = keyguardLauncher, + onApproved = { + copyNSec(context, scope, accountViewModel.account, clipboardManager) + }, + onError = { title, message -> + accountViewModel.toast(title, message) + } + ) }, shape = ButtonBorder, colors = ButtonDefaults.buttonColors( diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/AccountViewModel.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/AccountViewModel.kt index 50de63265..dcc033aaa 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/AccountViewModel.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/AccountViewModel.kt @@ -60,12 +60,22 @@ import kotlinx.collections.immutable.toImmutableList import kotlinx.collections.immutable.toImmutableSet import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Job +import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.launch import java.math.BigDecimal import java.util.Locale import kotlin.time.measureTimedValue +@Immutable +open class ToastMsg() + +@Immutable +class StringToastMsg(val title: String, val msg: String) : ToastMsg() + +@Immutable +class ResourceToastMsg(val titleResId: Int, val resourceId: Int) : ToastMsg() + @Stable class AccountViewModel(val account: Account) : ViewModel(), Dao { val accountLiveData: LiveData = account.live.map { it } @@ -75,6 +85,8 @@ class AccountViewModel(val account: Account) : ViewModel(), Dao { val userFollows: LiveData = account.userProfile().live().follows.map { it } val userRelays: LiveData = account.userProfile().live().relays.map { it } + val toasts = MutableSharedFlow() + val discoveryListLiveData = account.live.map { it.account.defaultDiscoveryFollowList }.distinctUntilChanged() @@ -95,6 +107,24 @@ class AccountViewModel(val account: Account) : ViewModel(), Dao { it.account.showSensitiveContent }.distinctUntilChanged() + fun clearToasts() { + viewModelScope.launch { + toasts.emit(null) + } + } + + fun toast(title: String, message: String) { + viewModelScope.launch { + toasts.emit(StringToastMsg(title, message)) + } + } + + fun toast(titleResId: Int, resourceId: Int) { + viewModelScope.launch { + toasts.emit(ResourceToastMsg(titleResId, resourceId)) + } + } + fun updateAutomaticallyStartPlayback( automaticallyStartPlayback: ConnectivityType ) { @@ -152,6 +182,17 @@ class AccountViewModel(val account: Account) : ViewModel(), Dao { } } + fun reactToOrDelete(note: Note) { + viewModelScope.launch(Dispatchers.IO) { + val reaction = account.reactionChoices.first() + if (hasReactedTo(note, reaction)) { + deleteReactionTo(note, reaction) + } else { + reactTo(note, reaction) + } + } + } + fun isNoteHidden(note: Note): Boolean { val isSensitive = note.event?.isSensitive() ?: false return account.isHidden(note.author!!) || (isSensitive && account.showSensitiveContent == false) @@ -170,7 +211,9 @@ class AccountViewModel(val account: Account) : ViewModel(), Dao { } fun deleteBoostsTo(note: Note) { - account.delete(account.boostsTo(note)) + viewModelScope.launch(Dispatchers.IO) { + account.delete(account.boostsTo(note)) + } } fun calculateIfNoteWasZappedByAccount(zappedNote: Note, onWasZapped: (Boolean) -> Unit) { @@ -286,7 +329,7 @@ class AccountViewModel(val account: Account) : ViewModel(), Dao { pollOption: Int?, message: String, context: Context, - onError: (String) -> Unit, + onError: (String, String) -> Unit, onProgress: (percent: Float) -> Unit, onPayViaIntent: (ImmutableList) -> Unit, zapType: LnZapEvent.ZapType @@ -308,7 +351,9 @@ class AccountViewModel(val account: Account) : ViewModel(), Dao { } fun boost(note: Note) { - account.boost(note) + viewModelScope.launch(Dispatchers.IO) { + account.boost(note) + } } fun removeEmojiPack(usersEmojiList: Note, emojiList: Note) { @@ -388,11 +433,51 @@ class AccountViewModel(val account: Account) : ViewModel(), Dao { } fun follow(user: User) { - account.follow(user) + viewModelScope.launch(Dispatchers.IO) { + account.follow(user) + } } fun unfollow(user: User) { - account.unfollow(user) + viewModelScope.launch(Dispatchers.IO) { + account.unfollow(user) + } + } + + fun followGeohash(tag: String) { + viewModelScope.launch(Dispatchers.IO) { + account.followGeohash(tag) + } + } + + fun unfollowGeohash(tag: String) { + viewModelScope.launch(Dispatchers.IO) { + account.unfollowGeohash(tag) + } + } + + fun followHashtag(tag: String) { + viewModelScope.launch(Dispatchers.IO) { + account.followHashtag(tag) + } + } + + fun unfollowHashtag(tag: String) { + viewModelScope.launch(Dispatchers.IO) { + account.unfollowHashtag(tag) + } + } + + fun showWord(word: String) { + viewModelScope.launch(Dispatchers.IO) { + account.showWord(word) + } + } + + fun hideWord(word: String) { + viewModelScope.launch(Dispatchers.IO) { + account.hideWord(word) + } } fun isLoggedUser(user: User?): Boolean { diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/ConnectOrbotDialog.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/ConnectOrbotDialog.kt index d2abb8c28..4e788c135 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/ConnectOrbotDialog.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/ConnectOrbotDialog.kt @@ -1,6 +1,5 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn -import android.widget.Toast import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row @@ -17,11 +16,9 @@ import androidx.compose.material3.Surface import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.MutableState -import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color -import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.SpanStyle import androidx.compose.ui.text.input.KeyboardCapitalization @@ -37,12 +34,9 @@ import com.vitorpamplona.amethyst.ui.actions.CloseButton import com.vitorpamplona.amethyst.ui.theme.ButtonBorder import com.vitorpamplona.amethyst.ui.theme.RichTextDefaults import com.vitorpamplona.amethyst.ui.theme.placeholderText -import kotlinx.coroutines.launch @Composable -fun ConnectOrbotDialog(onClose: () -> Unit, onPost: () -> Unit, portNumber: MutableState) { - val context = LocalContext.current - val scope = rememberCoroutineScope() +fun ConnectOrbotDialog(onClose: () -> Unit, onPost: () -> Unit, onError: (String) -> Unit, portNumber: MutableState) { Dialog( onDismissRequest = onClose, properties = DialogProperties(usePlatformDefaultWidth = false, decorFitsSystemWindows = false) @@ -67,13 +61,7 @@ fun ConnectOrbotDialog(onClose: () -> Unit, onPost: () -> Unit, portNumber: Muta try { Integer.parseInt(portNumber.value) } catch (_: Exception) { - scope.launch { - Toast.makeText( - context, - toastMessage, - Toast.LENGTH_LONG - ).show() - } + onError(toastMessage) return@UseOrbotButton } diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/GeoHashScreen.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/GeoHashScreen.kt index 0dc73aa73..fac64a79b 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/GeoHashScreen.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/GeoHashScreen.kt @@ -1,6 +1,5 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn -import android.widget.Toast import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column @@ -18,7 +17,6 @@ import androidx.compose.runtime.getValue import androidx.compose.runtime.livedata.observeAsState 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 @@ -160,9 +158,6 @@ fun GeoHashActionOptions( tag: String, accountViewModel: AccountViewModel ) { - val scope = rememberCoroutineScope() - val context = LocalContext.current - val userState by accountViewModel.userProfile().live().follows.observeAsState() val isFollowingTag by remember(userState) { derivedStateOf { @@ -174,48 +169,30 @@ fun GeoHashActionOptions( UnfollowButton { if (!accountViewModel.isWriteable()) { if (accountViewModel.loggedInWithExternalSigner()) { - scope.launch(Dispatchers.IO) { - accountViewModel.account.unfollowGeohash(tag) - } + accountViewModel.unfollowGeohash(tag) } else { - scope.launch { - Toast - .makeText( - context, - context.getString(R.string.login_with_a_private_key_to_be_able_to_unfollow), - Toast.LENGTH_SHORT - ) - .show() - } + accountViewModel.toast( + R.string.read_only_user, + R.string.login_with_a_private_key_to_be_able_to_unfollow + ) } } else { - scope.launch(Dispatchers.IO) { - accountViewModel.account.unfollowGeohash(tag) - } + accountViewModel.unfollowGeohash(tag) } } } else { FollowButton { if (!accountViewModel.isWriteable()) { if (accountViewModel.loggedInWithExternalSigner()) { - scope.launch(Dispatchers.IO) { - accountViewModel.account.followGeohash(tag) - } + accountViewModel.followGeohash(tag) } else { - scope.launch { - Toast - .makeText( - context, - context.getString(R.string.login_with_a_private_key_to_be_able_to_follow), - Toast.LENGTH_SHORT - ) - .show() - } + accountViewModel.toast( + R.string.read_only_user, + R.string.login_with_a_private_key_to_be_able_to_follow + ) } } else { - scope.launch(Dispatchers.IO) { - accountViewModel.account.followGeohash(tag) - } + accountViewModel.followGeohash(tag) } } } diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/HashtagScreen.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/HashtagScreen.kt index 569b1a028..0f0e13295 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/HashtagScreen.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/HashtagScreen.kt @@ -1,6 +1,5 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn -import android.widget.Toast import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column @@ -16,10 +15,8 @@ import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.getValue import androidx.compose.runtime.livedata.observeAsState 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.LocalContext import androidx.compose.ui.platform.LocalLifecycleOwner import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.dp @@ -31,8 +28,6 @@ import com.vitorpamplona.amethyst.service.NostrHashtagDataSource import com.vitorpamplona.amethyst.ui.screen.NostrHashtagFeedViewModel import com.vitorpamplona.amethyst.ui.screen.RefresheableFeedView import com.vitorpamplona.amethyst.ui.theme.StdPadding -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.launch @Composable fun HashtagScreen(tag: String?, accountViewModel: AccountViewModel, nav: (String) -> Unit) { @@ -136,9 +131,6 @@ fun HashtagActionOptions( tag: String, accountViewModel: AccountViewModel ) { - val scope = rememberCoroutineScope() - val context = LocalContext.current - val userState by accountViewModel.userProfile().live().follows.observeAsState() val isFollowingTag by remember(userState) { derivedStateOf { @@ -150,48 +142,30 @@ fun HashtagActionOptions( UnfollowButton { if (!accountViewModel.isWriteable()) { if (accountViewModel.loggedInWithExternalSigner()) { - scope.launch(Dispatchers.IO) { - accountViewModel.account.unfollowHashtag(tag) - } + accountViewModel.unfollowHashtag(tag) } else { - scope.launch { - Toast - .makeText( - context, - context.getString(R.string.login_with_a_private_key_to_be_able_to_unfollow), - Toast.LENGTH_SHORT - ) - .show() - } + accountViewModel.toast( + R.string.read_only_user, + R.string.login_with_a_private_key_to_be_able_to_unfollow + ) } } else { - scope.launch(Dispatchers.IO) { - accountViewModel.account.unfollowHashtag(tag) - } + accountViewModel.unfollowHashtag(tag) } } } else { FollowButton { if (!accountViewModel.isWriteable()) { if (accountViewModel.loggedInWithExternalSigner()) { - scope.launch(Dispatchers.IO) { - accountViewModel.account.followHashtag(tag) - } + accountViewModel.followHashtag(tag) } else { - scope.launch { - Toast - .makeText( - context, - context.getString(R.string.login_with_a_private_key_to_be_able_to_follow), - Toast.LENGTH_SHORT - ) - .show() - } + accountViewModel.toast( + R.string.read_only_user, + R.string.login_with_a_private_key_to_be_able_to_follow + ) } } else { - scope.launch(Dispatchers.IO) { - accountViewModel.account.followHashtag(tag) - } + accountViewModel.followHashtag(tag) } } } diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/HiddenUsersScreen.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/HiddenUsersScreen.kt index 4c9d2c5b3..6d207e204 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/HiddenUsersScreen.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/HiddenUsersScreen.kt @@ -1,6 +1,5 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn -import android.widget.Toast import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column @@ -35,7 +34,6 @@ import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color -import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalLifecycleOwner import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.font.FontWeight @@ -65,7 +63,6 @@ import com.vitorpamplona.amethyst.ui.theme.Size10dp import com.vitorpamplona.amethyst.ui.theme.StdPadding import com.vitorpamplona.amethyst.ui.theme.TabRowHeight import com.vitorpamplona.amethyst.ui.theme.placeholderText -import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch @Composable @@ -307,9 +304,6 @@ fun MutedWordActionOptions( word: String, accountViewModel: AccountViewModel ) { - val scope = rememberCoroutineScope() - val context = LocalContext.current - val isMutedWord by accountViewModel.account.liveHiddenUsers.map { word in it.hiddenWords }.distinctUntilChanged().observeAsState() @@ -318,48 +312,30 @@ fun MutedWordActionOptions( ShowWordButton { if (!accountViewModel.isWriteable()) { if (accountViewModel.loggedInWithExternalSigner()) { - scope.launch(Dispatchers.IO) { - accountViewModel.account.showWord(word) - } + accountViewModel.showWord(word) } else { - scope.launch { - Toast - .makeText( - context, - context.getString(R.string.login_with_a_private_key_to_be_able_to_unfollow), - Toast.LENGTH_SHORT - ) - .show() - } + accountViewModel.toast( + R.string.read_only_user, + R.string.login_with_a_private_key_to_be_able_to_show_word + ) } } else { - scope.launch(Dispatchers.IO) { - accountViewModel.account.showWord(word) - } + accountViewModel.showWord(word) } } } else { HideWordButton { if (!accountViewModel.isWriteable()) { if (accountViewModel.loggedInWithExternalSigner()) { - scope.launch(Dispatchers.IO) { - accountViewModel.account.hideWord(word) - } + accountViewModel.hideWord(word) } else { - scope.launch { - Toast - .makeText( - context, - context.getString(R.string.login_with_a_private_key_to_be_able_to_follow), - Toast.LENGTH_SHORT - ) - .show() - } + accountViewModel.toast( + R.string.read_only_user, + R.string.login_with_a_private_key_to_be_able_to_hide_word + ) } } else { - scope.launch(Dispatchers.IO) { - accountViewModel.account.hideWord(word) - } + accountViewModel.hideWord(word) } } } diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/MainScreen.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/MainScreen.kt index 13f17c98e..f2e2b9662 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/MainScreen.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/MainScreen.kt @@ -39,6 +39,7 @@ import androidx.compose.ui.input.nestedscroll.NestedScrollConnection import androidx.compose.ui.input.nestedscroll.NestedScrollSource import androidx.compose.ui.input.nestedscroll.nestedScroll import androidx.compose.ui.platform.LocalConfiguration +import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.platform.LocalLifecycleOwner import androidx.compose.ui.unit.dp @@ -49,6 +50,7 @@ import androidx.navigation.NavBackStackEntry import androidx.navigation.compose.currentBackStackEntryAsState import androidx.navigation.compose.rememberNavController import com.vitorpamplona.amethyst.model.BooleanType +import com.vitorpamplona.amethyst.ui.actions.InformationDialog import com.vitorpamplona.amethyst.ui.buttons.ChannelFabColumn import com.vitorpamplona.amethyst.ui.buttons.NewCommunityNoteButton import com.vitorpamplona.amethyst.ui.buttons.NewImageButton @@ -119,6 +121,8 @@ fun MainScreen( } } + DisplayErrorMessages(accountViewModel) + val navPopBack = remember(navController) { { navController.popBackStack() @@ -356,6 +360,30 @@ fun MainScreen( } } +@Composable +private fun DisplayErrorMessages(accountViewModel: AccountViewModel) { + val context = LocalContext.current + val openDialogMsg = accountViewModel.toasts.collectAsState(initial = null) + + openDialogMsg.value?.let { obj -> + when (obj) { + is ResourceToastMsg -> InformationDialog( + context.getString(obj.titleResId), + context.getString(obj.resourceId) + ) { + accountViewModel.clearToasts() + } + + is StringToastMsg -> InformationDialog( + obj.title, + obj.msg + ) { + accountViewModel.clearToasts() + } + } + } +} + @Composable fun WatchNavStateToUpdateBarVisibility(navState: State, bottomBarOffsetHeightPx: MutableState) { LaunchedEffect(key1 = navState.value) { diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/ProfileScreen.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/ProfileScreen.kt index 46f2db189..7db594e4d 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/ProfileScreen.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/ProfileScreen.kt @@ -1,8 +1,5 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn -import android.content.Intent -import android.net.Uri -import android.widget.Toast import androidx.compose.animation.Crossfade import androidx.compose.animation.core.tween import androidx.compose.foundation.* @@ -46,7 +43,6 @@ import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.IntSize import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp -import androidx.core.content.ContextCompat import androidx.lifecycle.Lifecycle import androidx.lifecycle.LifecycleEventObserver import androidx.lifecycle.distinctUntilChanged @@ -62,6 +58,7 @@ import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.service.NostrUserProfileDataSource import com.vitorpamplona.amethyst.service.connectivitystatus.ConnectivityStatus +import com.vitorpamplona.amethyst.ui.actions.InformationDialog import com.vitorpamplona.amethyst.ui.actions.NewUserMetadataView import com.vitorpamplona.amethyst.ui.components.CreateTextWithEmoji import com.vitorpamplona.amethyst.ui.components.DisplayNip05ProfileStatus @@ -74,8 +71,11 @@ import com.vitorpamplona.amethyst.ui.components.figureOutMimeType import com.vitorpamplona.amethyst.ui.dal.UserProfileReportsFeedFilter import com.vitorpamplona.amethyst.ui.navigation.ShowQRDialog import com.vitorpamplona.amethyst.ui.note.ClickableUserPicture +import com.vitorpamplona.amethyst.ui.note.ErrorMessageDialog import com.vitorpamplona.amethyst.ui.note.LightningAddressIcon import com.vitorpamplona.amethyst.ui.note.LoadAddressableNote +import com.vitorpamplona.amethyst.ui.note.payViaIntent +import com.vitorpamplona.amethyst.ui.note.routeToMessage import com.vitorpamplona.amethyst.ui.screen.FeedState import com.vitorpamplona.amethyst.ui.screen.LnZapFeedView import com.vitorpamplona.amethyst.ui.screen.NostrUserAppRecommendationsFeedViewModel @@ -739,9 +739,6 @@ private fun DisplayFollowUnfollowButton( baseUser: User, accountViewModel: AccountViewModel ) { - val scope = rememberCoroutineScope() - val context = LocalContext.current - val isLoggedInFollowingUser by accountViewModel.account.userProfile().live().follows.map { it.user.isFollowing(baseUser) }.distinctUntilChanged().observeAsState(initial = accountViewModel.account.isFollowing(baseUser)) @@ -754,24 +751,15 @@ private fun DisplayFollowUnfollowButton( UnfollowButton { if (!accountViewModel.isWriteable()) { if (accountViewModel.loggedInWithExternalSigner()) { - scope.launch(Dispatchers.IO) { - accountViewModel.account.unfollow(baseUser) - } + accountViewModel.unfollow(baseUser) } else { - scope.launch { - Toast - .makeText( - context, - context.getString(R.string.login_with_a_private_key_to_be_able_to_unfollow), - Toast.LENGTH_SHORT - ) - .show() - } + accountViewModel.toast( + R.string.read_only_user, + R.string.login_with_a_private_key_to_be_able_to_unfollow + ) } } else { - scope.launch(Dispatchers.IO) { - accountViewModel.account.unfollow(baseUser) - } + accountViewModel.unfollow(baseUser) } } } else { @@ -779,48 +767,30 @@ private fun DisplayFollowUnfollowButton( FollowButton(R.string.follow_back) { if (!accountViewModel.isWriteable()) { if (accountViewModel.loggedInWithExternalSigner()) { - scope.launch(Dispatchers.IO) { - accountViewModel.account.follow(baseUser) - } + accountViewModel.follow(baseUser) } else { - scope.launch { - Toast - .makeText( - context, - context.getString(R.string.login_with_a_private_key_to_be_able_to_follow), - Toast.LENGTH_SHORT - ) - .show() - } + accountViewModel.toast( + R.string.read_only_user, + R.string.login_with_a_private_key_to_be_able_to_follow + ) } } else { - scope.launch(Dispatchers.IO) { - accountViewModel.account.follow(baseUser) - } + accountViewModel.follow(baseUser) } } } else { FollowButton(R.string.follow) { if (!accountViewModel.isWriteable()) { if (accountViewModel.loggedInWithExternalSigner()) { - scope.launch(Dispatchers.IO) { - accountViewModel.account.follow(baseUser) - } + accountViewModel.follow(baseUser) } else { - scope.launch { - Toast - .makeText( - context, - context.getString(R.string.login_with_a_private_key_to_be_able_to_follow), - Toast.LENGTH_SHORT - ) - .show() - } + accountViewModel.toast( + R.string.read_only_user, + R.string.login_with_a_private_key_to_be_able_to_follow + ) } } else { - scope.launch(Dispatchers.IO) { - accountViewModel.account.follow(baseUser) - } + accountViewModel.follow(baseUser) } } } @@ -974,7 +944,7 @@ private fun DrawAdditionalInfo( val lud16 = remember(userState) { user.info?.lud16?.trim() ?: user.info?.lud06?.trim() } val pubkeyHex = remember { baseUser.pubkeyHex } - DisplayLNAddress(lud16, pubkeyHex, accountViewModel.account) + DisplayLNAddress(lud16, pubkeyHex, accountViewModel, nav) val identities = user.info?.latestMetadata?.identityClaims() if (!identities.isNullOrEmpty()) { @@ -1026,12 +996,39 @@ private fun DrawAdditionalInfo( fun DisplayLNAddress( lud16: String?, userHex: String, - account: Account + accountViewModel: AccountViewModel, + nav: (String) -> Unit ) { val context = LocalContext.current val scope = rememberCoroutineScope() var zapExpanded by remember { mutableStateOf(false) } + var showErrorMessageDialog by remember { mutableStateOf(null) } + + if (showErrorMessageDialog != null) { + ErrorMessageDialog( + title = stringResource(id = R.string.error_dialog_zap_error), + textContent = showErrorMessageDialog ?: "", + onClickStartMessage = { + scope.launch(Dispatchers.IO) { + val route = routeToMessage(userHex, showErrorMessageDialog, accountViewModel) + nav(route) + } + }, + onDismiss = { showErrorMessageDialog = null } + ) + } + + var showInfoMessageDialog by remember { mutableStateOf(null) } + if (showInfoMessageDialog != null) { + InformationDialog( + title = context.getString(R.string.payment_successful), + textContent = showInfoMessageDialog ?: "" + ) { + showInfoMessageDialog = null + } + } + if (!lud16.isNullOrEmpty()) { Row(verticalAlignment = Alignment.CenterVertically) { LightningAddressIcon(modifier = Size16Modifier, tint = BitcoinOrange) @@ -1054,50 +1051,31 @@ fun DisplayLNAddress( InvoiceRequestCard( lud16, userHex, - account, + accountViewModel.account, onSuccess = { zapExpanded = false // pay directly - if (account.hasWalletConnectSetup()) { - account.sendZapPaymentRequestFor(it, null) { response -> + if (accountViewModel.account.hasWalletConnectSetup()) { + accountViewModel.account.sendZapPaymentRequestFor(it, null) { response -> if (response is PayInvoiceSuccessResponse) { - scope.launch { - Toast.makeText( - context, - context.getString(R.string.payment_successful), // Turn this into a UI animation - Toast.LENGTH_LONG - ).show() - } + showInfoMessageDialog = context.getString(R.string.payment_successful) } else if (response is PayInvoiceErrorResponse) { - scope.launch { - Toast.makeText( - context, - response.error?.message - ?: response.error?.code?.toString() - ?: context.getString(R.string.error_parsing_error_message), - Toast.LENGTH_LONG - ).show() - } + showErrorMessageDialog = response.error?.message + ?: response.error?.code?.toString() + ?: context.getString(R.string.error_parsing_error_message) } } } else { - try { - val intent = Intent(Intent.ACTION_VIEW, Uri.parse("lightning:$it")) - intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK - ContextCompat.startActivity(context, intent, null) - } catch (e: Exception) { - scope.launch { - Toast.makeText( - context, - context.getString(R.string.lightning_wallets_not_found), - Toast.LENGTH_LONG - ).show() - } + payViaIntent(it, context) { + showErrorMessageDialog = it } } }, onClose = { zapExpanded = false + }, + onError = { title, message -> + accountViewModel.toast(title, message) } ) } diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedOff/LoginScreen.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedOff/LoginScreen.kt index 61647802a..90ff50241 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedOff/LoginScreen.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedOff/LoginScreen.kt @@ -311,6 +311,15 @@ fun LoginPage( connectOrbotDialogOpen = false useProxy.value = true }, + onError = { + scope.launch { + Toast.makeText( + context, + it, + Toast.LENGTH_LONG + ).show() + } + }, proxyPort ) } diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 3d1a8397e..f7a4b2d4c 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -30,13 +30,17 @@ Report Impersonation Report Explicit Content Report Illegal Behaviour - Login with a Private key to be able to reply - Login with a Private key to be able to boost posts - Login with a Private key to like Posts + You are using a public key and public keys are read-only. Login with a Private key to be able to reply + You are using a public key and public keys are read-only. Login with a Private key to be able to boost posts + You are using a public key and public keys are read-only. Login with a Private key to like posts No Zap Amount Setup. Long Press to change - Login with a Private key to be able to send Zaps - Login with a Private key to be able to Follow - Login with a Private key to be able to Unfollow + You are using a public key and public keys are read-only. Login with a Private key to be able to send zaps + You are using a public key and public keys are read-only. Login with a Private key to be able to follow + You are using a public key and public keys are read-only. Login with a Private key to be able to unfollow + You are using a public key and public keys are read-only. Login with a Private key to be able to hide a word or sentence + You are using a public key and public keys are read-only. Login with a Private key to be able to show a word or sentence + + Zaps View count Boost @@ -195,6 +199,8 @@ Secret key (nsec) copied to clipboard Copy my secret key Authentication failed + Biometrics failed to authenticate the owner of this phone + Biometrics failed to authenticate the owner of this phone. Error: %1$s Error "Created by %1$s" "Badge award image for %1$s" @@ -285,7 +291,8 @@ (0–100)% Close after days - Poll is closed to new votes + Unable to vote + Poll is closed to new votes Zap amount Only one vote per user is allowed on this type of poll @@ -301,6 +308,7 @@ Poll authors can\'t vote in their own polls. #zappoll + What does this mean? This content is the same since the post This content has changed. The author might not have seen or approved the change @@ -426,7 +434,7 @@ Warn when posts have reports from your follows New Reaction Symbol - No reaction types selected. Long Press to change + No reaction types pre-selected for this user. Long press on the heart button to change Zapraiser Adds a target amount of sats to raise for this post. Supporting clients may show this as a progress bar to incentivize donations @@ -581,6 +589,7 @@ Supporting clients will split and forward zaps to the users added here instead of yours Search and Add User Username or display name + Missing lightning setup User %1$s does not have a lightning address set up to receive sats Percentage 25 @@ -602,4 +611,37 @@ Show Profile pictures Select an Option + + Could not pay invoice + Could not withdraw + + Could not setup Wallet Connect + Error parsing NIP-47 connection string. Check if this is correct with your Wallet provider: %1$s. Error: %2$s + Error parsing NIP-47 connection string. Check if this is correct with your Wallet provider: %1$s. + + Could not redeem Cashu + Mint provided the following error message: %1$s + Cashu tokens already spent. + + Cashu Received + %1$s sats were sent to your wallet. (Fees: %2$s sats) + + Unable to fetch invoice from receiver\'s servers + + Your wallet connect provider returned the following error: %1$s + + Could not connect to Tor + Download relay document unavailable + Could not assemble LNUrl from Lightning Address \"%1$s\". Check the user\'s setup + The receiver\'s lightning service at %1$s is not available. It was calculated from the lightning address \"%2$s\". Error: %3$s. Check if the server is up and if the lightning address is correct + Could not resolve %1$s. Check if you are connected, if the server is up and if the lightning address %2$s is correct + Could not fetch invoice from %1$s + Error Parsing JSON from Lightning Address. Check the user\'s lightning setup + Callback URL not found in the User\'s lightning address server configuration + Error Parsing JSON from Lightning Address\'s invoice fetch. Check the user\'s lightning setup + Incorrect invoice amount (%1$s sats) from %2$s. It should have been %3$s + Unable to create a lightning invoice before sending the zap. The receiver\'s lightning wallet sent the following error: %1$s + Unable to create a lightning invoice before sending the zap. Element pr not found in the resulting JSON. + Read-only user + No reactions setup