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

View File

@ -895,7 +895,7 @@ class Account(
return returningContactList
}
fun follow(user: User) {
suspend fun follow(user: User) {
if (!isWriteable() && !loginWithExternalSigner) return
val contactList = migrateCommunitiesAndChannelsIfNeeded(userProfile().latestContactList)
@ -1064,7 +1064,7 @@ class Account(
LocalCache.consume(event)
}
fun unfollow(user: User) {
suspend fun unfollow(user: User) {
if (!isWriteable() && !loginWithExternalSigner) return
val contactList = migrateCommunitiesAndChannelsIfNeeded(userProfile().latestContactList)

View File

@ -1,8 +1,10 @@
package com.vitorpamplona.amethyst.service
import android.content.Context
import androidx.compose.runtime.Immutable
import com.fasterxml.jackson.databind.JsonNode
import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
import com.vitorpamplona.amethyst.R
import com.vitorpamplona.amethyst.service.lnurl.LightningAddressResolver
import com.vitorpamplona.amethyst.ui.components.GenericLoadable
import com.vitorpamplona.quartz.events.Event
@ -45,7 +47,7 @@ class CashuProcessor {
}
}
suspend fun melt(token: CashuToken, lud16: String, onSuccess: (String) -> Unit, onError: (String) -> Unit) {
suspend fun melt(token: CashuToken, lud16: String, onSuccess: (String, String) -> Unit, onError: (String, String) -> Unit, context: Context) {
checkNotInMainThread()
runCatching {
@ -54,16 +56,17 @@ class CashuProcessor {
milliSats = token.redeemInvoiceAmount * 1000, // Make invoice and leave room for fees
message = "Redeem Cashu",
onSuccess = { invoice ->
meltInvoice(token, invoice, onSuccess, onError)
meltInvoice(token, invoice, onSuccess, onError, context)
},
onProgress = {
},
onError = onError
onError = onError,
context = context
)
}
}
private fun meltInvoice(token: CashuToken, invoice: String, onSuccess: (String) -> Unit, onError: (String) -> Unit) {
private fun meltInvoice(token: CashuToken, invoice: String, onSuccess: (String, String) -> Unit, onError: (String, String) -> Unit, context: Context) {
try {
val client = HttpClient.getHttpClient()
val url = token.mint + "/melt" // Melt cashu tokens at Mint
@ -88,13 +91,24 @@ class CashuProcessor {
val successful = tree?.get("paid")?.asText() == "true"
if (successful) {
onSuccess("Redeemed ${token.totalAmount} Sats" + " (Fees: ${token.fees} Sats)")
onSuccess(
context.getString(R.string.cashu_sucessful_redemption),
context.getString(R.string.cashu_sucessful_redemption_explainer, token.totalAmount.toString(), token.fees.toString())
)
} else {
onError(tree?.get("detail")?.asText()?.split('.')?.getOrNull(0) ?: "Cashu: Tokens already spent.")
val msg = tree?.get("detail")?.asText()?.split('.')?.getOrNull(0)?.ifBlank { null }
onError(
context.getString(R.string.cashu_failed_redemption),
if (msg != null) {
context.getString(R.string.cashu_failed_redemption_explainer_error_msg, msg)
} else {
context.getString(R.string.cashu_failed_redemption_explainer_error_msg)
}
)
}
}
} catch (e: Exception) {
onError("Token melt failure: " + e.message)
onError(context.getString(R.string.cashu_sucessful_redemption), context.getString(R.string.cashu_failed_redemption_explainer_error_msg, e.message))
}
}
}

View File

@ -35,7 +35,7 @@ class ZapPaymentHandler(val account: Account) {
pollOption: Int?,
message: String,
context: Context,
onError: (String) -> Unit,
onError: (String, String) -> Unit,
onProgress: (percent: Float) -> Unit,
onPayViaIntent: (ImmutableList<Payable>) -> Unit,
zapType: LnZapEvent.ZapType
@ -48,7 +48,10 @@ class ZapPaymentHandler(val account: Account) {
val lud16 = note.author?.info?.lud16?.trim() ?: note.author?.info?.lud06?.trim()
if (lud16.isNullOrBlank()) {
onError(context.getString(R.string.user_does_not_have_a_lightning_address_setup_to_receive_sats))
onError(
context.getString(R.string.missing_lud16),
context.getString(R.string.user_does_not_have_a_lightning_address_setup_to_receive_sats)
)
return@withContext
}
@ -121,6 +124,9 @@ class ZapPaymentHandler(val account: Account) {
)
} else {
onError(
context.getString(
R.string.missing_lud16
),
context.getString(
R.string.user_x_does_not_have_a_lightning_address_setup_to_receive_sats,
user?.toBestDisplayName() ?: value.lnAddressOrPubKeyHex
@ -149,7 +155,7 @@ class ZapPaymentHandler(val account: Account) {
pollOption: Int?,
message: String,
context: Context,
onError: (String) -> Unit,
onError: (String, String) -> Unit,
onProgress: (percent: Float) -> Unit,
onPayInvoiceThroughIntent: (String) -> Unit,
zapType: LnZapEvent.ZapType,
@ -181,10 +187,14 @@ class ZapPaymentHandler(val account: Account) {
if (response is PayInvoiceErrorResponse) {
onProgress(0.0f)
onError(
context.getString(R.string.error_dialog_pay_invoice_error),
context.getString(
R.string.wallet_connect_pay_invoice_error_error,
response.error?.message
?: response.error?.code?.toString()
?: "Error parsing error message"
)
)
} else {
onProgress(1f)
}
@ -192,16 +202,13 @@ class ZapPaymentHandler(val account: Account) {
)
onProgress(0.8f)
} else {
try {
onPayInvoiceThroughIntent(it)
} catch (e: Exception) {
onError(context.getString(R.string.lightning_wallets_not_found2))
}
onProgress(0f)
}
},
onError = onError,
onProgress = onProgress
onProgress = onProgress,
context = context
)
}
}

View File

@ -1,7 +1,9 @@
package com.vitorpamplona.amethyst.service.lnurl
import android.content.Context
import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
import com.vitorpamplona.amethyst.BuildConfig
import com.vitorpamplona.amethyst.R
import com.vitorpamplona.amethyst.service.HttpClient
import com.vitorpamplona.amethyst.service.checkNotInMainThread
import com.vitorpamplona.quartz.encoders.LnInvoiceUtil
@ -31,13 +33,24 @@ class LightningAddressResolver() {
return null
}
private suspend fun fetchLightningAddressJson(lnaddress: String, onSuccess: suspend (String) -> Unit, onError: (String) -> Unit) = withContext(Dispatchers.IO) {
private suspend fun fetchLightningAddressJson(
lnaddress: String,
onSuccess: suspend (String) -> Unit,
onError: (String, String) -> Unit,
context: Context
) = withContext(Dispatchers.IO) {
checkNotInMainThread()
val url = assembleUrl(lnaddress)
if (url == null) {
onError("Could not assemble LNUrl from Lightning Address \"${lnaddress}\". Check the user's setup")
onError(
context.getString(R.string.error_unable_to_fetch_invoice),
context.getString(
R.string.could_not_assemble_lnurl_from_lightning_address_check_the_user_s_setup,
lnaddress
)
)
return@withContext
}
@ -51,16 +64,39 @@ class LightningAddressResolver() {
if (it.isSuccessful) {
onSuccess(it.body.string())
} else {
onError("The receiver's lightning service at $url is not available. It was calculated from the lightning address \"${lnaddress}\". Error: ${it.code}. Check if the server is up and if the lightning address is correct")
onError(
context.getString(R.string.error_unable_to_fetch_invoice),
context.getString(
R.string.the_receiver_s_lightning_service_at_is_not_available_it_was_calculated_from_the_lightning_address_error_check_if_the_server_is_up_and_if_the_lightning_address_is_correct,
url,
lnaddress,
it.code.toString()
)
)
}
}
} catch (e: Exception) {
e.printStackTrace()
onError("Could not resolve $url. Check if you are connected, if the server is up and if the lightning address $lnaddress is correct")
onError(
context.getString(R.string.error_unable_to_fetch_invoice),
context.getString(
R.string.could_not_resolve_check_if_you_are_connected_if_the_server_is_up_and_if_the_lightning_address_is_correct,
url,
lnaddress
)
)
}
}
suspend fun fetchLightningInvoice(lnCallback: String, milliSats: Long, message: String, nostrRequest: String? = null, onSuccess: suspend (String) -> Unit, onError: (String) -> Unit) = withContext(Dispatchers.IO) {
suspend fun fetchLightningInvoice(
lnCallback: String,
milliSats: Long,
message: String,
nostrRequest: String? = null,
onSuccess: suspend (String) -> Unit,
onError: (String, String) -> Unit,
context: Context
) = withContext(Dispatchers.IO) {
val encodedMessage = URLEncoder.encode(message, "utf-8")
val urlBinder = if (lnCallback.contains("?")) "&" else "?"
@ -80,18 +116,22 @@ class LightningAddressResolver() {
if (it.isSuccessful) {
onSuccess(it.body.string())
} else {
onError("Could not fetch invoice from $lnCallback")
onError(
context.getString(R.string.error_unable_to_fetch_invoice),
context.getString(R.string.could_not_fetch_invoice_from, lnCallback)
)
}
}
}
suspend fun lnAddressToLnUrl(lnaddress: String, onSuccess: (String) -> Unit, onError: (String) -> Unit) {
suspend fun lnAddressToLnUrl(lnaddress: String, onSuccess: (String) -> Unit, onError: (String, String) -> Unit, context: Context) {
fetchLightningAddressJson(
lnaddress,
onSuccess = {
onSuccess(it.toByteArray().toLnUrl())
},
onError = onError
onError = onError,
context = context
)
}
@ -101,8 +141,9 @@ class LightningAddressResolver() {
message: String,
nostrRequest: String? = null,
onSuccess: suspend (String) -> Unit,
onError: (String) -> Unit,
onProgress: (percent: Float) -> Unit
onError: (String, String) -> Unit,
onProgress: (percent: Float) -> Unit,
context: Context
) {
val mapper = jacksonObjectMapper()
@ -114,14 +155,20 @@ class LightningAddressResolver() {
val lnurlp = try {
mapper.readTree(lnAddressJson)
} catch (t: Throwable) {
onError("Error Parsing JSON from Lightning Address. Check the user's lightning setup")
onError(
context.getString(R.string.error_unable_to_fetch_invoice),
context.getString(R.string.error_parsing_json_from_lightning_address_check_the_user_s_lightning_setup)
)
null
}
val callback = lnurlp?.get("callback")?.asText()
if (callback == null) {
onError("Callback URL not found in the User's lightning address server configuration")
onError(
context.getString(R.string.error_unable_to_fetch_invoice),
context.getString(R.string.callback_url_not_found_in_the_user_s_lightning_address_server_configuration)
)
}
val allowsNostr = lnurlp?.get("allowsNostr")?.asBoolean() ?: false
@ -138,7 +185,10 @@ class LightningAddressResolver() {
val lnInvoice = try {
mapper.readTree(it)
} catch (t: Throwable) {
onError("Error Parsing JSON from Lightning Address's invoice fetch. Check the user's lightning setup")
onError(
context.getString(R.string.error_unable_to_fetch_invoice),
context.getString(R.string.error_parsing_json_from_lightning_address_s_invoice_fetch_check_the_user_s_lightning_setup)
)
null
}
@ -151,21 +201,40 @@ class LightningAddressResolver() {
onSuccess(pr)
} else {
onProgress(0.0f)
onError("Incorrect invoice amount (${invoiceAmount.toLong()} sats) from $lnaddress. It should have been $expectedAmountInSats")
onError(
context.getString(R.string.error_unable_to_fetch_invoice),
context.getString(
R.string.incorrect_invoice_amount_sats_from_it_should_have_been,
invoiceAmount.toLong().toString(),
lnaddress,
expectedAmountInSats.toString()
)
)
}
} ?: lnInvoice?.get("reason")?.asText()?.ifBlank { null }?.let { reason ->
onProgress(0.0f)
onError("Unable to create a lightning invoice before sending the zap. The receiver's lightning wallet sent the following error: $reason")
onError(
context.getString(R.string.error_unable_to_fetch_invoice),
context.getString(
R.string.unable_to_create_a_lightning_invoice_before_sending_the_zap_the_receiver_s_lightning_wallet_sent_the_following_error,
reason
)
)
} ?: run {
onProgress(0.0f)
onError("nable to create a lightning invoice before sending the zap. Element pr not found in the resulting JSON.")
}
},
onError = onError
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,
context
)
}
}

View File

@ -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))
}
}
}
)
}

