Moves many Toasts to better designed Information Dialogs.

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