View File

@ -380,6 +380,9 @@ fun NewPostView(
},
onClose = {
postViewModel.wantsInvoice = false
},
onError = { title, message ->
accountViewModel.toast(title, message)
}
)
}

View File

@ -350,15 +350,10 @@ fun ServerConfig(
Nip11Retriever.ErrorCode.FAIL_WITH_HTTP_STATUS -> context.getString(R.string.relay_information_document_error_assemble_url, url, exceptionMessage)
}
scope.launch {
Toast
.makeText(
context,
msg,
Toast.LENGTH_SHORT
accountViewModel.toast(
context.getString(R.string.unable_to_download_relay_document),
msg
)
.show()
}
}
)
}

View File

@ -1,6 +1,5 @@
package com.vitorpamplona.amethyst.ui.actions
import android.widget.Toast
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.combinedClickable
import androidx.compose.foundation.layout.Arrangement
@ -16,15 +15,14 @@ import androidx.compose.material3.Surface
import androidx.compose.material3.Switch
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import androidx.compose.ui.window.Dialog
import androidx.compose.ui.window.DialogProperties
@ -34,7 +32,6 @@ import com.vitorpamplona.amethyst.model.RelayInformation
import com.vitorpamplona.amethyst.service.Nip11Retriever
import com.vitorpamplona.amethyst.service.relays.Relay
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
import kotlinx.coroutines.launch
data class RelayList(
val relay: Relay,
@ -55,7 +52,6 @@ fun RelaySelectionDialog(
accountViewModel: AccountViewModel,
nav: (String) -> Unit
) {
val scope = rememberCoroutineScope()
val context = LocalContext.current
var relays by remember {
@ -69,6 +65,13 @@ fun RelaySelectionDialog(
}
)
}
val hasSelectedRelay by remember {
derivedStateOf {
relays.any { it.isSelected }
}
}
var relayInfo: RelayInfoDialog? by remember { mutableStateOf(null) }
relayInfo?.let {
@ -121,21 +124,15 @@ fun RelaySelectionDialog(
SaveButton(
onPost = {
val selectedRelays = relays.filter { it.isSelected }
if (selectedRelays.isEmpty()) {
scope.launch {
Toast.makeText(context, context.getString(R.string.select_a_relay_to_continue), Toast.LENGTH_SHORT).show()
}
return@SaveButton
}
onPost(selectedRelays.map { it.relay })
onClose()
},
isActive = true
isActive = hasSelectedRelay
)
}
RelaySwitch(
text = stringResource(R.string.select_deselect_all),
text = context.getString(R.string.select_deselect_all),
checked = selected,
onClick = {
selected = !selected
@ -181,15 +178,10 @@ fun RelaySelectionDialog(
Nip11Retriever.ErrorCode.FAIL_WITH_HTTP_STATUS -> context.getString(R.string.relay_information_document_error_assemble_url, url, exceptionMessage)
}
scope.launch {
Toast
.makeText(
context,
msg,
Toast.LENGTH_SHORT
accountViewModel.toast(
context.getString(R.string.unable_to_download_relay_document),
msg
)
.show()
}
}
)
}

View File

@ -2,7 +2,6 @@ package com.vitorpamplona.amethyst.ui.components
import android.content.Intent
import android.net.Uri
import android.widget.Toast
import androidx.compose.animation.Crossfade
import androidx.compose.foundation.border
import androidx.compose.foundation.layout.Column
@ -133,26 +132,20 @@ fun CashuPreview(token: CashuToken, accountViewModel: AccountViewModel) {
CashuProcessor().melt(
token,
lud16,
onSuccess = {
scope.launch {
Toast.makeText(context, it, Toast.LENGTH_SHORT).show()
}
onSuccess = { title, message ->
accountViewModel.toast(title, message)
},
onError = {
scope.launch {
Toast.makeText(context, it, Toast.LENGTH_SHORT).show()
}
}
onError = { title, message ->
accountViewModel.toast(title, message)
},
context
)
}
} else {
scope.launch {
Toast.makeText(
context,
accountViewModel.toast(
context.getString(R.string.no_lightning_address_set),
Toast.LENGTH_SHORT
).show()
}
context.getString(R.string.user_x_does_not_have_a_lightning_address_setup_to_receive_sats, accountViewModel.account.userProfile().toBestDisplayName())
)
}
},
shape = QuoteBorder,
@ -177,11 +170,7 @@ fun CashuPreview(token: CashuToken, accountViewModel: AccountViewModel) {
startActivity(context, intent, null)
} else {
// Copying the token to clipboard for now
var orignaltoken = token.token
clipboardManager.setText(AnnotatedString("$orignaltoken"))
scope.launch {
Toast.makeText(context, context.getString(R.string.copied_token_to_clipboard), Toast.LENGTH_SHORT).show()
}
clipboardManager.setText(AnnotatedString(token.token))
}
},
shape = QuoteBorder,

View File

@ -1,8 +1,5 @@
package com.vitorpamplona.amethyst.ui.components
import android.content.Intent
import android.net.Uri
import android.widget.Toast
import androidx.compose.animation.Crossfade
import androidx.compose.foundation.text.ClickableText
import androidx.compose.material3.LocalTextStyle
@ -12,8 +9,9 @@ import androidx.compose.runtime.*
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.text.AnnotatedString
import androidx.compose.ui.text.style.TextDirection
import androidx.core.content.ContextCompat
import com.vitorpamplona.amethyst.R
import com.vitorpamplona.amethyst.ui.note.ErrorMessageDialog
import com.vitorpamplona.amethyst.ui.note.payViaIntent
import com.vitorpamplona.quartz.encoders.LnWithdrawalUtil
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
@ -43,27 +41,26 @@ fun MayBeWithdrawal(lnurlWord: String) {
@Composable
fun ClickableWithdrawal(withdrawalString: String) {
val context = LocalContext.current
val scope = rememberCoroutineScope()
val withdraw = remember(withdrawalString) {
AnnotatedString("$withdrawalString ")
}
var showErrorMessageDialog by remember { mutableStateOf<String?>(null) }
if (showErrorMessageDialog != null) {
ErrorMessageDialog(
title = context.getString(R.string.error_dialog_pay_withdraw_error),
textContent = showErrorMessageDialog ?: "",
onDismiss = { showErrorMessageDialog = null }
)
}
ClickableText(
text = withdraw,
onClick = {
try {
val intent = Intent(Intent.ACTION_VIEW, Uri.parse("lightning:$withdrawalString"))
intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK
ContextCompat.startActivity(context, intent, null)
} catch (e: Exception) {
scope.launch {
Toast.makeText(
context,
context.getString(R.string.lightning_wallets_not_found),
Toast.LENGTH_LONG
).show()
}
payViaIntent(withdrawalString, context) {
showErrorMessageDialog = it
}
},
style = LocalTextStyle.current.copy(color = MaterialTheme.colorScheme.primary)

View File

@ -1,8 +1,5 @@
package com.vitorpamplona.amethyst.ui.components
import android.content.Intent
import android.net.Uri
import android.widget.Toast
import androidx.compose.animation.Crossfade
import androidx.compose.foundation.border
import androidx.compose.foundation.layout.Column
@ -23,8 +20,9 @@ import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextDirection
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.core.content.ContextCompat.startActivity
import com.vitorpamplona.amethyst.R
import com.vitorpamplona.amethyst.ui.note.ErrorMessageDialog
import com.vitorpamplona.amethyst.ui.note.payViaIntent
import com.vitorpamplona.amethyst.ui.theme.QuoteBorder
import com.vitorpamplona.amethyst.ui.theme.subtleBorder
import com.vitorpamplona.quartz.encoders.LnInvoiceUtil
@ -67,7 +65,16 @@ fun MayBeInvoicePreview(lnbcWord: String) {
@Composable
fun InvoicePreview(lnInvoice: String, amount: String?) {
val context = LocalContext.current
val scope = rememberCoroutineScope()
var showErrorMessageDialog by remember { mutableStateOf<String?>(null) }
if (showErrorMessageDialog != null) {
ErrorMessageDialog(
title = context.getString(R.string.error_dialog_pay_invoice_error),
textContent = showErrorMessageDialog ?: "",
onDismiss = { showErrorMessageDialog = null }
)
}
Column(
modifier = Modifier
@ -120,18 +127,8 @@ fun InvoicePreview(lnInvoice: String, amount: String?) {
.fillMaxWidth()
.padding(vertical = 10.dp),
onClick = {
try {
val intent = Intent(Intent.ACTION_VIEW, Uri.parse("lightning:$lnInvoice"))
intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK
startActivity(context, intent, null)
} catch (e: Exception) {
scope.launch {
Toast.makeText(
context,
context.getString(R.string.lightning_wallets_not_found),
Toast.LENGTH_LONG
).show()
}
payViaIntent(lnInvoice, context) {
showErrorMessageDialog = it
}
},
shape = QuoteBorder,

View File

@ -1,6 +1,5 @@
package com.vitorpamplona.amethyst.ui.components
import android.widget.Toast
import androidx.compose.foundation.border
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
@ -50,7 +49,8 @@ fun InvoiceRequestCard(
titleText: String? = null,
buttonText: String? = null,
onSuccess: (String) -> Unit,
onClose: () -> Unit
onClose: () -> Unit,
onError: (String, String) -> Unit
) {
Column(
modifier = Modifier
@ -64,7 +64,7 @@ fun InvoiceRequestCard(
.fillMaxWidth()
.padding(30.dp)
) {
InvoiceRequest(lud16, toUserPubKeyHex, account, titleText, buttonText, onSuccess, onClose)
InvoiceRequest(lud16, toUserPubKeyHex, account, titleText, buttonText, onSuccess, onClose, onError)
}
}
}
@ -77,7 +77,8 @@ fun InvoiceRequest(
titleText: String? = null,
buttonText: String? = null,
onSuccess: (String) -> Unit,
onClose: () -> Unit
onClose: () -> Unit,
onError: (String, String) -> Unit
) {
val context = LocalContext.current
val scope = rememberCoroutineScope()
@ -162,14 +163,10 @@ fun InvoiceRequest(
message,
zapRequest?.toJson(),
onSuccess = onSuccess,
onError = {
scope.launch {
Toast.makeText(context, it, Toast.LENGTH_SHORT).show()
onClose()
}
},
onError = onError,
onProgress = {
}
},
context = context
)
}
},

View File

@ -6,7 +6,6 @@ import android.content.ContextWrapper
import android.os.Build
import android.util.Log
import android.view.Window
import android.widget.Toast
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.clickable
import androidx.compose.foundation.combinedClickable
@ -49,7 +48,6 @@ import androidx.compose.runtime.MutableState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
@ -78,6 +76,7 @@ import com.vitorpamplona.amethyst.model.ConnectivityType
import com.vitorpamplona.amethyst.service.BlurHashRequester
import com.vitorpamplona.amethyst.service.connectivitystatus.ConnectivityStatus
import com.vitorpamplona.amethyst.ui.actions.CloseButton
import com.vitorpamplona.amethyst.ui.actions.InformationDialog
import com.vitorpamplona.amethyst.ui.actions.LoadingAnimation
import com.vitorpamplona.amethyst.ui.actions.SaveToGallery
import com.vitorpamplona.amethyst.ui.note.BlankNote
@ -808,7 +807,17 @@ private fun verifyHash(content: ZoomableUrlContent, context: Context): Boolean?
@Composable
private fun HashVerificationSymbol(verifiedHash: Boolean, modifier: Modifier) {
val localContext = LocalContext.current
val scope = rememberCoroutineScope()
val openDialogMsg = remember { mutableStateOf<String?>(null) }
openDialogMsg.value?.let {
InformationDialog(
title = localContext.getString(R.string.hash_verification_info_title),
textContent = it
) {
openDialogMsg.value = null
}
}
Box(
modifier
@ -819,13 +828,7 @@ private fun HashVerificationSymbol(verifiedHash: Boolean, modifier: Modifier) {
if (verifiedHash) {
IconButton(
onClick = {
scope.launch {
Toast.makeText(
localContext,
localContext.getString(R.string.hash_verification_passed),
Toast.LENGTH_LONG
).show()
}
openDialogMsg.value = localContext.getString(R.string.hash_verification_passed)
}
) {
HashCheckIcon(Size30dp)
@ -833,13 +836,7 @@ private fun HashVerificationSymbol(verifiedHash: Boolean, modifier: Modifier) {
} else {
IconButton(
onClick = {
scope.launch {
Toast.makeText(
localContext,
localContext.getString(R.string.hash_verification_failed),
Toast.LENGTH_LONG
).show()
}
openDialogMsg.value = localContext.getString(R.string.hash_verification_failed)
}
) {
HashCheckFailedIcon(Size30dp)

View File

@ -566,7 +566,7 @@ fun ListContent(
NewRelayListView({ wantsToEditRelays = false }, accountViewModel, nav = nav)
}
if (backupDialogOpen) {
AccountBackupDialog(accountViewModel.account, onClose = { backupDialogOpen = false })
AccountBackupDialog(accountViewModel, onClose = { backupDialogOpen = false })
}
if (conectOrbotDialogOpen) {
ConnectOrbotDialog(
@ -577,6 +577,12 @@ fun ListContent(
checked = true
enableTor(accountViewModel.account, true, proxyPort, context, coroutineScope)
},
onError = {
accountViewModel.toast(
context.getString(R.string.could_not_connect_to_tor),
it
)
},
proxyPort
)
}

View File

@ -88,7 +88,6 @@ import com.vitorpamplona.amethyst.model.ConnectivityType
import com.vitorpamplona.amethyst.model.Note
import com.vitorpamplona.amethyst.model.RelayBriefInfo
import com.vitorpamplona.amethyst.model.User
import com.vitorpamplona.amethyst.service.OnlineChecker
import com.vitorpamplona.amethyst.service.ReverseGeoLocationUtil
import com.vitorpamplona.amethyst.service.connectivitystatus.ConnectivityStatus
import com.vitorpamplona.amethyst.ui.actions.NewRelayListView
@ -162,6 +161,7 @@ import com.vitorpamplona.amethyst.ui.theme.replyBackground
import com.vitorpamplona.amethyst.ui.theme.replyModifier
import com.vitorpamplona.amethyst.ui.theme.subtleBorder
import com.vitorpamplona.quartz.encoders.ATag
import com.vitorpamplona.quartz.encoders.HexKey
import com.vitorpamplona.quartz.encoders.toNpub
import com.vitorpamplona.quartz.events.AppDefinitionEvent
import com.vitorpamplona.quartz.events.AudioHeaderEvent
@ -1286,8 +1286,8 @@ fun routeFor(note: Note, loggedIn: User): String? {
return null
}
fun routeToMessage(user: User, draftMessage: String?, accountViewModel: AccountViewModel): String {
val withKey = ChatroomKey(persistentSetOf(user.pubkeyHex))
fun routeToMessage(user: HexKey, draftMessage: String?, accountViewModel: AccountViewModel): String {
val withKey = ChatroomKey(persistentSetOf(user))
accountViewModel.account.userProfile().createChatroom(withKey)
return if (draftMessage != null) {
"Room/${withKey.hashCode()}?message=$draftMessage"
@ -1296,6 +1296,10 @@ fun routeToMessage(user: User, draftMessage: String?, accountViewModel: AccountV
}
}
fun routeToMessage(user: User, draftMessage: String?, accountViewModel: AccountViewModel): String {
return routeToMessage(user.pubkeyHex, draftMessage, accountViewModel)
}
fun routeFor(note: Channel): String {
return "Channel/${note.idHex}"
}

View File

@ -291,22 +291,18 @@ private fun RenderMainPopup(
Icons.Default.PersonRemove,
stringResource(R.string.quick_action_unfollow)
) {
scope.launch(Dispatchers.IO) {
accountViewModel.unfollow(note.author!!)
onDismiss()
}
}
} else {
NoteQuickActionItem(
Icons.Default.PersonAdd,
stringResource(R.string.quick_action_follow)
) {
scope.launch(Dispatchers.IO) {
accountViewModel.follow(note.author!!)
onDismiss()
}
}
}
VerticalDivider(primaryLight)
NoteQuickActionItem(

View File

@ -1,11 +1,12 @@
package com.vitorpamplona.amethyst.ui.note
import android.widget.Toast
import androidx.compose.foundation.*
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.*
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Bolt
import androidx.compose.material.icons.outlined.Bolt
import androidx.compose.material.ripple.rememberRipple
import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.runtime.livedata.observeAsState
@ -27,6 +28,7 @@ import com.vitorpamplona.amethyst.model.Note
import com.vitorpamplona.amethyst.service.ZapPaymentHandler
import com.vitorpamplona.amethyst.ui.components.TranslatableRichTextViewer
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
import com.vitorpamplona.amethyst.ui.screen.loggedIn.StringToastMsg
import com.vitorpamplona.amethyst.ui.theme.BitcoinOrange
import com.vitorpamplona.amethyst.ui.theme.ButtonBorder
import com.vitorpamplona.amethyst.ui.theme.Font14SP
@ -294,7 +296,7 @@ fun ZapVote(
}
var zappingProgress by remember { mutableStateOf(0f) }
var showErrorMessageDialog by remember { mutableStateOf<String?>(null) }
var showErrorMessageDialog by remember { mutableStateOf<StringToastMsg?>(null) }
val context = LocalContext.current
val scope = rememberCoroutineScope()
@ -305,50 +307,30 @@ fun ZapVote(
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier.combinedClickable(
role = Role.Button,
// interactionSource = remember { MutableInteractionSource() },
// indication = rememberRipple(bounded = false, radius = 24.dp),
interactionSource = remember { MutableInteractionSource() },
indication = rememberRipple(bounded = false, radius = 24.dp),
onClick = {
if (!accountViewModel.isWriteable() && !accountViewModel.loggedInWithExternalSigner()) {
scope.launch {
Toast
.makeText(
context,
context.getString(R.string.login_with_a_private_key_to_be_able_to_send_zaps),
Toast.LENGTH_SHORT
accountViewModel.toast(
R.string.read_only_user,
R.string.login_with_a_private_key_to_be_able_to_send_zaps
)
.show()
}
} else if (pollViewModel.isPollClosed()) {
scope.launch {
Toast
.makeText(
context,
context.getString(R.string.poll_is_closed),
Toast.LENGTH_SHORT
accountViewModel.toast(
R.string.poll_unable_to_vote,
R.string.poll_is_closed_explainer
)
.show()
}
} else if (isLoggedUser) {
scope.launch {
Toast
.makeText(
context,
context.getString(R.string.poll_author_no_vote),
Toast.LENGTH_SHORT
accountViewModel.toast(
R.string.poll_unable_to_vote,
R.string.poll_author_no_vote
)
.show()
}
} else if (pollViewModel.isVoteAmountAtomic() && poolOption.zappedByLoggedIn) {
// only allow one vote per option when min==max, i.e. atomic vote amount specified
scope.launch {
Toast
.makeText(
context,
R.string.one_vote_per_user_on_atomic_votes,
Toast.LENGTH_SHORT
accountViewModel.toast(
R.string.poll_unable_to_vote,
R.string.one_vote_per_user_on_atomic_votes
)
.show()
}
return@combinedClickable
} else if (accountViewModel.account.zapAmountChoices.size == 1 &&
pollViewModel.isValidInputVoteAmount(accountViewModel.account.zapAmountChoices.first())
@ -359,13 +341,9 @@ fun ZapVote(
poolOption.option,
"",
context,
onError = {
scope.launch {
onError = { title, message ->
zappingProgress = 0f
Toast
.makeText(context, it, Toast.LENGTH_SHORT)
.show()
}
showErrorMessageDialog = StringToastMsg(title, message)
},
onProgress = {
scope.launch(Dispatchers.Main) {
@ -395,11 +373,9 @@ fun ZapVote(
onChangeAmount = {
wantsToZap = false
},
onError = {
scope.launch {
onError = { title, message ->
showErrorMessageDialog = StringToastMsg(title, message)
zappingProgress = 0f
showErrorMessageDialog = it
}
},
onProgress = {
scope.launch(Dispatchers.Main) {
@ -423,19 +399,22 @@ fun ZapVote(
wantsToPay = persistentListOf()
scope.launch {
zappingProgress = 0f
showErrorMessageDialog = it
showErrorMessageDialog = StringToastMsg(
context.getString(R.string.error_dialog_zap_error),
it
)
}
}
)
}
if (showErrorMessageDialog != null) {
showErrorMessageDialog?.let { toast ->
ErrorMessageDialog(
title = stringResource(id = R.string.error_dialog_zap_error),
textContent = showErrorMessageDialog ?: "",
title = toast.title,
textContent = toast.msg,
onClickStartMessage = {
baseNote.author?.let {
nav(routeToMessage(it, showErrorMessageDialog, accountViewModel))
nav(routeToMessage(it, toast.msg, accountViewModel))
}
},
onDismiss = { showErrorMessageDialog = null }
@ -494,7 +473,7 @@ fun FilteredZapAmountChoicePopup(
pollOption: Int,
onDismiss: () -> Unit,
onChangeAmount: () -> Unit,
onError: (text: String) -> Unit,
onError: (title: String, text: String) -> Unit,
onProgress: (percent: Float) -> Unit,
onPayViaIntent: (ImmutableList<ZapPaymentHandler.Payable>) -> Unit
) {

View File

@ -2,7 +2,6 @@ package com.vitorpamplona.amethyst.ui.note
import android.content.Context
import android.util.Log
import android.widget.Toast
import androidx.compose.animation.AnimatedContent
import androidx.compose.animation.AnimatedContentTransitionScope
import androidx.compose.animation.ContentTransform
@ -110,7 +109,6 @@ import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.persistentListOf
import kotlinx.collections.immutable.toImmutableList
import kotlinx.collections.immutable.toImmutableMap
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import java.math.BigDecimal
@ -540,9 +538,6 @@ fun ReplyReaction(
iconSize: Dp = Size17dp,
onPress: () -> Unit
) {
val context = LocalContext.current
val scope = rememberCoroutineScope()
IconButton(
modifier = remember {
Modifier.size(iconSize)
@ -554,13 +549,10 @@ fun ReplyReaction(
if (accountViewModel.loggedInWithExternalSigner()) {
onPress()
} else {
scope.launch {
Toast.makeText(
context,
context.getString(R.string.login_with_a_private_key_to_be_able_to_reply),
Toast.LENGTH_SHORT
).show()
}
accountViewModel.toast(
R.string.read_only_user,
R.string.login_with_a_private_key_to_be_able_to_reply
)
}
}
}
@ -643,9 +635,6 @@ fun BoostReaction(
iconSize: Dp = 20.dp,
onQuotePress: () -> Unit
) {
val context = LocalContext.current
val scope = rememberCoroutineScope()
var wantsToBoost by remember { mutableStateOf(false) }
val iconButtonModifier = remember {
@ -657,29 +646,22 @@ fun BoostReaction(
onClick = {
if (accountViewModel.isWriteable()) {
if (accountViewModel.hasBoosted(baseNote)) {
scope.launch(Dispatchers.IO) {
accountViewModel.deleteBoostsTo(baseNote)
}
} else {
wantsToBoost = true
}
} else {
if (accountViewModel.loggedInWithExternalSigner()) {
if (accountViewModel.hasBoosted(baseNote)) {
scope.launch(Dispatchers.IO) {
accountViewModel.deleteBoostsTo(baseNote)
}
} else {
wantsToBoost = true
}
} else {
scope.launch {
Toast.makeText(
context,
context.getString(R.string.login_with_a_private_key_to_be_able_to_boost_posts),
Toast.LENGTH_SHORT
).show()
}
accountViewModel.toast(
R.string.read_only_user,
R.string.login_with_a_private_key_to_be_able_to_boost_posts
)
}
}
}
@ -699,10 +681,8 @@ fun BoostReaction(
onQuotePress()
},
onRepost = {
scope.launch(Dispatchers.IO) {
accountViewModel.boost(baseNote)
}
}
)
}
}
@ -741,9 +721,6 @@ fun LikeReaction(
heartSize: Dp = 16.dp,
iconFontSize: TextUnit = Font14SP
) {
val context = LocalContext.current
val scope = rememberCoroutineScope()
val iconButtonModifier = remember {
Modifier.size(iconSize)
}
@ -761,21 +738,12 @@ fun LikeReaction(
likeClick(
baseNote,
accountViewModel,
scope,
context,
onMultipleChoices = {
wantsToReact = true
},
onWantsToSignReaction = {
if (accountViewModel.account.reactionChoices.size == 1) {
scope.launch(Dispatchers.IO) {
val reaction = accountViewModel.account.reactionChoices.first()
if (accountViewModel.hasReactedTo(baseNote, reaction)) {
accountViewModel.deleteReactionTo(baseNote, reaction)
} else {
accountViewModel.reactTo(baseNote, reaction)
}
}
accountViewModel.reactToOrDelete(baseNote)
} else if (accountViewModel.account.reactionChoices.size > 1) {
wantsToReact = true
}
@ -896,44 +864,25 @@ fun LikeText(baseNote: Note, grayTint: Color) {
private fun likeClick(
baseNote: Note,
accountViewModel: AccountViewModel,
scope: CoroutineScope,
context: Context,
onMultipleChoices: () -> Unit,
onWantsToSignReaction: () -> Unit
) {
if (accountViewModel.account.reactionChoices.isEmpty()) {
scope.launch {
Toast
.makeText(
context,
context.getString(R.string.no_reaction_type_setup_long_press_to_change),
Toast.LENGTH_SHORT
accountViewModel.toast(
R.string.no_reactions_setup,
R.string.no_reaction_type_setup_long_press_to_change
)
.show()
}
} else if (!accountViewModel.isWriteable()) {
if (accountViewModel.loggedInWithExternalSigner()) {
onWantsToSignReaction()
} else {
scope.launch {
Toast
.makeText(
context,
context.getString(R.string.login_with_a_private_key_to_like_posts),
Toast.LENGTH_SHORT
accountViewModel.toast(
R.string.read_only_user,
R.string.login_with_a_private_key_to_like_posts
)
.show()
}
}
} else if (accountViewModel.account.reactionChoices.size == 1) {
scope.launch(Dispatchers.IO) {
val reaction = accountViewModel.account.reactionChoices.first()
if (accountViewModel.hasReactedTo(baseNote, reaction)) {
accountViewModel.deleteReactionTo(baseNote, reaction)
} else {
accountViewModel.reactTo(baseNote, reaction)
}
}
accountViewModel.reactToOrDelete(baseNote)
} else if (accountViewModel.account.reactionChoices.size > 1) {
onMultipleChoices()
}
@ -976,18 +925,19 @@ fun ZapReaction(
zapClick(
baseNote,
accountViewModel,
scope,
context,
onZappingProgress = { progress: Float ->
scope.launch {
zappingProgress = progress
}
},
onMultipleChoices = {
wantsToZap = true
},
onError = {
onError = { title, message ->
scope.launch {
zappingProgress = 0f
showErrorMessageDialog = it
showErrorMessageDialog = message
}
},
onPayViaIntent = {
@ -1015,10 +965,10 @@ fun ZapReaction(
wantsToZap = false
wantsToChangeZapAmount = true
},
onError = {
onError = { title, message ->
scope.launch {
zappingProgress = 0f
showErrorMessageDialog = it
showErrorMessageDialog = message
}
},
onProgress = {
@ -1075,10 +1025,10 @@ fun ZapReaction(
if (wantsToSetCustomZap) {
ZapCustomDialog(
onClose = { wantsToSetCustomZap = false },
onError = {
onError = { title, message ->
scope.launch {
zappingProgress = 0f
showErrorMessageDialog = it
showErrorMessageDialog = message
}
},
onProgress = {
@ -1121,33 +1071,22 @@ fun ZapReaction(
private fun zapClick(
baseNote: Note,
accountViewModel: AccountViewModel,
scope: CoroutineScope,
context: Context,
onZappingProgress: (Float) -> Unit,
onMultipleChoices: () -> Unit,
onError: (String) -> Unit,
onError: (String, String) -> Unit,
onPayViaIntent: (ImmutableList<ZapPaymentHandler.Payable>) -> Unit
) {
if (accountViewModel.account.zapAmountChoices.isEmpty()) {
scope.launch {
Toast
.makeText(
context,
context.getString(R.string.no_zap_amount_setup_long_press_to_change),
Toast.LENGTH_SHORT
accountViewModel.toast(
context.getString(R.string.error_dialog_zap_error),
context.getString(R.string.no_zap_amount_setup_long_press_to_change)
)
.show()
}
} else if (!accountViewModel.isWriteable() && !accountViewModel.loggedInWithExternalSigner()) {
scope.launch {
Toast
.makeText(
context,
context.getString(R.string.login_with_a_private_key_to_be_able_to_send_zaps),
Toast.LENGTH_SHORT
accountViewModel.toast(
context.getString(R.string.error_dialog_zap_error),
context.getString(R.string.login_with_a_private_key_to_be_able_to_send_zaps)
)
.show()
}
} else if (accountViewModel.account.zapAmountChoices.size == 1) {
accountViewModel.zap(
baseNote,
@ -1157,9 +1096,7 @@ private fun zapClick(
context,
onError = onError,
onProgress = {
scope.launch(Dispatchers.Main) {
onZappingProgress(it)
}
},
zapType = accountViewModel.account.defaultZapType,
onPayViaIntent = onPayViaIntent
@ -1289,15 +1226,12 @@ private fun BoostTypeChoicePopup(baseNote: Note, iconSize: Dp, accountViewModel:
onDismissRequest = { onDismiss() }
) {
FlowRow {
val scope = rememberCoroutineScope()
Button(
modifier = Modifier.padding(horizontal = 3.dp),
onClick = {
if (accountViewModel.isWriteable()) {
scope.launch(Dispatchers.IO) {
accountViewModel.boost(baseNote)
onDismiss()
}
} else {
onRepost()
onDismiss()
@ -1470,12 +1404,11 @@ fun ZapAmountChoicePopup(
accountViewModel: AccountViewModel,
onDismiss: () -> Unit,
onChangeAmount: () -> Unit,
onError: (text: String) -> Unit,
onError: (title: String, text: String) -> Unit,
onProgress: (percent: Float) -> Unit,
onPayViaIntent: (ImmutableList<ZapPaymentHandler.Payable>) -> Unit
) {
val context = LocalContext.current
val scope = rememberCoroutineScope()
val accountState by accountViewModel.accountLiveData.observeAsState()
val account = accountState?.account ?: return
val zapMessage = ""

View File

@ -1,6 +1,5 @@
package com.vitorpamplona.amethyst.ui.note
import android.widget.Toast
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.interaction.MutableInteractionSource
@ -49,7 +48,6 @@ import com.vitorpamplona.amethyst.ui.theme.Size15dp
import com.vitorpamplona.amethyst.ui.theme.StdStartPadding
import com.vitorpamplona.amethyst.ui.theme.placeholderText
import kotlinx.collections.immutable.toImmutableList
import kotlinx.coroutines.launch
@Composable
public fun RelayBadgesHorizontal(baseNote: Note, accountViewModel: AccountViewModel, nav: (String) -> Unit) {
@ -159,15 +157,10 @@ fun RenderRelay(relay: RelayBriefInfo, accountViewModel: AccountViewModel, nav:
Nip11Retriever.ErrorCode.FAIL_WITH_HTTP_STATUS -> context.getString(R.string.relay_information_document_error_assemble_url, url, exceptionMessage)
}
scope.launch {
Toast
.makeText(
context,
msg,
Toast.LENGTH_SHORT
accountViewModel.toast(
context.getString(R.string.unable_to_download_relay_document),
msg
)
.show()
}
}
)
}

View File

@ -5,7 +5,6 @@ import android.app.KeyguardManager
import android.content.Context
import android.content.Intent
import android.os.Build
import android.widget.Toast
import androidx.activity.compose.ManagedActivityResultLauncher
import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.result.ActivityResult
@ -82,7 +81,6 @@ import com.vitorpamplona.quartz.encoders.decodePublicKey
import com.vitorpamplona.quartz.encoders.toHexKey
import com.vitorpamplona.quartz.events.LnZapEvent
import kotlinx.collections.immutable.toImmutableList
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch
import androidx.compose.runtime.rememberCoroutineScope as rememberCoroutineScope
@ -228,9 +226,16 @@ fun UpdateZapAmountDialog(
try {
postViewModel.updateNIP47(nip47uri)
} catch (e: IllegalArgumentException) {
scope.launch {
Toast.makeText(context, e.message, Toast.LENGTH_SHORT)
.show()
if (e.message != null) {
accountViewModel.toast(
context.getString(R.string.error_parsing_nip47_title),
context.getString(R.string.error_parsing_nip47, nip47uri, e.message!!)
)
} else {
accountViewModel.toast(
context.getString(R.string.error_parsing_nip47_title),
context.getString(R.string.error_parsing_nip47_no_error, nip47uri)
)
}
}
}
@ -444,9 +449,16 @@ fun UpdateZapAmountDialog(
try {
postViewModel.updateNIP47(it)
} catch (e: IllegalArgumentException) {
scope.launch {
Toast.makeText(context, e.message, Toast.LENGTH_SHORT)
.show()
if (e.message != null) {
accountViewModel.toast(
context.getString(R.string.error_parsing_nip47_title),
context.getString(R.string.error_parsing_nip47, it, e.message!!)
)
} else {
accountViewModel.toast(
context.getString(R.string.error_parsing_nip47_title),
context.getString(R.string.error_parsing_nip47_no_error, it)
)
}
}
}
@ -505,7 +517,6 @@ fun UpdateZapAmountDialog(
mutableStateOf(false)
}
val scope = rememberCoroutineScope()
val context = LocalContext.current
val keyguardLauncher =
@ -544,13 +555,16 @@ fun UpdateZapAmountDialog(
IconButton(onClick = {
if (!showPassword) {
authenticate(
authTitle,
context,
scope,
keyguardLauncher
) {
title = authTitle,
context = context,
keyguardLauncher = keyguardLauncher,
onApproved = {
showPassword = true
},
onError = { title, message ->
accountViewModel.toast(title, message)
}
)
} else {
showPassword = false
}
@ -580,9 +594,9 @@ fun UpdateZapAmountDialog(
fun authenticate(
title: String,
context: Context,
scope: CoroutineScope,
keyguardLauncher: ManagedActivityResultLauncher<Intent, ActivityResult>,
onApproved: () -> Unit
onApproved: () -> Unit,
onError: (String, String) -> Unit
) {
val fragmentContext = context.getFragmentActivity()!!
val keyguardManager =
@ -626,26 +640,19 @@ fun authenticate(
when (errorCode) {
BiometricPrompt.ERROR_NEGATIVE_BUTTON -> keyguardPrompt()
BiometricPrompt.ERROR_LOCKOUT -> keyguardPrompt()
else ->
scope.launch {
Toast.makeText(
context,
"${context.getString(R.string.biometric_error)}: $errString",
Toast.LENGTH_SHORT
).show()
}
else -> onError(
context.getString(R.string.biometric_authentication_failed),
context.getString(R.string.biometric_authentication_failed_explainer_with_error, errString)
)
}
}
override fun onAuthenticationFailed() {
super.onAuthenticationFailed()
scope.launch {
Toast.makeText(
context,
onError(
context.getString(R.string.biometric_authentication_failed),
Toast.LENGTH_SHORT
).show()
}
context.getString(R.string.biometric_authentication_failed_explainer)
)
}
override fun onAuthenticationSucceeded(result: BiometricPrompt.AuthenticationResult) {

View File

@ -435,11 +435,9 @@ fun NoteDropDownMenu(note: Note, popupExpanded: MutableState<Boolean>, accountVi
},
onClick = {
val author = note.author ?: return@DropdownMenuItem
scope.launch(Dispatchers.IO) {
accountViewModel.follow(author)
onDismiss()
}
}
)
Divider()
}

View File

@ -91,7 +91,7 @@ class ZapOptionstViewModel : ViewModel() {
@Composable
fun ZapCustomDialog(
onClose: () -> Unit,
onError: (text: String) -> Unit,
onError: (title: String, text: String) -> Unit,
onProgress: (percent: Float) -> Unit,
onPayViaIntent: (ImmutableList<ZapPaymentHandler.Payable>) -> Unit,
accountViewModel: AccountViewModel,
@ -254,7 +254,7 @@ fun ErrorMessageDialog(
title: String,
textContent: String,
buttonColors: ButtonColors = ButtonDefaults.buttonColors(),
onClickStartMessage: () -> Unit,
onClickStartMessage: (() -> Unit)? = null,
onDismiss: () -> Unit
) {
AlertDialog(
@ -274,6 +274,7 @@ fun ErrorMessageDialog(
.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween
) {
onClickStartMessage?.let {
TextButton(onClick = onClickStartMessage) {
Icon(
painter = painterResource(R.drawable.ic_dm),
@ -282,6 +283,7 @@ fun ErrorMessageDialog(
Spacer(StdHorzSpacer)
Text(stringResource(R.string.error_dialog_talk_to_user))
}
}
Button(onClick = onDismiss, colors = buttonColors, contentPadding = PaddingValues(horizontal = Size16dp)) {
Row(
verticalAlignment = Alignment.CenterVertically

View File

@ -1,6 +1,5 @@
package com.vitorpamplona.amethyst.ui.note
import android.widget.Toast
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
@ -20,7 +19,6 @@ import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
@ -182,9 +180,6 @@ fun ShowFollowingOrUnfollowingButton(
baseAuthor: User,
accountViewModel: AccountViewModel
) {
val scope = rememberCoroutineScope()
val context = LocalContext.current
var isFollowing by remember { mutableStateOf(false) }
val accountFollowsState by accountViewModel.account.userProfile().live().follows.observeAsState()
@ -203,48 +198,30 @@ fun ShowFollowingOrUnfollowingButton(
UnfollowButton {
if (!accountViewModel.isWriteable()) {
if (accountViewModel.loggedInWithExternalSigner()) {
scope.launch(Dispatchers.IO) {
accountViewModel.unfollow(baseAuthor)
}
} else {
scope.launch {
Toast
.makeText(
context,
context.getString(R.string.login_with_a_private_key_to_be_able_to_unfollow),
Toast.LENGTH_SHORT
accountViewModel.toast(
R.string.read_only_user,
R.string.login_with_a_private_key_to_be_able_to_unfollow
)
.show()
}
}
} else {
scope.launch(Dispatchers.IO) {
accountViewModel.unfollow(baseAuthor)
}
}
}
} else {
FollowButton {
if (!accountViewModel.isWriteable()) {
if (accountViewModel.loggedInWithExternalSigner()) {
scope.launch(Dispatchers.IO) {
accountViewModel.account.follow(baseAuthor)
}
} else {
scope.launch {
Toast
.makeText(
context,
context.getString(R.string.login_with_a_private_key_to_be_able_to_follow),
Toast.LENGTH_SHORT
)
.show()
}
}
} else {
scope.launch(Dispatchers.IO) {
accountViewModel.follow(baseAuthor)
} else {
accountViewModel.toast(
R.string.read_only_user,
R.string.login_with_a_private_key_to_be_able_to_follow
)
}
} else {
accountViewModel.follow(baseAuthor)
}
}
}

View File

@ -52,7 +52,7 @@ import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch
@Composable
fun AccountBackupDialog(account: Account, onClose: () -> Unit) {
fun AccountBackupDialog(accountViewModel: AccountViewModel, onClose: () -> Unit) {
Dialog(
onDismissRequest = onClose,
properties = DialogProperties(usePlatformDefaultWidth = false)
@ -90,7 +90,7 @@ fun AccountBackupDialog(account: Account, onClose: () -> Unit) {
Spacer(modifier = Modifier.height(30.dp))
NSecCopyButton(account)
NSecCopyButton(accountViewModel)
}
}
}
@ -99,7 +99,7 @@ fun AccountBackupDialog(account: Account, onClose: () -> Unit) {
@Composable
private fun NSecCopyButton(
account: Account
accountViewModel: AccountViewModel
) {
val clipboardManager = LocalClipboardManager.current
val context = LocalContext.current
@ -108,7 +108,7 @@ private fun NSecCopyButton(
val keyguardLauncher =
rememberLauncherForActivityResult(ActivityResultContracts.StartActivityForResult()) { result: ActivityResult ->
if (result.resultCode == Activity.RESULT_OK) {
copyNSec(context, scope, account, clipboardManager)
copyNSec(context, scope, accountViewModel.account, clipboardManager)
}
}
@ -118,11 +118,14 @@ private fun NSecCopyButton(
authenticate(
title = context.getString(R.string.copy_my_secret_key),
context = context,
scope = scope,
keyguardLauncher = keyguardLauncher
) {
copyNSec(context, scope, account, clipboardManager)
keyguardLauncher = keyguardLauncher,
onApproved = {
copyNSec(context, scope, accountViewModel.account, clipboardManager)
},
onError = { title, message ->
accountViewModel.toast(title, message)
}
)
},
shape = ButtonBorder,
colors = ButtonDefaults.buttonColors(

View File

@ -60,12 +60,22 @@ import kotlinx.collections.immutable.toImmutableList
import kotlinx.collections.immutable.toImmutableSet
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.launch
import java.math.BigDecimal
import java.util.Locale
import kotlin.time.measureTimedValue
@Immutable
open class ToastMsg()
@Immutable
class StringToastMsg(val title: String, val msg: String) : ToastMsg()
@Immutable
class ResourceToastMsg(val titleResId: Int, val resourceId: Int) : ToastMsg()
@Stable
class AccountViewModel(val account: Account) : ViewModel(), Dao {
val accountLiveData: LiveData<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 userRelays: LiveData<UserState> = account.userProfile().live().relays.map { it }
val toasts = MutableSharedFlow<ToastMsg?>()
val discoveryListLiveData = account.live.map {
it.account.defaultDiscoveryFollowList
}.distinctUntilChanged()
@ -95,6 +107,24 @@ class AccountViewModel(val account: Account) : ViewModel(), Dao {
it.account.showSensitiveContent
}.distinctUntilChanged()
fun clearToasts() {
viewModelScope.launch {
toasts.emit(null)
}
}
fun toast(title: String, message: String) {
viewModelScope.launch {
toasts.emit(StringToastMsg(title, message))
}
}
fun toast(titleResId: Int, resourceId: Int) {
viewModelScope.launch {
toasts.emit(ResourceToastMsg(titleResId, resourceId))
}
}
fun updateAutomaticallyStartPlayback(
automaticallyStartPlayback: ConnectivityType
) {
@ -152,6 +182,17 @@ class AccountViewModel(val account: Account) : ViewModel(), Dao {
}
}
fun reactToOrDelete(note: Note) {
viewModelScope.launch(Dispatchers.IO) {
val reaction = account.reactionChoices.first()
if (hasReactedTo(note, reaction)) {
deleteReactionTo(note, reaction)
} else {
reactTo(note, reaction)
}
}
}
fun isNoteHidden(note: Note): Boolean {
val isSensitive = note.event?.isSensitive() ?: false
return account.isHidden(note.author!!) || (isSensitive && account.showSensitiveContent == false)
@ -170,8 +211,10 @@ class AccountViewModel(val account: Account) : ViewModel(), Dao {
}
fun deleteBoostsTo(note: Note) {
viewModelScope.launch(Dispatchers.IO) {
account.delete(account.boostsTo(note))
}
}
fun calculateIfNoteWasZappedByAccount(zappedNote: Note, onWasZapped: (Boolean) -> Unit) {
viewModelScope.launch(Dispatchers.Default) {
@ -286,7 +329,7 @@ class AccountViewModel(val account: Account) : ViewModel(), Dao {
pollOption: Int?,
message: String,
context: Context,
onError: (String) -> Unit,
onError: (String, String) -> Unit,
onProgress: (percent: Float) -> Unit,
onPayViaIntent: (ImmutableList<ZapPaymentHandler.Payable>) -> Unit,
zapType: LnZapEvent.ZapType
@ -308,8 +351,10 @@ class AccountViewModel(val account: Account) : ViewModel(), Dao {
}
fun boost(note: Note) {
viewModelScope.launch(Dispatchers.IO) {
account.boost(note)
}
}
fun removeEmojiPack(usersEmojiList: Note, emojiList: Note) {
account.removeEmojiPack(usersEmojiList, emojiList)
@ -388,12 +433,52 @@ class AccountViewModel(val account: Account) : ViewModel(), Dao {
}
fun follow(user: User) {
viewModelScope.launch(Dispatchers.IO) {
account.follow(user)
}
}
fun unfollow(user: 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 {
return account.userProfile().pubkeyHex == user?.pubkeyHex

View File

@ -1,6 +1,5 @@
package com.vitorpamplona.amethyst.ui.screen.loggedIn
import android.widget.Toast
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
@ -17,11 +16,9 @@ import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.MutableState
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.SpanStyle
import androidx.compose.ui.text.input.KeyboardCapitalization
@ -37,12 +34,9 @@ import com.vitorpamplona.amethyst.ui.actions.CloseButton
import com.vitorpamplona.amethyst.ui.theme.ButtonBorder
import com.vitorpamplona.amethyst.ui.theme.RichTextDefaults
import com.vitorpamplona.amethyst.ui.theme.placeholderText
import kotlinx.coroutines.launch
@Composable
fun ConnectOrbotDialog(onClose: () -> Unit, onPost: () -> Unit, portNumber: MutableState<String>) {
val context = LocalContext.current
val scope = rememberCoroutineScope()
fun ConnectOrbotDialog(onClose: () -> Unit, onPost: () -> Unit, onError: (String) -> Unit, portNumber: MutableState<String>) {
Dialog(
onDismissRequest = onClose,
properties = DialogProperties(usePlatformDefaultWidth = false, decorFitsSystemWindows = false)
@ -67,13 +61,7 @@ fun ConnectOrbotDialog(onClose: () -> Unit, onPost: () -> Unit, portNumber: Muta
try {
Integer.parseInt(portNumber.value)
} catch (_: Exception) {
scope.launch {
Toast.makeText(
context,
toastMessage,
Toast.LENGTH_LONG
).show()
}
onError(toastMessage)
return@UseOrbotButton
}

View File

@ -1,6 +1,5 @@
package com.vitorpamplona.amethyst.ui.screen.loggedIn
import android.widget.Toast
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
@ -18,7 +17,6 @@ import androidx.compose.runtime.getValue
import androidx.compose.runtime.livedata.observeAsState
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
@ -160,9 +158,6 @@ fun GeoHashActionOptions(
tag: String,
accountViewModel: AccountViewModel
) {
val scope = rememberCoroutineScope()
val context = LocalContext.current
val userState by accountViewModel.userProfile().live().follows.observeAsState()
val isFollowingTag by remember(userState) {
derivedStateOf {
@ -174,48 +169,30 @@ fun GeoHashActionOptions(
UnfollowButton {
if (!accountViewModel.isWriteable()) {
if (accountViewModel.loggedInWithExternalSigner()) {
scope.launch(Dispatchers.IO) {
accountViewModel.account.unfollowGeohash(tag)
}
accountViewModel.unfollowGeohash(tag)
} else {
scope.launch {
Toast
.makeText(
context,
context.getString(R.string.login_with_a_private_key_to_be_able_to_unfollow),
Toast.LENGTH_SHORT
accountViewModel.toast(
R.string.read_only_user,
R.string.login_with_a_private_key_to_be_able_to_unfollow
)
.show()
}
}
} else {
scope.launch(Dispatchers.IO) {
accountViewModel.account.unfollowGeohash(tag)
}
accountViewModel.unfollowGeohash(tag)
}
}
} else {
FollowButton {
if (!accountViewModel.isWriteable()) {
if (accountViewModel.loggedInWithExternalSigner()) {
scope.launch(Dispatchers.IO) {
accountViewModel.account.followGeohash(tag)
}
accountViewModel.followGeohash(tag)
} else {
scope.launch {
Toast
.makeText(
context,
context.getString(R.string.login_with_a_private_key_to_be_able_to_follow),
Toast.LENGTH_SHORT
accountViewModel.toast(
R.string.read_only_user,
R.string.login_with_a_private_key_to_be_able_to_follow
)
.show()
}
}
} else {
scope.launch(Dispatchers.IO) {
accountViewModel.account.followGeohash(tag)
}
accountViewModel.followGeohash(tag)
}
}
}

View File

@ -1,6 +1,5 @@
package com.vitorpamplona.amethyst.ui.screen.loggedIn
import android.widget.Toast
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
@ -16,10 +15,8 @@ import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue
import androidx.compose.runtime.livedata.observeAsState
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalLifecycleOwner
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
@ -31,8 +28,6 @@ import com.vitorpamplona.amethyst.service.NostrHashtagDataSource
import com.vitorpamplona.amethyst.ui.screen.NostrHashtagFeedViewModel
import com.vitorpamplona.amethyst.ui.screen.RefresheableFeedView
import com.vitorpamplona.amethyst.ui.theme.StdPadding
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
@Composable
fun HashtagScreen(tag: String?, accountViewModel: AccountViewModel, nav: (String) -> Unit) {
@ -136,9 +131,6 @@ fun HashtagActionOptions(
tag: String,
accountViewModel: AccountViewModel
) {
val scope = rememberCoroutineScope()
val context = LocalContext.current
val userState by accountViewModel.userProfile().live().follows.observeAsState()
val isFollowingTag by remember(userState) {
derivedStateOf {
@ -150,48 +142,30 @@ fun HashtagActionOptions(
UnfollowButton {
if (!accountViewModel.isWriteable()) {
if (accountViewModel.loggedInWithExternalSigner()) {
scope.launch(Dispatchers.IO) {
accountViewModel.account.unfollowHashtag(tag)
}
accountViewModel.unfollowHashtag(tag)
} else {
scope.launch {
Toast
.makeText(
context,
context.getString(R.string.login_with_a_private_key_to_be_able_to_unfollow),
Toast.LENGTH_SHORT
accountViewModel.toast(
R.string.read_only_user,
R.string.login_with_a_private_key_to_be_able_to_unfollow
)
.show()
}
}
} else {
scope.launch(Dispatchers.IO) {
accountViewModel.account.unfollowHashtag(tag)
}
accountViewModel.unfollowHashtag(tag)
}
}
} else {
FollowButton {
if (!accountViewModel.isWriteable()) {
if (accountViewModel.loggedInWithExternalSigner()) {
scope.launch(Dispatchers.IO) {
accountViewModel.account.followHashtag(tag)
}
accountViewModel.followHashtag(tag)
} else {
scope.launch {
Toast
.makeText(
context,
context.getString(R.string.login_with_a_private_key_to_be_able_to_follow),
Toast.LENGTH_SHORT
accountViewModel.toast(
R.string.read_only_user,
R.string.login_with_a_private_key_to_be_able_to_follow
)
.show()
}
}
} else {
scope.launch(Dispatchers.IO) {
accountViewModel.account.followHashtag(tag)
}
accountViewModel.followHashtag(tag)
}
}
}

View File

@ -1,6 +1,5 @@
package com.vitorpamplona.amethyst.ui.screen.loggedIn
import android.widget.Toast
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
@ -35,7 +34,6 @@ import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalLifecycleOwner
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontWeight
@ -65,7 +63,6 @@ import com.vitorpamplona.amethyst.ui.theme.Size10dp
import com.vitorpamplona.amethyst.ui.theme.StdPadding
import com.vitorpamplona.amethyst.ui.theme.TabRowHeight
import com.vitorpamplona.amethyst.ui.theme.placeholderText
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
@Composable
@ -307,9 +304,6 @@ fun MutedWordActionOptions(
word: String,
accountViewModel: AccountViewModel
) {
val scope = rememberCoroutineScope()
val context = LocalContext.current
val isMutedWord by accountViewModel.account.liveHiddenUsers.map {
word in it.hiddenWords
}.distinctUntilChanged().observeAsState()
@ -318,48 +312,30 @@ fun MutedWordActionOptions(
ShowWordButton {
if (!accountViewModel.isWriteable()) {
if (accountViewModel.loggedInWithExternalSigner()) {
scope.launch(Dispatchers.IO) {
accountViewModel.account.showWord(word)
}
accountViewModel.showWord(word)
} else {
scope.launch {
Toast
.makeText(
context,
context.getString(R.string.login_with_a_private_key_to_be_able_to_unfollow),
Toast.LENGTH_SHORT
accountViewModel.toast(
R.string.read_only_user,
R.string.login_with_a_private_key_to_be_able_to_show_word
)
.show()
}
}
} else {
scope.launch(Dispatchers.IO) {
accountViewModel.account.showWord(word)
}
accountViewModel.showWord(word)
}
}
} else {
HideWordButton {
if (!accountViewModel.isWriteable()) {
if (accountViewModel.loggedInWithExternalSigner()) {
scope.launch(Dispatchers.IO) {
accountViewModel.account.hideWord(word)
}
accountViewModel.hideWord(word)
} else {
scope.launch {
Toast
.makeText(
context,
context.getString(R.string.login_with_a_private_key_to_be_able_to_follow),
Toast.LENGTH_SHORT
accountViewModel.toast(
R.string.read_only_user,
R.string.login_with_a_private_key_to_be_able_to_hide_word
)
.show()
}
}
} else {
scope.launch(Dispatchers.IO) {
accountViewModel.account.hideWord(word)
}
accountViewModel.hideWord(word)
}
}
}

View File

@ -39,6 +39,7 @@ import androidx.compose.ui.input.nestedscroll.NestedScrollConnection
import androidx.compose.ui.input.nestedscroll.NestedScrollSource
import androidx.compose.ui.input.nestedscroll.nestedScroll
import androidx.compose.ui.platform.LocalConfiguration
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.platform.LocalLifecycleOwner
import androidx.compose.ui.unit.dp
@ -49,6 +50,7 @@ import androidx.navigation.NavBackStackEntry
import androidx.navigation.compose.currentBackStackEntryAsState
import androidx.navigation.compose.rememberNavController
import com.vitorpamplona.amethyst.model.BooleanType
import com.vitorpamplona.amethyst.ui.actions.InformationDialog
import com.vitorpamplona.amethyst.ui.buttons.ChannelFabColumn
import com.vitorpamplona.amethyst.ui.buttons.NewCommunityNoteButton
import com.vitorpamplona.amethyst.ui.buttons.NewImageButton
@ -119,6 +121,8 @@ fun MainScreen(
}
}
DisplayErrorMessages(accountViewModel)
val navPopBack = remember(navController) {
{
navController.popBackStack()
@ -356,6 +360,30 @@ fun MainScreen(
}
}
@Composable
private fun DisplayErrorMessages(accountViewModel: AccountViewModel) {
val context = LocalContext.current
val openDialogMsg = accountViewModel.toasts.collectAsState(initial = null)
openDialogMsg.value?.let { obj ->
when (obj) {
is ResourceToastMsg -> InformationDialog(
context.getString(obj.titleResId),
context.getString(obj.resourceId)
) {
accountViewModel.clearToasts()
}
is StringToastMsg -> InformationDialog(
obj.title,
obj.msg
) {
accountViewModel.clearToasts()
}
}
}
}
@Composable
fun WatchNavStateToUpdateBarVisibility(navState: State<NavBackStackEntry?>, bottomBarOffsetHeightPx: MutableState<Float>) {
LaunchedEffect(key1 = navState.value) {

View File

@ -1,8 +1,5 @@
package com.vitorpamplona.amethyst.ui.screen.loggedIn
import android.content.Intent
import android.net.Uri
import android.widget.Toast
import androidx.compose.animation.Crossfade
import androidx.compose.animation.core.tween
import androidx.compose.foundation.*
@ -46,7 +43,6 @@ import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.IntSize
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.core.content.ContextCompat
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.LifecycleEventObserver
import androidx.lifecycle.distinctUntilChanged
@ -62,6 +58,7 @@ import com.vitorpamplona.amethyst.model.Note
import com.vitorpamplona.amethyst.model.User
import com.vitorpamplona.amethyst.service.NostrUserProfileDataSource
import com.vitorpamplona.amethyst.service.connectivitystatus.ConnectivityStatus
import com.vitorpamplona.amethyst.ui.actions.InformationDialog
import com.vitorpamplona.amethyst.ui.actions.NewUserMetadataView
import com.vitorpamplona.amethyst.ui.components.CreateTextWithEmoji
import com.vitorpamplona.amethyst.ui.components.DisplayNip05ProfileStatus
@ -74,8 +71,11 @@ import com.vitorpamplona.amethyst.ui.components.figureOutMimeType
import com.vitorpamplona.amethyst.ui.dal.UserProfileReportsFeedFilter
import com.vitorpamplona.amethyst.ui.navigation.ShowQRDialog
import com.vitorpamplona.amethyst.ui.note.ClickableUserPicture
import com.vitorpamplona.amethyst.ui.note.ErrorMessageDialog
import com.vitorpamplona.amethyst.ui.note.LightningAddressIcon
import com.vitorpamplona.amethyst.ui.note.LoadAddressableNote
import com.vitorpamplona.amethyst.ui.note.payViaIntent
import com.vitorpamplona.amethyst.ui.note.routeToMessage
import com.vitorpamplona.amethyst.ui.screen.FeedState
import com.vitorpamplona.amethyst.ui.screen.LnZapFeedView
import com.vitorpamplona.amethyst.ui.screen.NostrUserAppRecommendationsFeedViewModel
@ -739,9 +739,6 @@ private fun DisplayFollowUnfollowButton(
baseUser: User,
accountViewModel: AccountViewModel
) {
val scope = rememberCoroutineScope()
val context = LocalContext.current
val isLoggedInFollowingUser by accountViewModel.account.userProfile().live().follows.map {
it.user.isFollowing(baseUser)
}.distinctUntilChanged().observeAsState(initial = accountViewModel.account.isFollowing(baseUser))
@ -754,24 +751,15 @@ private fun DisplayFollowUnfollowButton(
UnfollowButton {
if (!accountViewModel.isWriteable()) {
if (accountViewModel.loggedInWithExternalSigner()) {
scope.launch(Dispatchers.IO) {
accountViewModel.account.unfollow(baseUser)
}
accountViewModel.unfollow(baseUser)
} else {
scope.launch {
Toast
.makeText(
context,
context.getString(R.string.login_with_a_private_key_to_be_able_to_unfollow),
Toast.LENGTH_SHORT
accountViewModel.toast(
R.string.read_only_user,
R.string.login_with_a_private_key_to_be_able_to_unfollow
)
.show()
}
}
} else {
scope.launch(Dispatchers.IO) {
accountViewModel.account.unfollow(baseUser)
}
accountViewModel.unfollow(baseUser)
}
}
} else {
@ -779,48 +767,30 @@ private fun DisplayFollowUnfollowButton(
FollowButton(R.string.follow_back) {
if (!accountViewModel.isWriteable()) {
if (accountViewModel.loggedInWithExternalSigner()) {
scope.launch(Dispatchers.IO) {
accountViewModel.account.follow(baseUser)
}
accountViewModel.follow(baseUser)
} else {
scope.launch {
Toast
.makeText(
context,
context.getString(R.string.login_with_a_private_key_to_be_able_to_follow),
Toast.LENGTH_SHORT
accountViewModel.toast(
R.string.read_only_user,
R.string.login_with_a_private_key_to_be_able_to_follow
)
.show()
}
}
} else {
scope.launch(Dispatchers.IO) {
accountViewModel.account.follow(baseUser)
}
accountViewModel.follow(baseUser)
}
}
} else {
FollowButton(R.string.follow) {
if (!accountViewModel.isWriteable()) {
if (accountViewModel.loggedInWithExternalSigner()) {
scope.launch(Dispatchers.IO) {
accountViewModel.account.follow(baseUser)
}
accountViewModel.follow(baseUser)
} else {
scope.launch {
Toast
.makeText(
context,
context.getString(R.string.login_with_a_private_key_to_be_able_to_follow),
Toast.LENGTH_SHORT
accountViewModel.toast(
R.string.read_only_user,
R.string.login_with_a_private_key_to_be_able_to_follow
)
.show()
}
}
} else {
scope.launch(Dispatchers.IO) {
accountViewModel.account.follow(baseUser)
}
accountViewModel.follow(baseUser)
}
}
}
@ -974,7 +944,7 @@ private fun DrawAdditionalInfo(
val lud16 = remember(userState) { user.info?.lud16?.trim() ?: user.info?.lud06?.trim() }
val pubkeyHex = remember { baseUser.pubkeyHex }
DisplayLNAddress(lud16, pubkeyHex, accountViewModel.account)
DisplayLNAddress(lud16, pubkeyHex, accountViewModel, nav)
val identities = user.info?.latestMetadata?.identityClaims()
if (!identities.isNullOrEmpty()) {
@ -1026,12 +996,39 @@ private fun DrawAdditionalInfo(
fun DisplayLNAddress(
lud16: String?,
userHex: String,
account: Account
accountViewModel: AccountViewModel,
nav: (String) -> Unit
) {
val context = LocalContext.current
val scope = rememberCoroutineScope()
var zapExpanded by remember { mutableStateOf(false) }
var showErrorMessageDialog by remember { mutableStateOf<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()) {
Row(verticalAlignment = Alignment.CenterVertically) {
LightningAddressIcon(modifier = Size16Modifier, tint = BitcoinOrange)
@ -1054,50 +1051,31 @@ fun DisplayLNAddress(
InvoiceRequestCard(
lud16,
userHex,
account,
accountViewModel.account,
onSuccess = {
zapExpanded = false
// pay directly
if (account.hasWalletConnectSetup()) {
account.sendZapPaymentRequestFor(it, null) { response ->
if (accountViewModel.account.hasWalletConnectSetup()) {
accountViewModel.account.sendZapPaymentRequestFor(it, null) { response ->
if (response is PayInvoiceSuccessResponse) {
scope.launch {
Toast.makeText(
context,
context.getString(R.string.payment_successful), // Turn this into a UI animation
Toast.LENGTH_LONG
).show()
}
showInfoMessageDialog = context.getString(R.string.payment_successful)
} else if (response is PayInvoiceErrorResponse) {
scope.launch {
Toast.makeText(
context,
response.error?.message
showErrorMessageDialog = response.error?.message
?: response.error?.code?.toString()
?: context.getString(R.string.error_parsing_error_message),
Toast.LENGTH_LONG
).show()
}
?: context.getString(R.string.error_parsing_error_message)
}
}
} else {
try {
val intent = Intent(Intent.ACTION_VIEW, Uri.parse("lightning:$it"))
intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK
ContextCompat.startActivity(context, intent, null)
} catch (e: Exception) {
scope.launch {
Toast.makeText(
context,
context.getString(R.string.lightning_wallets_not_found),
Toast.LENGTH_LONG
).show()
}
payViaIntent(it, context) {
showErrorMessageDialog = it
}
}
},
onClose = {
zapExpanded = false
},
onError = { title, message ->
accountViewModel.toast(title, message)
}
)
}

View File

@ -311,6 +311,15 @@ fun LoginPage(
connectOrbotDialogOpen = false
useProxy.value = true
},
onError = {
scope.launch {
Toast.makeText(
context,
it,
Toast.LENGTH_LONG
).show()
}
},
proxyPort
)
}

View File

@ -30,13 +30,17 @@
<string name="report_impersonation">Report Impersonation</string>
<string name="report_explicit_content">Report Explicit Content</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_boost_posts">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_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">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">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="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_follow">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_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">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">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="view_count">View count</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="copy_my_secret_key">Copy my secret key</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="badge_created_by">"Created by %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_closing_time">Close after</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="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_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_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="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_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_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="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="zap_split_weight">Percentage</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="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>