mirror of
https://github.com/vitorpamplona/amethyst.git
synced 2026-08-06 14:54:36 +00:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
227aac5b76 | ||
|
|
82ea72d2d1 | ||
|
|
f1637c3d20 | ||
|
|
07be841246 | ||
|
|
3ce5d92556 | ||
|
|
8f11396e00 | ||
|
|
2259523387 | ||
|
|
d67711b123 | ||
|
|
c89ca51a1b | ||
|
|
c5c951dd59 | ||
|
|
9f68f0227e | ||
|
|
7faed2ddd3 | ||
|
|
7dceb701b2 | ||
|
|
e454ccc326 | ||
|
|
bac50668e8 | ||
|
|
ff9e1556d5 | ||
|
|
dffb49b071 | ||
|
|
7387d9060f | ||
|
|
52e1842174 |
+3
-3
@@ -13,8 +13,8 @@ android {
|
||||
applicationId "com.vitorpamplona.amethyst"
|
||||
minSdk 26
|
||||
targetSdk 34
|
||||
versionCode 309
|
||||
versionName "0.79.0"
|
||||
versionCode 311
|
||||
versionName "0.79.2"
|
||||
|
||||
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
||||
vectorDrawables {
|
||||
@@ -171,7 +171,7 @@ dependencies {
|
||||
playImplementation 'com.google.mlkit:translate:17.0.1'
|
||||
|
||||
// PushNotifications
|
||||
playImplementation platform('com.google.firebase:firebase-bom:32.2.3')
|
||||
playImplementation platform('com.google.firebase:firebase-bom:32.3.1')
|
||||
playImplementation 'com.google.firebase:firebase-messaging-ktx'
|
||||
|
||||
// Charts
|
||||
|
||||
@@ -33,6 +33,10 @@ import com.vitorpamplona.amethyst.service.NostrVideoDataSource
|
||||
import com.vitorpamplona.amethyst.service.relays.Client
|
||||
import com.vitorpamplona.amethyst.ui.actions.ImageUploader
|
||||
import com.vitorpamplona.quartz.encoders.decodePublicKeyAsHexOrNull
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.GlobalScope
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.launch
|
||||
import java.io.File
|
||||
|
||||
object ServiceManager {
|
||||
@@ -87,9 +91,12 @@ object ServiceManager {
|
||||
// Notification Elements
|
||||
NostrHomeDataSource.start()
|
||||
NostrAccountDataSource.start()
|
||||
NostrChatroomListDataSource.start()
|
||||
NostrDiscoveryDataSource.start()
|
||||
NostrVideoDataSource.start()
|
||||
GlobalScope.launch(Dispatchers.IO) {
|
||||
delay(3000)
|
||||
NostrChatroomListDataSource.start()
|
||||
NostrDiscoveryDataSource.start()
|
||||
NostrVideoDataSource.start()
|
||||
}
|
||||
|
||||
// More Info Data Sources
|
||||
NostrSingleEventDataSource.start()
|
||||
|
||||
@@ -98,7 +98,6 @@ class Account(
|
||||
// Observers line up here.
|
||||
val live: AccountLiveData = AccountLiveData(this)
|
||||
val liveLanguages: AccountLiveData = AccountLiveData(this)
|
||||
val liveLastRead: AccountLiveData = AccountLiveData(this)
|
||||
val saveable: AccountLiveData = AccountLiveData(this)
|
||||
|
||||
@Immutable
|
||||
@@ -896,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)
|
||||
@@ -1065,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)
|
||||
@@ -3113,12 +3112,14 @@ class Account(
|
||||
live.invalidateData()
|
||||
}
|
||||
|
||||
fun markAsRead(route: String, timestampInSecs: Long) {
|
||||
fun markAsRead(route: String, timestampInSecs: Long): Boolean {
|
||||
val lastTime = lastReadPerRoute[route]
|
||||
if (lastTime == null || timestampInSecs > lastTime) {
|
||||
return if (lastTime == null || timestampInSecs > lastTime) {
|
||||
lastReadPerRoute = lastReadPerRoute + Pair(route, timestampInSecs)
|
||||
saveable.invalidateData()
|
||||
liveLastRead.invalidateData()
|
||||
true
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -22,6 +22,7 @@ import kotlinx.collections.immutable.persistentSetOf
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
import kotlinx.collections.immutable.toImmutableSet
|
||||
import kotlinx.coroutines.*
|
||||
import kotlinx.coroutines.channels.BufferOverflow
|
||||
import kotlinx.coroutines.flow.MutableSharedFlow
|
||||
import kotlinx.coroutines.flow.asSharedFlow
|
||||
import java.io.File
|
||||
@@ -348,7 +349,12 @@ object LocalCache {
|
||||
val channel = getOrCreateChannel(note.idHex) {
|
||||
LiveActivitiesChannel(note.address)
|
||||
} as? LiveActivitiesChannel
|
||||
channel?.updateChannelInfo(author, event, event.createdAt)
|
||||
|
||||
val creator = event.host()?.ifBlank { null }?.let {
|
||||
checkGetOrCreateUser(it)
|
||||
} ?: author
|
||||
|
||||
channel?.updateChannelInfo(creator, event, event.createdAt)
|
||||
|
||||
refreshObservers(note)
|
||||
}
|
||||
@@ -1548,7 +1554,7 @@ object LocalCache {
|
||||
|
||||
@Stable
|
||||
class LocalCacheLiveData {
|
||||
private val _newEventBundles = MutableSharedFlow<Set<Note>>()
|
||||
private val _newEventBundles = MutableSharedFlow<Set<Note>>(0, 10, BufferOverflow.DROP_OLDEST)
|
||||
val newEventBundles = _newEventBundles.asSharedFlow() // read-only public view
|
||||
|
||||
// Refreshes observers in batches.
|
||||
|
||||
@@ -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))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ package com.vitorpamplona.amethyst.service
|
||||
|
||||
import com.vitorpamplona.amethyst.model.Account
|
||||
import com.vitorpamplona.amethyst.model.LocalCache
|
||||
import com.vitorpamplona.amethyst.model.User
|
||||
import com.vitorpamplona.amethyst.service.relays.COMMON_FEED_TYPES
|
||||
import com.vitorpamplona.amethyst.service.relays.Client
|
||||
import com.vitorpamplona.amethyst.service.relays.EOSEAccount
|
||||
@@ -34,6 +35,7 @@ object NostrAccountDataSource : NostrDataSource("AccountData") {
|
||||
lateinit var account: Account
|
||||
|
||||
val latestEOSEs = EOSEAccount()
|
||||
val hasLoadedTheBasics = mutableMapOf<User, Boolean>()
|
||||
|
||||
fun createAccountContactListFilter(): TypedFilter {
|
||||
return TypedFilter(
|
||||
@@ -141,7 +143,11 @@ object NostrAccountDataSource : NostrDataSource("AccountData") {
|
||||
)
|
||||
|
||||
val accountChannel = requestNewChannel { time, relayUrl ->
|
||||
latestEOSEs.addOrUpdate(account.userProfile(), account.defaultNotificationFollowList, relayUrl, time)
|
||||
if (hasLoadedTheBasics[account.userProfile()] != null) {
|
||||
latestEOSEs.addOrUpdate(account.userProfile(), account.defaultNotificationFollowList, relayUrl, time)
|
||||
} else {
|
||||
hasLoadedTheBasics[account.userProfile()] = true
|
||||
}
|
||||
}
|
||||
|
||||
override fun consume(event: Event, relay: Relay) {
|
||||
@@ -201,18 +207,28 @@ object NostrAccountDataSource : NostrDataSource("AccountData") {
|
||||
}
|
||||
|
||||
override fun updateChannelFilters() {
|
||||
// gets everthing about the user logged in
|
||||
accountChannel.typedFilters = listOf(
|
||||
createAccountMetadataFilter(),
|
||||
createAccountContactListFilter(),
|
||||
createAccountRelayListFilter(),
|
||||
createNotificationFilter(),
|
||||
createGiftWrapsToMeFilter(),
|
||||
createAccountReportsFilter(),
|
||||
createAccountAcceptedAwardsFilter(),
|
||||
createAccountBookmarkListFilter(),
|
||||
createAccountLastPostsListFilter()
|
||||
).ifEmpty { null }
|
||||
return if (hasLoadedTheBasics[account.userProfile()] != null) {
|
||||
// gets everthing about the user logged in
|
||||
accountChannel.typedFilters = listOf(
|
||||
createAccountMetadataFilter(),
|
||||
createAccountContactListFilter(),
|
||||
createAccountRelayListFilter(),
|
||||
createNotificationFilter(),
|
||||
createGiftWrapsToMeFilter(),
|
||||
createAccountReportsFilter(),
|
||||
createAccountAcceptedAwardsFilter(),
|
||||
createAccountBookmarkListFilter(),
|
||||
createAccountLastPostsListFilter()
|
||||
).ifEmpty { null }
|
||||
} else {
|
||||
// just the basics.
|
||||
accountChannel.typedFilters = listOf(
|
||||
createAccountMetadataFilter(),
|
||||
createAccountContactListFilter(),
|
||||
createAccountRelayListFilter(),
|
||||
createAccountBookmarkListFilter()
|
||||
).ifEmpty { null }
|
||||
}
|
||||
}
|
||||
|
||||
override fun auth(relay: Relay, challenge: String) {
|
||||
|
||||
@@ -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,9 +187,13 @@ class ZapPaymentHandler(val account: Account) {
|
||||
if (response is PayInvoiceErrorResponse) {
|
||||
onProgress(0.0f)
|
||||
onError(
|
||||
response.error?.message
|
||||
?: response.error?.code?.toString()
|
||||
?: "Error parsing error message"
|
||||
context.getString(R.string.error_dialog_pay_invoice_error),
|
||||
context.getString(
|
||||
R.string.wallet_connect_pay_invoice_error_error,
|
||||
response.error?.message
|
||||
?: response.error?.code?.toString()
|
||||
?: "Error parsing error message"
|
||||
)
|
||||
)
|
||||
} else {
|
||||
onProgress(1f)
|
||||
@@ -192,16 +202,13 @@ class ZapPaymentHandler(val account: Account) {
|
||||
)
|
||||
onProgress(0.8f)
|
||||
} else {
|
||||
try {
|
||||
onPayInvoiceThroughIntent(it)
|
||||
} catch (e: Exception) {
|
||||
onError(context.getString(R.string.lightning_wallets_not_found2))
|
||||
}
|
||||
onPayInvoiceThroughIntent(it)
|
||||
onProgress(0f)
|
||||
}
|
||||
},
|
||||
onError = onError,
|
||||
onProgress = onProgress
|
||||
onProgress = onProgress,
|
||||
context = context
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
+87
-18
@@ -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 the server is up and if the lightning address $lnaddress is correct")
|
||||
onError(
|
||||
context.getString(R.string.error_unable_to_fetch_invoice),
|
||||
context.getString(
|
||||
R.string.could_not_resolve_check_if_you_are_connected_if_the_server_is_up_and_if_the_lightning_address_is_correct,
|
||||
url,
|
||||
lnaddress
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun fetchLightningInvoice(lnCallback: String, milliSats: Long, message: String, nostrRequest: String? = null, onSuccess: suspend (String) -> Unit, onError: (String) -> Unit) = withContext(Dispatchers.IO) {
|
||||
suspend fun fetchLightningInvoice(
|
||||
lnCallback: String,
|
||||
milliSats: Long,
|
||||
message: String,
|
||||
nostrRequest: String? = null,
|
||||
onSuccess: suspend (String) -> Unit,
|
||||
onError: (String, String) -> Unit,
|
||||
context: Context
|
||||
) = withContext(Dispatchers.IO) {
|
||||
val encodedMessage = URLEncoder.encode(message, "utf-8")
|
||||
|
||||
val urlBinder = if (lnCallback.contains("?")) "&" else "?"
|
||||
@@ -80,18 +116,22 @@ class LightningAddressResolver() {
|
||||
if (it.isSuccessful) {
|
||||
onSuccess(it.body.string())
|
||||
} else {
|
||||
onError("Could not fetch invoice from $lnCallback")
|
||||
onError(
|
||||
context.getString(R.string.error_unable_to_fetch_invoice),
|
||||
context.getString(R.string.could_not_fetch_invoice_from, lnCallback)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun lnAddressToLnUrl(lnaddress: String, onSuccess: (String) -> Unit, onError: (String) -> Unit) {
|
||||
suspend fun lnAddressToLnUrl(lnaddress: String, onSuccess: (String) -> Unit, onError: (String, String) -> Unit, context: Context) {
|
||||
fetchLightningAddressJson(
|
||||
lnaddress,
|
||||
onSuccess = {
|
||||
onSuccess(it.toByteArray().toLnUrl())
|
||||
},
|
||||
onError = onError
|
||||
onError = onError,
|
||||
context = context
|
||||
)
|
||||
}
|
||||
|
||||
@@ -101,8 +141,9 @@ class LightningAddressResolver() {
|
||||
message: String,
|
||||
nostrRequest: String? = null,
|
||||
onSuccess: suspend (String) -> Unit,
|
||||
onError: (String) -> Unit,
|
||||
onProgress: (percent: Float) -> Unit
|
||||
onError: (String, String) -> Unit,
|
||||
onProgress: (percent: Float) -> Unit,
|
||||
context: Context
|
||||
) {
|
||||
val mapper = jacksonObjectMapper()
|
||||
|
||||
@@ -114,14 +155,20 @@ class LightningAddressResolver() {
|
||||
val lnurlp = try {
|
||||
mapper.readTree(lnAddressJson)
|
||||
} catch (t: Throwable) {
|
||||
onError("Error Parsing JSON from Lightning Address. Check the user's lightning setup")
|
||||
onError(
|
||||
context.getString(R.string.error_unable_to_fetch_invoice),
|
||||
context.getString(R.string.error_parsing_json_from_lightning_address_check_the_user_s_lightning_setup)
|
||||
)
|
||||
null
|
||||
}
|
||||
|
||||
val callback = lnurlp?.get("callback")?.asText()
|
||||
|
||||
if (callback == null) {
|
||||
onError("Callback URL not found in the User's lightning address server configuration")
|
||||
onError(
|
||||
context.getString(R.string.error_unable_to_fetch_invoice),
|
||||
context.getString(R.string.callback_url_not_found_in_the_user_s_lightning_address_server_configuration)
|
||||
)
|
||||
}
|
||||
|
||||
val allowsNostr = lnurlp?.get("allowsNostr")?.asBoolean() ?: false
|
||||
@@ -138,7 +185,10 @@ class LightningAddressResolver() {
|
||||
val lnInvoice = try {
|
||||
mapper.readTree(it)
|
||||
} catch (t: Throwable) {
|
||||
onError("Error Parsing JSON from Lightning Address's invoice fetch. Check the user's lightning setup")
|
||||
onError(
|
||||
context.getString(R.string.error_unable_to_fetch_invoice),
|
||||
context.getString(R.string.error_parsing_json_from_lightning_address_s_invoice_fetch_check_the_user_s_lightning_setup)
|
||||
)
|
||||
null
|
||||
}
|
||||
|
||||
@@ -151,21 +201,40 @@ class LightningAddressResolver() {
|
||||
onSuccess(pr)
|
||||
} else {
|
||||
onProgress(0.0f)
|
||||
onError("Incorrect invoice amount (${invoiceAmount.toLong()} sats) from $lnaddress. It should have been $expectedAmountInSats")
|
||||
onError(
|
||||
context.getString(R.string.error_unable_to_fetch_invoice),
|
||||
context.getString(
|
||||
R.string.incorrect_invoice_amount_sats_from_it_should_have_been,
|
||||
invoiceAmount.toLong().toString(),
|
||||
lnaddress,
|
||||
expectedAmountInSats.toString()
|
||||
)
|
||||
)
|
||||
}
|
||||
} ?: lnInvoice?.get("reason")?.asText()?.ifBlank { null }?.let { reason ->
|
||||
onProgress(0.0f)
|
||||
onError("Unable to create a lightning invoice before sending the zap. The receiver's lightning wallet sent the following error: $reason")
|
||||
onError(
|
||||
context.getString(R.string.error_unable_to_fetch_invoice),
|
||||
context.getString(
|
||||
R.string.unable_to_create_a_lightning_invoice_before_sending_the_zap_the_receiver_s_lightning_wallet_sent_the_following_error,
|
||||
reason
|
||||
)
|
||||
)
|
||||
} ?: run {
|
||||
onProgress(0.0f)
|
||||
onError("nable to create a lightning invoice before sending the zap. Element pr not found in the resulting JSON.")
|
||||
onError(
|
||||
context.getString(R.string.error_unable_to_fetch_invoice),
|
||||
context.getString(R.string.unable_to_create_a_lightning_invoice_before_sending_the_zap_element_pr_not_found_in_the_resulting_json)
|
||||
)
|
||||
}
|
||||
},
|
||||
onError = onError
|
||||
onError = onError,
|
||||
context
|
||||
)
|
||||
}
|
||||
},
|
||||
onError = onError
|
||||
onError = onError,
|
||||
context
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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))
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
@@ -14,6 +14,7 @@ import com.vitorpamplona.amethyst.service.FileHeader
|
||||
import com.vitorpamplona.amethyst.service.relays.Relay
|
||||
import com.vitorpamplona.amethyst.ui.components.MediaCompressor
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.channels.BufferOverflow
|
||||
import kotlinx.coroutines.flow.MutableSharedFlow
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
@@ -22,7 +23,7 @@ open class NewMediaModel : ViewModel() {
|
||||
var account: Account? = null
|
||||
|
||||
var isUploadingImage by mutableStateOf(false)
|
||||
val imageUploadingError = MutableSharedFlow<String?>()
|
||||
val imageUploadingError = MutableSharedFlow<String?>(0, 3, onBufferOverflow = BufferOverflow.DROP_OLDEST)
|
||||
var mediaType by mutableStateOf<String?>(null)
|
||||
|
||||
var selectedServer by mutableStateOf<ServersAvailable?>(null)
|
||||
|
||||
@@ -380,6 +380,9 @@ fun NewPostView(
|
||||
},
|
||||
onClose = {
|
||||
postViewModel.wantsInvoice = false
|
||||
},
|
||||
onError = { title, message ->
|
||||
accountViewModel.toast(title, message)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
@@ -33,6 +33,7 @@ import com.vitorpamplona.quartz.events.PrivateDmEvent
|
||||
import com.vitorpamplona.quartz.events.TextNoteEvent
|
||||
import com.vitorpamplona.quartz.events.ZapSplitSetup
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.channels.BufferOverflow
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.MutableSharedFlow
|
||||
import kotlinx.coroutines.flow.mapLatest
|
||||
@@ -58,7 +59,7 @@ open class NewPostViewModel() : ViewModel() {
|
||||
var message by mutableStateOf(TextFieldValue(""))
|
||||
var urlPreview by mutableStateOf<String?>(null)
|
||||
var isUploadingImage by mutableStateOf(false)
|
||||
val imageUploadingError = MutableSharedFlow<String?>()
|
||||
val imageUploadingError = MutableSharedFlow<String?>(0, 3, onBufferOverflow = BufferOverflow.DROP_OLDEST)
|
||||
|
||||
var userSuggestions by mutableStateOf<List<User>>(emptyList())
|
||||
var userSuggestionAnchor: TextRange? = null
|
||||
|
||||
@@ -350,15 +350,10 @@ fun ServerConfig(
|
||||
Nip11Retriever.ErrorCode.FAIL_WITH_HTTP_STATUS -> context.getString(R.string.relay_information_document_error_assemble_url, url, exceptionMessage)
|
||||
}
|
||||
|
||||
scope.launch {
|
||||
Toast
|
||||
.makeText(
|
||||
context,
|
||||
msg,
|
||||
Toast.LENGTH_SHORT
|
||||
)
|
||||
.show()
|
||||
}
|
||||
accountViewModel.toast(
|
||||
context.getString(R.string.unable_to_download_relay_document),
|
||||
msg
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
@@ -15,6 +15,7 @@ import com.vitorpamplona.quartz.events.GitHubIdentity
|
||||
import com.vitorpamplona.quartz.events.MastodonIdentity
|
||||
import com.vitorpamplona.quartz.events.TwitterIdentity
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.channels.BufferOverflow
|
||||
import kotlinx.coroutines.flow.MutableSharedFlow
|
||||
import kotlinx.coroutines.launch
|
||||
import java.io.ByteArrayInputStream
|
||||
@@ -41,7 +42,7 @@ class NewUserMetadataViewModel : ViewModel() {
|
||||
|
||||
var isUploadingImageForPicture by mutableStateOf(false)
|
||||
var isUploadingImageForBanner by mutableStateOf(false)
|
||||
val imageUploadingError = MutableSharedFlow<String?>()
|
||||
val imageUploadingError = MutableSharedFlow<String?>(0, 3, onBufferOverflow = BufferOverflow.DROP_OLDEST)
|
||||
|
||||
fun load(account: Account) {
|
||||
this.account = account
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
package com.vitorpamplona.amethyst.ui.actions
|
||||
|
||||
import android.widget.Toast
|
||||
import androidx.compose.foundation.ExperimentalFoundationApi
|
||||
import androidx.compose.foundation.combinedClickable
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
@@ -16,15 +15,14 @@ import androidx.compose.material3.Surface
|
||||
import androidx.compose.material3.Switch
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.derivedStateOf
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.window.Dialog
|
||||
import androidx.compose.ui.window.DialogProperties
|
||||
@@ -34,7 +32,6 @@ import com.vitorpamplona.amethyst.model.RelayInformation
|
||||
import com.vitorpamplona.amethyst.service.Nip11Retriever
|
||||
import com.vitorpamplona.amethyst.service.relays.Relay
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
data class RelayList(
|
||||
val relay: Relay,
|
||||
@@ -55,7 +52,6 @@ fun RelaySelectionDialog(
|
||||
accountViewModel: AccountViewModel,
|
||||
nav: (String) -> Unit
|
||||
) {
|
||||
val scope = rememberCoroutineScope()
|
||||
val context = LocalContext.current
|
||||
|
||||
var relays by remember {
|
||||
@@ -69,6 +65,13 @@ fun RelaySelectionDialog(
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
val hasSelectedRelay by remember {
|
||||
derivedStateOf {
|
||||
relays.any { it.isSelected }
|
||||
}
|
||||
}
|
||||
|
||||
var relayInfo: RelayInfoDialog? by remember { mutableStateOf(null) }
|
||||
|
||||
relayInfo?.let {
|
||||
@@ -121,21 +124,15 @@ fun RelaySelectionDialog(
|
||||
SaveButton(
|
||||
onPost = {
|
||||
val selectedRelays = relays.filter { it.isSelected }
|
||||
if (selectedRelays.isEmpty()) {
|
||||
scope.launch {
|
||||
Toast.makeText(context, context.getString(R.string.select_a_relay_to_continue), Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
return@SaveButton
|
||||
}
|
||||
onPost(selectedRelays.map { it.relay })
|
||||
onClose()
|
||||
},
|
||||
isActive = true
|
||||
isActive = hasSelectedRelay
|
||||
)
|
||||
}
|
||||
|
||||
RelaySwitch(
|
||||
text = stringResource(R.string.select_deselect_all),
|
||||
text = context.getString(R.string.select_deselect_all),
|
||||
checked = selected,
|
||||
onClick = {
|
||||
selected = !selected
|
||||
@@ -181,15 +178,10 @@ fun RelaySelectionDialog(
|
||||
Nip11Retriever.ErrorCode.FAIL_WITH_HTTP_STATUS -> context.getString(R.string.relay_information_document_error_assemble_url, url, exceptionMessage)
|
||||
}
|
||||
|
||||
scope.launch {
|
||||
Toast
|
||||
.makeText(
|
||||
context,
|
||||
msg,
|
||||
Toast.LENGTH_SHORT
|
||||
)
|
||||
.show()
|
||||
}
|
||||
accountViewModel.toast(
|
||||
context.getString(R.string.unable_to_download_relay_document),
|
||||
msg
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
@@ -2,7 +2,6 @@ package com.vitorpamplona.amethyst.ui.components
|
||||
|
||||
import android.content.Intent
|
||||
import android.net.Uri
|
||||
import android.widget.Toast
|
||||
import androidx.compose.animation.Crossfade
|
||||
import androidx.compose.foundation.border
|
||||
import androidx.compose.foundation.layout.Column
|
||||
@@ -133,26 +132,20 @@ fun CashuPreview(token: CashuToken, accountViewModel: AccountViewModel) {
|
||||
CashuProcessor().melt(
|
||||
token,
|
||||
lud16,
|
||||
onSuccess = {
|
||||
scope.launch {
|
||||
Toast.makeText(context, it, Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
onSuccess = { title, message ->
|
||||
accountViewModel.toast(title, message)
|
||||
},
|
||||
onError = {
|
||||
scope.launch {
|
||||
Toast.makeText(context, it, Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
}
|
||||
onError = { title, message ->
|
||||
accountViewModel.toast(title, message)
|
||||
},
|
||||
context
|
||||
)
|
||||
}
|
||||
} else {
|
||||
scope.launch {
|
||||
Toast.makeText(
|
||||
context,
|
||||
context.getString(R.string.no_lightning_address_set),
|
||||
Toast.LENGTH_SHORT
|
||||
).show()
|
||||
}
|
||||
accountViewModel.toast(
|
||||
context.getString(R.string.no_lightning_address_set),
|
||||
context.getString(R.string.user_x_does_not_have_a_lightning_address_setup_to_receive_sats, accountViewModel.account.userProfile().toBestDisplayName())
|
||||
)
|
||||
}
|
||||
},
|
||||
shape = QuoteBorder,
|
||||
@@ -177,11 +170,7 @@ fun CashuPreview(token: CashuToken, accountViewModel: AccountViewModel) {
|
||||
startActivity(context, intent, null)
|
||||
} else {
|
||||
// Copying the token to clipboard for now
|
||||
var orignaltoken = token.token
|
||||
clipboardManager.setText(AnnotatedString("$orignaltoken"))
|
||||
scope.launch {
|
||||
Toast.makeText(context, context.getString(R.string.copied_token_to_clipboard), Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
clipboardManager.setText(AnnotatedString(token.token))
|
||||
}
|
||||
},
|
||||
shape = QuoteBorder,
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
)
|
||||
}
|
||||
},
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -6,9 +6,7 @@ import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.RowScope
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.foundation.layout.wrapContentHeight
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.material3.Divider
|
||||
@@ -19,14 +17,12 @@ import androidx.compose.material3.NavigationBarItem
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.DisposableEffect
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.State
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.derivedStateOf
|
||||
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.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
@@ -38,14 +34,14 @@ import androidx.compose.ui.unit.Dp
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import androidx.navigation.NavBackStackEntry
|
||||
import com.vitorpamplona.amethyst.model.LocalCache
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||
import com.vitorpamplona.amethyst.ui.theme.BottomTopHeight
|
||||
import com.vitorpamplona.amethyst.ui.theme.DividerThickness
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import com.vitorpamplona.amethyst.ui.theme.Size0dp
|
||||
import com.vitorpamplona.amethyst.ui.theme.Size10dp
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
|
||||
val bottomNavigationItems = listOf(
|
||||
val bottomNavigationItems = persistentListOf(
|
||||
Route.Home,
|
||||
Route.Message,
|
||||
Route.Video,
|
||||
@@ -61,6 +57,7 @@ enum class Keyboard {
|
||||
fun keyboardAsState(): State<Keyboard> {
|
||||
val keyboardState = remember { mutableStateOf(Keyboard.Closed) }
|
||||
val view = LocalView.current
|
||||
|
||||
DisposableEffect(view) {
|
||||
val onGlobalListener = ViewTreeObserver.OnGlobalLayoutListener {
|
||||
val rect = Rect()
|
||||
@@ -106,9 +103,7 @@ private fun RenderBottomMenu(
|
||||
Divider(
|
||||
thickness = DividerThickness
|
||||
)
|
||||
NavigationBar(
|
||||
tonalElevation = 0.dp
|
||||
) {
|
||||
NavigationBar(tonalElevation = Size0dp) {
|
||||
bottomNavigationItems.forEach { item ->
|
||||
HasNewItemsIcon(item, accountViewModel, navEntryState, nav)
|
||||
}
|
||||
@@ -123,11 +118,9 @@ private fun RowScope.HasNewItemsIcon(
|
||||
navEntryState: State<NavBackStackEntry?>,
|
||||
nav: (Route, Boolean) -> Unit
|
||||
) {
|
||||
var hasNewItems by remember { mutableStateOf(false) }
|
||||
|
||||
WatchPossibleNotificationChanges(route, accountViewModel) {
|
||||
if (it != hasNewItems) {
|
||||
hasNewItems = it
|
||||
val selected by remember(navEntryState.value) {
|
||||
derivedStateOf {
|
||||
navEntryState.value?.destination?.route?.substringBefore("?") == route.base
|
||||
}
|
||||
}
|
||||
|
||||
@@ -138,76 +131,12 @@ private fun RowScope.HasNewItemsIcon(
|
||||
if ("Home" == route.base) 24.dp else 20.dp
|
||||
}
|
||||
|
||||
BottomIcon(
|
||||
icon = route.icon,
|
||||
size = size,
|
||||
iconSize = iconSize,
|
||||
base = route.base,
|
||||
hasNewItems = hasNewItems,
|
||||
navEntryState = navEntryState
|
||||
) { selected ->
|
||||
nav(route, selected)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun WatchPossibleNotificationChanges(
|
||||
route: Route,
|
||||
accountViewModel: AccountViewModel,
|
||||
onChange: (Boolean) -> Unit
|
||||
) {
|
||||
val accountState by accountViewModel.accountLiveData.observeAsState()
|
||||
val notifState by accountViewModel.accountLastReadLiveData.observeAsState()
|
||||
|
||||
LaunchedEffect(key1 = notifState, key2 = accountState) {
|
||||
launch(Dispatchers.IO) {
|
||||
onChange(route.hasNewItems(accountViewModel.account, emptySet()))
|
||||
}
|
||||
}
|
||||
|
||||
LaunchedEffect(Unit) {
|
||||
launch(Dispatchers.IO) {
|
||||
LocalCache.live.newEventBundles.collect {
|
||||
launch(Dispatchers.IO) {
|
||||
onChange(route.hasNewItems(accountViewModel.account, it))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun RowScope.BottomIcon(
|
||||
icon: Int,
|
||||
size: Dp,
|
||||
iconSize: Dp,
|
||||
base: String,
|
||||
hasNewItems: Boolean,
|
||||
navEntryState: State<NavBackStackEntry?>,
|
||||
onClick: (Boolean) -> Unit
|
||||
) {
|
||||
val selected by remember(navEntryState.value) {
|
||||
derivedStateOf {
|
||||
navEntryState.value?.destination?.route?.substringBefore("?") == base
|
||||
}
|
||||
}
|
||||
|
||||
NavigationIcon(icon, size, iconSize, selected, hasNewItems, onClick)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun RowScope.NavigationIcon(
|
||||
icon: Int,
|
||||
size: Dp,
|
||||
iconSize: Dp,
|
||||
selected: Boolean,
|
||||
hasNewItems: Boolean,
|
||||
onClick: (Boolean) -> Unit
|
||||
) {
|
||||
NavigationBarItem(
|
||||
icon = {
|
||||
val hasNewItems = accountViewModel.notificationDots.hasNewItems[route]?.collectAsState()
|
||||
|
||||
NotifiableIcon(
|
||||
icon,
|
||||
route.icon,
|
||||
size,
|
||||
iconSize,
|
||||
selected,
|
||||
@@ -215,12 +144,18 @@ private fun RowScope.NavigationIcon(
|
||||
)
|
||||
},
|
||||
selected = selected,
|
||||
onClick = { onClick(selected) }
|
||||
onClick = { nav(route, selected) }
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun NotifiableIcon(icon: Int, size: Dp, iconSize: Dp, selected: Boolean, hasNewItems: Boolean) {
|
||||
private fun NotifiableIcon(
|
||||
icon: Int,
|
||||
size: Dp,
|
||||
iconSize: Dp,
|
||||
selected: Boolean,
|
||||
hasNewItems: State<Boolean>?
|
||||
) {
|
||||
Box(remember { Modifier.size(size) }) {
|
||||
Icon(
|
||||
painter = painterResource(id = icon),
|
||||
@@ -229,37 +164,36 @@ private fun NotifiableIcon(icon: Int, size: Dp, iconSize: Dp, selected: Boolean,
|
||||
tint = if (selected) MaterialTheme.colorScheme.primary else Color.Unspecified
|
||||
)
|
||||
|
||||
if (hasNewItems) {
|
||||
Box(
|
||||
remember {
|
||||
Modifier
|
||||
.width(10.dp)
|
||||
.height(10.dp)
|
||||
.align(Alignment.TopEnd)
|
||||
}
|
||||
) {
|
||||
Box(
|
||||
modifier = remember {
|
||||
Modifier
|
||||
.width(10.dp)
|
||||
.height(10.dp)
|
||||
.clip(shape = CircleShape)
|
||||
}.background(MaterialTheme.colorScheme.primary),
|
||||
contentAlignment = Alignment.TopEnd
|
||||
) {
|
||||
Text(
|
||||
"",
|
||||
color = Color.White,
|
||||
textAlign = TextAlign.Center,
|
||||
fontSize = 12.sp,
|
||||
modifier = remember {
|
||||
Modifier
|
||||
.wrapContentHeight()
|
||||
.align(Alignment.TopEnd)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
if (hasNewItems?.value == true) {
|
||||
NotificationDotIcon(
|
||||
Modifier.align(Alignment.TopEnd)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun NotificationDotIcon(modifier: Modifier) {
|
||||
Box(modifier.size(Size10dp)) {
|
||||
Box(
|
||||
modifier = remember {
|
||||
Modifier
|
||||
.size(Size10dp)
|
||||
.clip(shape = CircleShape)
|
||||
}.background(MaterialTheme.colorScheme.primary),
|
||||
contentAlignment = Alignment.TopEnd
|
||||
) {
|
||||
Text(
|
||||
"",
|
||||
color = Color.White,
|
||||
textAlign = TextAlign.Center,
|
||||
fontSize = 12.sp,
|
||||
modifier = remember {
|
||||
Modifier
|
||||
.wrapContentHeight()
|
||||
.align(Alignment.TopEnd)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -108,6 +108,7 @@ import com.vitorpamplona.amethyst.ui.screen.loggedIn.RoomNameOnlyDisplay
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.ShortChannelHeader
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.SpinnerSelectionDialog
|
||||
import com.vitorpamplona.amethyst.ui.theme.BottomTopHeight
|
||||
import com.vitorpamplona.amethyst.ui.theme.DividerThickness
|
||||
import com.vitorpamplona.amethyst.ui.theme.DoubleHorzSpacer
|
||||
import com.vitorpamplona.amethyst.ui.theme.HalfVertSpacer
|
||||
import com.vitorpamplona.amethyst.ui.theme.HeaderPictureModifier
|
||||
@@ -376,11 +377,9 @@ fun StoriesTopBar(followLists: FollowListViewModel, drawerState: DrawerState, ac
|
||||
GenericMainTopBar(drawerState, accountViewModel, nav) { accountViewModel ->
|
||||
val list by accountViewModel.storiesListLiveData.observeAsState(GLOBAL_FOLLOWS)
|
||||
|
||||
FollowList(
|
||||
FollowListWithRoutes(
|
||||
followListsModel = followLists,
|
||||
listName = list,
|
||||
withGlobal = true,
|
||||
withRoutes = false
|
||||
listName = list
|
||||
) { listName ->
|
||||
accountViewModel.account.changeDefaultStoriesFollowList(listName.code)
|
||||
}
|
||||
@@ -392,11 +391,9 @@ fun HomeTopBar(followLists: FollowListViewModel, drawerState: DrawerState, accou
|
||||
GenericMainTopBar(drawerState, accountViewModel, nav) { accountViewModel ->
|
||||
val list by accountViewModel.homeListLiveData.observeAsState(KIND3_FOLLOWS)
|
||||
|
||||
FollowList(
|
||||
FollowListWithRoutes(
|
||||
followListsModel = followLists,
|
||||
listName = list,
|
||||
withGlobal = true,
|
||||
withRoutes = true
|
||||
listName = list
|
||||
) { listName ->
|
||||
if (listName.type == CodeNameType.ROUTE) {
|
||||
nav(listName.code)
|
||||
@@ -412,11 +409,9 @@ fun NotificationTopBar(followLists: FollowListViewModel, drawerState: DrawerStat
|
||||
GenericMainTopBar(drawerState, accountViewModel, nav) { accountViewModel ->
|
||||
val list by accountViewModel.notificationListLiveData.observeAsState(GLOBAL_FOLLOWS)
|
||||
|
||||
FollowList(
|
||||
FollowListWithoutRoutes(
|
||||
followListsModel = followLists,
|
||||
listName = list,
|
||||
withGlobal = true,
|
||||
withRoutes = false
|
||||
listName = list
|
||||
) { listName ->
|
||||
accountViewModel.account.changeDefaultNotificationFollowList(listName.code)
|
||||
}
|
||||
@@ -428,11 +423,9 @@ fun DiscoveryTopBar(followLists: FollowListViewModel, drawerState: DrawerState,
|
||||
GenericMainTopBar(drawerState, accountViewModel, nav) { accountViewModel ->
|
||||
val list by accountViewModel.discoveryListLiveData.observeAsState(GLOBAL_FOLLOWS)
|
||||
|
||||
FollowList(
|
||||
FollowListWithoutRoutes(
|
||||
followListsModel = followLists,
|
||||
listName = list,
|
||||
withGlobal = true,
|
||||
withRoutes = false
|
||||
listName = list
|
||||
) { listName ->
|
||||
accountViewModel.account.changeDefaultDiscoveryFollowList(listName.code)
|
||||
}
|
||||
@@ -491,7 +484,7 @@ fun GenericMainTopBar(
|
||||
}
|
||||
}
|
||||
)
|
||||
Divider(thickness = 0.25.dp)
|
||||
Divider(thickness = DividerThickness)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -510,7 +503,6 @@ private fun LoggedInUserPictureDrawer(
|
||||
onClick: () -> Unit
|
||||
) {
|
||||
val profilePicture by accountViewModel.account.userProfile().live().profilePictureChanges.observeAsState()
|
||||
|
||||
val pubkeyHex = remember { accountViewModel.userProfile().pubkeyHex }
|
||||
|
||||
val automaticallyShowProfilePicture = remember {
|
||||
@@ -536,42 +528,35 @@ private fun LoggedInUserPictureDrawer(
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun FollowList(
|
||||
fun FollowListWithRoutes(
|
||||
followListsModel: FollowListViewModel,
|
||||
listName: String,
|
||||
withGlobal: Boolean,
|
||||
withRoutes: Boolean,
|
||||
onChange: (CodeName) -> Unit
|
||||
) {
|
||||
val context = LocalContext.current
|
||||
|
||||
val kind3Follow = CodeName(KIND3_FOLLOWS, ResourceName(R.string.follow_list_kind3follows, context), CodeNameType.HARDCODED)
|
||||
val globalFollow = CodeName(GLOBAL_FOLLOWS, ResourceName(R.string.follow_list_global, context), CodeNameType.HARDCODED)
|
||||
|
||||
val defaultOptions = if (withGlobal) listOf(kind3Follow, globalFollow) else listOf(kind3Follow)
|
||||
|
||||
val followLists by followListsModel.peopleLists.collectAsState()
|
||||
val routeList by followListsModel.routes.collectAsState()
|
||||
|
||||
val allLists = remember(followLists) {
|
||||
if (withRoutes) {
|
||||
(defaultOptions + followLists + routeList)
|
||||
} else {
|
||||
(defaultOptions + followLists)
|
||||
}
|
||||
}
|
||||
|
||||
val followNames by remember(followLists) {
|
||||
derivedStateOf {
|
||||
allLists.map { it.name }.toImmutableList()
|
||||
}
|
||||
}
|
||||
val allLists by followListsModel.kind3GlobalPeopleRoutes.collectAsState()
|
||||
|
||||
SimpleTextSpinner(
|
||||
placeholder = allLists.firstOrNull { it.code == listName }?.name?.name() ?: "Select an Option",
|
||||
options = followNames,
|
||||
placeholderCode = listName,
|
||||
options = allLists,
|
||||
onSelect = {
|
||||
onChange(allLists.getOrNull(it) ?: kind3Follow)
|
||||
onChange(allLists.getOrNull(it) ?: followListsModel.kind3Follow)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun FollowListWithoutRoutes(
|
||||
followListsModel: FollowListViewModel,
|
||||
listName: String,
|
||||
onChange: (CodeName) -> Unit
|
||||
) {
|
||||
val allLists by followListsModel.kind3GlobalPeople.collectAsState()
|
||||
|
||||
SimpleTextSpinner(
|
||||
placeholderCode = listName,
|
||||
options = allLists,
|
||||
onSelect = {
|
||||
onChange(allLists.getOrNull(it) ?: followListsModel.kind3Follow)
|
||||
}
|
||||
)
|
||||
}
|
||||
@@ -582,6 +567,7 @@ enum class CodeNameType {
|
||||
|
||||
abstract class Name {
|
||||
abstract fun name(): String
|
||||
open fun name(context: Context) = name()
|
||||
}
|
||||
|
||||
class GeoHashName(val geoHashTag: String) : Name() {
|
||||
@@ -590,12 +576,13 @@ class GeoHashName(val geoHashTag: String) : Name() {
|
||||
class HashtagName(val hashTag: String) : Name() {
|
||||
override fun name() = "#$hashTag"
|
||||
}
|
||||
class ResourceName(val resourceId: Int, val context: Context) : Name() {
|
||||
override fun name() = context.getString(resourceId)
|
||||
class ResourceName(val resourceId: Int) : Name() {
|
||||
override fun name() = " $resourceId " // Space to make sure it goes first
|
||||
override fun name(context: Context) = context.getString(resourceId)
|
||||
}
|
||||
|
||||
class PeopleListName(val note: AddressableNote) : Name() {
|
||||
override fun name() = note.dTag() ?: ""
|
||||
override fun name() = (note.event as? PeopleListEvent)?.nameOrTitle() ?: note.dTag() ?: ""
|
||||
}
|
||||
class CommunityName(val note: AddressableNote) : Name() {
|
||||
override fun name() = "/n/${(note.dTag() ?: "")}"
|
||||
@@ -606,11 +593,14 @@ data class CodeName(val code: String, val name: Name, val type: CodeNameType)
|
||||
|
||||
@Stable
|
||||
class FollowListViewModel(val account: Account) : ViewModel() {
|
||||
private var _peopleLists = MutableStateFlow<ImmutableList<CodeName>>(emptyList<CodeName>().toPersistentList())
|
||||
val peopleLists = _peopleLists.asStateFlow()
|
||||
val kind3Follow = CodeName(KIND3_FOLLOWS, ResourceName(R.string.follow_list_kind3follows), CodeNameType.HARDCODED)
|
||||
val globalFollow = CodeName(GLOBAL_FOLLOWS, ResourceName(R.string.follow_list_global), CodeNameType.HARDCODED)
|
||||
|
||||
private var _routes = MutableStateFlow<ImmutableList<CodeName>>(emptyList<CodeName>().toPersistentList())
|
||||
val routes = _routes.asStateFlow()
|
||||
private var _kind3GlobalPeopleRoutes = MutableStateFlow<ImmutableList<CodeName>>(emptyList<CodeName>().toPersistentList())
|
||||
val kind3GlobalPeopleRoutes = _kind3GlobalPeopleRoutes.asStateFlow()
|
||||
|
||||
private var _kind3GlobalPeople = MutableStateFlow<ImmutableList<CodeName>>(emptyList<CodeName>().toPersistentList())
|
||||
val kind3GlobalPeople = _kind3GlobalPeople.asStateFlow()
|
||||
|
||||
fun refresh() {
|
||||
viewModelScope.launch(Dispatchers.Default) {
|
||||
@@ -632,11 +622,7 @@ class FollowListViewModel(val account: Account) : ViewModel() {
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}.sortedBy { it.name.name() }.toImmutableList()
|
||||
|
||||
if (!equalImmutableLists(_peopleLists.value, newFollowLists)) {
|
||||
_peopleLists.emit(newFollowLists)
|
||||
}
|
||||
}.sortedBy { it.name.name() }
|
||||
|
||||
val communities = account.userProfile().cachedFollowingCommunitiesSet().mapNotNull {
|
||||
LocalCache.checkGetOrCreateAddressableNote(it)?.let { communityNote ->
|
||||
@@ -652,10 +638,18 @@ class FollowListViewModel(val account: Account) : ViewModel() {
|
||||
CodeName("Geohash/$it", GeoHashName(it), CodeNameType.ROUTE)
|
||||
}
|
||||
|
||||
val routeList = (communities + hashtags + geotags).sortedBy { it.name.name() }.toImmutableList()
|
||||
val routeList = (communities + hashtags + geotags).sortedBy { it.name.name() }
|
||||
|
||||
if (!equalImmutableLists(_routes.value, routeList)) {
|
||||
_routes.emit(routeList)
|
||||
val kind3GlobalPeopleRouteList = listOf(listOf(kind3Follow, globalFollow), newFollowLists, routeList).flatten().toImmutableList()
|
||||
|
||||
if (!equalImmutableLists(_kind3GlobalPeopleRoutes.value, kind3GlobalPeopleRouteList)) {
|
||||
_kind3GlobalPeopleRoutes.emit(kind3GlobalPeopleRouteList)
|
||||
}
|
||||
|
||||
val kind3GlobalPeopleList = listOf(listOf(kind3Follow, globalFollow), newFollowLists).flatten().toImmutableList()
|
||||
|
||||
if (!equalImmutableLists(_kind3GlobalPeople.value, kind3GlobalPeopleList)) {
|
||||
_kind3GlobalPeople.emit(kind3GlobalPeopleList)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -691,14 +685,24 @@ class FollowListViewModel(val account: Account) : ViewModel() {
|
||||
|
||||
@Composable
|
||||
fun SimpleTextSpinner(
|
||||
placeholder: String,
|
||||
options: ImmutableList<Name>,
|
||||
placeholderCode: String,
|
||||
options: ImmutableList<CodeName>,
|
||||
onSelect: (Int) -> Unit,
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
val interactionSource = remember { MutableInteractionSource() }
|
||||
var optionsShowing by remember { mutableStateOf(false) }
|
||||
var currentText by remember(placeholder) { mutableStateOf(placeholder) }
|
||||
|
||||
val context = LocalContext.current
|
||||
val selectAnOption = stringResource(
|
||||
id = R.string.select_an_option
|
||||
)
|
||||
|
||||
var currentText by remember {
|
||||
mutableStateOf(
|
||||
options.firstOrNull { it.code == placeholderCode }?.name?.name(context) ?: selectAnOption
|
||||
)
|
||||
}
|
||||
|
||||
Box(
|
||||
modifier = modifier,
|
||||
@@ -706,7 +710,7 @@ fun SimpleTextSpinner(
|
||||
) {
|
||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||
Spacer(modifier = Modifier.size(20.dp))
|
||||
Text(placeholder)
|
||||
Text(currentText)
|
||||
Icon(
|
||||
imageVector = Icons.Default.ExpandMore,
|
||||
null,
|
||||
@@ -732,22 +736,22 @@ fun SimpleTextSpinner(
|
||||
options = options,
|
||||
onDismiss = { optionsShowing = false },
|
||||
onSelect = {
|
||||
currentText = options[it].name()
|
||||
currentText = options[it].name.name(context)
|
||||
optionsShowing = false
|
||||
onSelect(it)
|
||||
}
|
||||
) {
|
||||
RenderOption(it)
|
||||
RenderOption(it.name)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun RenderOption(it: Name) {
|
||||
when (it) {
|
||||
fun RenderOption(option: Name) {
|
||||
when (option) {
|
||||
is GeoHashName -> {
|
||||
LoadCityName(it.geoHashTag) {
|
||||
LoadCityName(option.geoHashTag) {
|
||||
Row(
|
||||
horizontalArrangement = Arrangement.Center,
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
@@ -761,7 +765,7 @@ fun RenderOption(it: Name) {
|
||||
horizontalArrangement = Arrangement.Center,
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
) {
|
||||
Text(text = it.name(), color = MaterialTheme.colorScheme.onSurface)
|
||||
Text(text = option.name(), color = MaterialTheme.colorScheme.onSurface)
|
||||
}
|
||||
}
|
||||
is ResourceName -> {
|
||||
@@ -769,7 +773,7 @@ fun RenderOption(it: Name) {
|
||||
horizontalArrangement = Arrangement.Center,
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
) {
|
||||
Text(text = stringResource(id = it.resourceId), color = MaterialTheme.colorScheme.onSurface)
|
||||
Text(text = stringResource(id = option.resourceId), color = MaterialTheme.colorScheme.onSurface)
|
||||
}
|
||||
}
|
||||
is PeopleListName -> {
|
||||
@@ -777,7 +781,7 @@ fun RenderOption(it: Name) {
|
||||
horizontalArrangement = Arrangement.Center,
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
) {
|
||||
Text(text = it.name(), color = MaterialTheme.colorScheme.onSurface)
|
||||
Text(text = option.name(), color = MaterialTheme.colorScheme.onSurface)
|
||||
}
|
||||
}
|
||||
is CommunityName -> {
|
||||
@@ -785,7 +789,7 @@ fun RenderOption(it: Name) {
|
||||
horizontalArrangement = Arrangement.Center,
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
) {
|
||||
val name by it.note.live().metadata.map {
|
||||
val name by option.note.live().metadata.map {
|
||||
"/n/" + (it.note as? AddressableNote)?.dTag()
|
||||
}.observeAsState()
|
||||
|
||||
|
||||
@@ -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
|
||||
)
|
||||
}
|
||||
|
||||
@@ -38,7 +38,6 @@ import com.vitorpamplona.amethyst.ui.screen.BadgeCard
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||
import com.vitorpamplona.amethyst.ui.theme.newItemBackgroundColor
|
||||
import com.vitorpamplona.amethyst.ui.theme.placeholderText
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
@OptIn(ExperimentalFoundationApi::class)
|
||||
@@ -64,11 +63,7 @@ fun BadgeCompose(likeSetCard: BadgeCard, isInnerNote: Boolean = false, routeForL
|
||||
val newItemColor = MaterialTheme.colorScheme.newItemBackgroundColor
|
||||
|
||||
LaunchedEffect(key1 = likeSetCard) {
|
||||
scope.launch(Dispatchers.IO) {
|
||||
val isNew = likeSetCard.createdAt() > accountViewModel.account.loadLastRead(routeForLastRead)
|
||||
|
||||
accountViewModel.account.markAsRead(routeForLastRead, likeSetCard.createdAt())
|
||||
|
||||
accountViewModel.loadAndMarkAsRead(routeForLastRead, likeSetCard.createdAt()) { isNew ->
|
||||
val newBackgroundColor = if (isNew) {
|
||||
newItemColor.compositeOver(defaultBackgroundColor)
|
||||
} else {
|
||||
|
||||
@@ -61,6 +61,7 @@ import com.vitorpamplona.amethyst.ui.screen.loggedIn.OfflineFlag
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.ScheduledFlag
|
||||
import com.vitorpamplona.amethyst.ui.theme.DividerThickness
|
||||
import com.vitorpamplona.amethyst.ui.theme.DoubleHorzSpacer
|
||||
import com.vitorpamplona.amethyst.ui.theme.DoubleVertSpacer
|
||||
import com.vitorpamplona.amethyst.ui.theme.QuoteBorder
|
||||
import com.vitorpamplona.amethyst.ui.theme.Size35dp
|
||||
import com.vitorpamplona.amethyst.ui.theme.StdHorzSpacer
|
||||
@@ -277,37 +278,24 @@ private fun CheckNewAndRenderChannelCard(
|
||||
) {
|
||||
val newItemColor = MaterialTheme.colorScheme.newItemBackgroundColor
|
||||
val defaultBackgroundColor = MaterialTheme.colorScheme.background
|
||||
val backgroundColor = remember { mutableStateOf<Color>(defaultBackgroundColor) }
|
||||
val backgroundColor = remember {
|
||||
mutableStateOf<Color>(
|
||||
parentBackgroundColor?.value ?: defaultBackgroundColor
|
||||
)
|
||||
}
|
||||
|
||||
LaunchedEffect(key1 = routeForLastRead, key2 = parentBackgroundColor?.value) {
|
||||
launch(Dispatchers.IO) {
|
||||
routeForLastRead?.let {
|
||||
val lastTime = accountViewModel.account.loadLastRead(it)
|
||||
|
||||
val createdAt = baseNote.createdAt()
|
||||
if (createdAt != null) {
|
||||
accountViewModel.account.markAsRead(it, createdAt)
|
||||
|
||||
val isNew = createdAt > lastTime
|
||||
|
||||
val newBackgroundColor = if (isNew) {
|
||||
if (parentBackgroundColor != null) {
|
||||
newItemColor.compositeOver(parentBackgroundColor.value)
|
||||
} else {
|
||||
newItemColor.compositeOver(defaultBackgroundColor)
|
||||
}
|
||||
routeForLastRead?.let {
|
||||
accountViewModel.loadAndMarkAsRead(routeForLastRead, baseNote.createdAt()) { isNew ->
|
||||
val newBackgroundColor = if (isNew) {
|
||||
if (parentBackgroundColor != null) {
|
||||
newItemColor.compositeOver(parentBackgroundColor.value)
|
||||
} else {
|
||||
parentBackgroundColor?.value ?: defaultBackgroundColor
|
||||
}
|
||||
|
||||
if (newBackgroundColor != backgroundColor.value) {
|
||||
launch(Dispatchers.Main) {
|
||||
backgroundColor.value = newBackgroundColor
|
||||
}
|
||||
newItemColor.compositeOver(defaultBackgroundColor)
|
||||
}
|
||||
} else {
|
||||
parentBackgroundColor?.value ?: defaultBackgroundColor
|
||||
}
|
||||
} ?: run {
|
||||
val newBackgroundColor = parentBackgroundColor?.value ?: defaultBackgroundColor
|
||||
|
||||
if (newBackgroundColor != backgroundColor.value) {
|
||||
launch(Dispatchers.Main) {
|
||||
@@ -315,6 +303,13 @@ private fun CheckNewAndRenderChannelCard(
|
||||
}
|
||||
}
|
||||
}
|
||||
} ?: run {
|
||||
val newBackgroundColor = parentBackgroundColor?.value ?: defaultBackgroundColor
|
||||
if (newBackgroundColor != backgroundColor.value) {
|
||||
launch(Dispatchers.Main) {
|
||||
backgroundColor.value = newBackgroundColor
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -510,6 +505,8 @@ fun RenderLiveActivityThumb(
|
||||
}
|
||||
}
|
||||
|
||||
Spacer(modifier = DoubleVertSpacer)
|
||||
|
||||
ChannelHeader(
|
||||
channelHex = remember { baseNote.idHex },
|
||||
showVideo = false,
|
||||
@@ -716,7 +713,9 @@ fun RenderChannelThumb(baseNote: Note, channel: Channel, accountViewModel: Accou
|
||||
Spacer(modifier = DoubleHorzSpacer)
|
||||
|
||||
Column(
|
||||
modifier = Modifier.fillMaxWidth().fillMaxHeight()
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.fillMaxHeight()
|
||||
) {
|
||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||
Text(
|
||||
|
||||
@@ -415,9 +415,7 @@ private fun WatchNotificationChanges(
|
||||
accountViewModel: AccountViewModel,
|
||||
onNewStatus: (Boolean) -> Unit
|
||||
) {
|
||||
val cacheState by accountViewModel.accountLastReadLiveData.observeAsState()
|
||||
|
||||
LaunchedEffect(key1 = note, cacheState) {
|
||||
LaunchedEffect(key1 = note, accountViewModel.accountMarkAsReadUpdates.value) {
|
||||
launch(Dispatchers.IO) {
|
||||
note.event?.createdAt()?.let {
|
||||
val lastTime = accountViewModel.account.loadLastRead(route)
|
||||
|
||||
@@ -73,8 +73,6 @@ import com.vitorpamplona.quartz.events.EmptyTagList
|
||||
import com.vitorpamplona.quartz.events.ImmutableListOfLists
|
||||
import com.vitorpamplona.quartz.events.PrivateDmEvent
|
||||
import com.vitorpamplona.quartz.events.toImmutableListOfLists
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
@OptIn(ExperimentalFoundationApi::class)
|
||||
@Composable
|
||||
@@ -260,12 +258,7 @@ fun NormalChatNote(
|
||||
|
||||
if (routeForLastRead != null) {
|
||||
LaunchedEffect(key1 = routeForLastRead) {
|
||||
launch(Dispatchers.IO) {
|
||||
val createdAt = note.createdAt()
|
||||
if (createdAt != null) {
|
||||
accountViewModel.account.markAsRead(routeForLastRead, createdAt)
|
||||
}
|
||||
}
|
||||
accountViewModel.loadAndMarkAsRead(routeForLastRead, note.createdAt()) { }
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -28,7 +28,6 @@ import com.vitorpamplona.amethyst.ui.screen.MessageSetCard
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||
import com.vitorpamplona.amethyst.ui.theme.DividerThickness
|
||||
import com.vitorpamplona.amethyst.ui.theme.newItemBackgroundColor
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
@OptIn(ExperimentalFoundationApi::class)
|
||||
@@ -48,11 +47,7 @@ fun MessageSetCompose(messageSetCard: MessageSetCard, routeForLastRead: String,
|
||||
val newItemColor = MaterialTheme.colorScheme.newItemBackgroundColor
|
||||
|
||||
LaunchedEffect(key1 = messageSetCard) {
|
||||
launch(Dispatchers.IO) {
|
||||
val isNew = messageSetCard.createdAt() > accountViewModel.account.loadLastRead(routeForLastRead)
|
||||
|
||||
accountViewModel.account.markAsRead(routeForLastRead, messageSetCard.createdAt())
|
||||
|
||||
accountViewModel.loadAndMarkAsRead(routeForLastRead, messageSetCard.createdAt()) { isNew ->
|
||||
val newBackgroundColor = if (isNew) {
|
||||
newItemColor.compositeOver(defaultBackgroundColor)
|
||||
} else {
|
||||
|
||||
@@ -96,11 +96,7 @@ fun MultiSetCompose(multiSetCard: MultiSetCard, routeForLastRead: String, showHi
|
||||
val newItemColor = MaterialTheme.colorScheme.newItemBackgroundColor
|
||||
|
||||
LaunchedEffect(key1 = multiSetCard) {
|
||||
launch(Dispatchers.IO) {
|
||||
val isNew = multiSetCard.maxCreatedAt > accountViewModel.account.loadLastRead(routeForLastRead)
|
||||
|
||||
accountViewModel.account.markAsRead(routeForLastRead, multiSetCard.maxCreatedAt)
|
||||
|
||||
accountViewModel.loadAndMarkAsRead(routeForLastRead, multiSetCard.maxCreatedAt) { isNew ->
|
||||
val newBackgroundColor = if (isNew) {
|
||||
newItemColor.compositeOver(defaultBackgroundColor)
|
||||
} else {
|
||||
|
||||
@@ -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
|
||||
@@ -837,7 +837,7 @@ private fun CheckNewAndRenderNote(
|
||||
) {
|
||||
val newItemColor = MaterialTheme.colorScheme.newItemBackgroundColor
|
||||
val defaultBackgroundColor = MaterialTheme.colorScheme.background
|
||||
val backgroundColor = remember { mutableStateOf<Color>(defaultBackgroundColor) }
|
||||
val backgroundColor = remember(baseNote) { mutableStateOf<Color>(parentBackgroundColor?.value ?: defaultBackgroundColor) }
|
||||
|
||||
LaunchedEffect(key1 = routeForLastRead, key2 = parentBackgroundColor?.value) {
|
||||
routeForLastRead?.let {
|
||||
@@ -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}"
|
||||
}
|
||||
@@ -3639,8 +3643,10 @@ fun RenderLiveActivityEventInner(baseNote: Note, accountViewModel: AccountViewMo
|
||||
var isOnline by remember { mutableStateOf(false) }
|
||||
|
||||
LaunchedEffect(key1 = media) {
|
||||
launch(Dispatchers.IO) {
|
||||
isOnline = OnlineChecker.isOnline(media)
|
||||
accountViewModel.checkIsOnline(media) { newIsOnline ->
|
||||
if (isOnline != newIsOnline) {
|
||||
isOnline = newIsOnline
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -291,20 +291,16 @@ private fun RenderMainPopup(
|
||||
Icons.Default.PersonRemove,
|
||||
stringResource(R.string.quick_action_unfollow)
|
||||
) {
|
||||
scope.launch(Dispatchers.IO) {
|
||||
accountViewModel.unfollow(note.author!!)
|
||||
onDismiss()
|
||||
}
|
||||
accountViewModel.unfollow(note.author!!)
|
||||
onDismiss()
|
||||
}
|
||||
} else {
|
||||
NoteQuickActionItem(
|
||||
Icons.Default.PersonAdd,
|
||||
stringResource(R.string.quick_action_follow)
|
||||
) {
|
||||
scope.launch(Dispatchers.IO) {
|
||||
accountViewModel.follow(note.author!!)
|
||||
onDismiss()
|
||||
}
|
||||
accountViewModel.follow(note.author!!)
|
||||
onDismiss()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
)
|
||||
.show()
|
||||
}
|
||||
accountViewModel.toast(
|
||||
R.string.read_only_user,
|
||||
R.string.login_with_a_private_key_to_be_able_to_send_zaps
|
||||
)
|
||||
} else if (pollViewModel.isPollClosed()) {
|
||||
scope.launch {
|
||||
Toast
|
||||
.makeText(
|
||||
context,
|
||||
context.getString(R.string.poll_is_closed),
|
||||
Toast.LENGTH_SHORT
|
||||
)
|
||||
.show()
|
||||
}
|
||||
accountViewModel.toast(
|
||||
R.string.poll_unable_to_vote,
|
||||
R.string.poll_is_closed_explainer
|
||||
)
|
||||
} else if (isLoggedUser) {
|
||||
scope.launch {
|
||||
Toast
|
||||
.makeText(
|
||||
context,
|
||||
context.getString(R.string.poll_author_no_vote),
|
||||
Toast.LENGTH_SHORT
|
||||
)
|
||||
.show()
|
||||
}
|
||||
accountViewModel.toast(
|
||||
R.string.poll_unable_to_vote,
|
||||
R.string.poll_author_no_vote
|
||||
)
|
||||
} else if (pollViewModel.isVoteAmountAtomic() && poolOption.zappedByLoggedIn) {
|
||||
// only allow one vote per option when min==max, i.e. atomic vote amount specified
|
||||
scope.launch {
|
||||
Toast
|
||||
.makeText(
|
||||
context,
|
||||
R.string.one_vote_per_user_on_atomic_votes,
|
||||
Toast.LENGTH_SHORT
|
||||
)
|
||||
.show()
|
||||
}
|
||||
accountViewModel.toast(
|
||||
R.string.poll_unable_to_vote,
|
||||
R.string.one_vote_per_user_on_atomic_votes
|
||||
)
|
||||
return@combinedClickable
|
||||
} else if (accountViewModel.account.zapAmountChoices.size == 1 &&
|
||||
pollViewModel.isValidInputVoteAmount(accountViewModel.account.zapAmountChoices.first())
|
||||
@@ -359,13 +341,9 @@ fun ZapVote(
|
||||
poolOption.option,
|
||||
"",
|
||||
context,
|
||||
onError = {
|
||||
scope.launch {
|
||||
zappingProgress = 0f
|
||||
Toast
|
||||
.makeText(context, it, Toast.LENGTH_SHORT)
|
||||
.show()
|
||||
}
|
||||
onError = { title, message ->
|
||||
zappingProgress = 0f
|
||||
showErrorMessageDialog = StringToastMsg(title, message)
|
||||
},
|
||||
onProgress = {
|
||||
scope.launch(Dispatchers.Main) {
|
||||
@@ -395,11 +373,9 @@ fun ZapVote(
|
||||
onChangeAmount = {
|
||||
wantsToZap = false
|
||||
},
|
||||
onError = {
|
||||
scope.launch {
|
||||
zappingProgress = 0f
|
||||
showErrorMessageDialog = it
|
||||
}
|
||||
onError = { title, message ->
|
||||
showErrorMessageDialog = StringToastMsg(title, message)
|
||||
zappingProgress = 0f
|
||||
},
|
||||
onProgress = {
|
||||
scope.launch(Dispatchers.Main) {
|
||||
@@ -423,19 +399,22 @@ fun ZapVote(
|
||||
wantsToPay = persistentListOf()
|
||||
scope.launch {
|
||||
zappingProgress = 0f
|
||||
showErrorMessageDialog = it
|
||||
showErrorMessageDialog = StringToastMsg(
|
||||
context.getString(R.string.error_dialog_zap_error),
|
||||
it
|
||||
)
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
if (showErrorMessageDialog != null) {
|
||||
showErrorMessageDialog?.let { toast ->
|
||||
ErrorMessageDialog(
|
||||
title = stringResource(id = R.string.error_dialog_zap_error),
|
||||
textContent = showErrorMessageDialog ?: "",
|
||||
title = toast.title,
|
||||
textContent = toast.msg,
|
||||
onClickStartMessage = {
|
||||
baseNote.author?.let {
|
||||
nav(routeToMessage(it, showErrorMessageDialog, accountViewModel))
|
||||
nav(routeToMessage(it, toast.msg, accountViewModel))
|
||||
}
|
||||
},
|
||||
onDismiss = { showErrorMessageDialog = null }
|
||||
@@ -494,7 +473,7 @@ fun FilteredZapAmountChoicePopup(
|
||||
pollOption: Int,
|
||||
onDismiss: () -> Unit,
|
||||
onChangeAmount: () -> Unit,
|
||||
onError: (text: String) -> Unit,
|
||||
onError: (title: String, text: String) -> Unit,
|
||||
onProgress: (percent: Float) -> Unit,
|
||||
onPayViaIntent: (ImmutableList<ZapPaymentHandler.Payable>) -> Unit
|
||||
) {
|
||||
|
||||
@@ -2,7 +2,6 @@ package com.vitorpamplona.amethyst.ui.note
|
||||
|
||||
import android.content.Context
|
||||
import android.util.Log
|
||||
import android.widget.Toast
|
||||
import androidx.compose.animation.AnimatedContent
|
||||
import androidx.compose.animation.AnimatedContentTransitionScope
|
||||
import androidx.compose.animation.ContentTransform
|
||||
@@ -110,7 +109,6 @@ import kotlinx.collections.immutable.ImmutableList
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
import kotlinx.collections.immutable.toImmutableMap
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import java.math.BigDecimal
|
||||
@@ -540,9 +538,6 @@ fun ReplyReaction(
|
||||
iconSize: Dp = Size17dp,
|
||||
onPress: () -> Unit
|
||||
) {
|
||||
val context = LocalContext.current
|
||||
val scope = rememberCoroutineScope()
|
||||
|
||||
IconButton(
|
||||
modifier = remember {
|
||||
Modifier.size(iconSize)
|
||||
@@ -554,13 +549,10 @@ fun ReplyReaction(
|
||||
if (accountViewModel.loggedInWithExternalSigner()) {
|
||||
onPress()
|
||||
} else {
|
||||
scope.launch {
|
||||
Toast.makeText(
|
||||
context,
|
||||
context.getString(R.string.login_with_a_private_key_to_be_able_to_reply),
|
||||
Toast.LENGTH_SHORT
|
||||
).show()
|
||||
}
|
||||
accountViewModel.toast(
|
||||
R.string.read_only_user,
|
||||
R.string.login_with_a_private_key_to_be_able_to_reply
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -643,9 +635,6 @@ fun BoostReaction(
|
||||
iconSize: Dp = 20.dp,
|
||||
onQuotePress: () -> Unit
|
||||
) {
|
||||
val context = LocalContext.current
|
||||
val scope = rememberCoroutineScope()
|
||||
|
||||
var wantsToBoost by remember { mutableStateOf(false) }
|
||||
|
||||
val iconButtonModifier = remember {
|
||||
@@ -657,29 +646,22 @@ fun BoostReaction(
|
||||
onClick = {
|
||||
if (accountViewModel.isWriteable()) {
|
||||
if (accountViewModel.hasBoosted(baseNote)) {
|
||||
scope.launch(Dispatchers.IO) {
|
||||
accountViewModel.deleteBoostsTo(baseNote)
|
||||
}
|
||||
accountViewModel.deleteBoostsTo(baseNote)
|
||||
} else {
|
||||
wantsToBoost = true
|
||||
}
|
||||
} else {
|
||||
if (accountViewModel.loggedInWithExternalSigner()) {
|
||||
if (accountViewModel.hasBoosted(baseNote)) {
|
||||
scope.launch(Dispatchers.IO) {
|
||||
accountViewModel.deleteBoostsTo(baseNote)
|
||||
}
|
||||
accountViewModel.deleteBoostsTo(baseNote)
|
||||
} else {
|
||||
wantsToBoost = true
|
||||
}
|
||||
} else {
|
||||
scope.launch {
|
||||
Toast.makeText(
|
||||
context,
|
||||
context.getString(R.string.login_with_a_private_key_to_be_able_to_boost_posts),
|
||||
Toast.LENGTH_SHORT
|
||||
).show()
|
||||
}
|
||||
accountViewModel.toast(
|
||||
R.string.read_only_user,
|
||||
R.string.login_with_a_private_key_to_be_able_to_boost_posts
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -699,9 +681,7 @@ fun BoostReaction(
|
||||
onQuotePress()
|
||||
},
|
||||
onRepost = {
|
||||
scope.launch(Dispatchers.IO) {
|
||||
accountViewModel.boost(baseNote)
|
||||
}
|
||||
accountViewModel.boost(baseNote)
|
||||
}
|
||||
)
|
||||
}
|
||||
@@ -741,9 +721,6 @@ fun LikeReaction(
|
||||
heartSize: Dp = 16.dp,
|
||||
iconFontSize: TextUnit = Font14SP
|
||||
) {
|
||||
val context = LocalContext.current
|
||||
val scope = rememberCoroutineScope()
|
||||
|
||||
val iconButtonModifier = remember {
|
||||
Modifier.size(iconSize)
|
||||
}
|
||||
@@ -761,21 +738,12 @@ fun LikeReaction(
|
||||
likeClick(
|
||||
baseNote,
|
||||
accountViewModel,
|
||||
scope,
|
||||
context,
|
||||
onMultipleChoices = {
|
||||
wantsToReact = true
|
||||
},
|
||||
onWantsToSignReaction = {
|
||||
if (accountViewModel.account.reactionChoices.size == 1) {
|
||||
scope.launch(Dispatchers.IO) {
|
||||
val reaction = accountViewModel.account.reactionChoices.first()
|
||||
if (accountViewModel.hasReactedTo(baseNote, reaction)) {
|
||||
accountViewModel.deleteReactionTo(baseNote, reaction)
|
||||
} else {
|
||||
accountViewModel.reactTo(baseNote, reaction)
|
||||
}
|
||||
}
|
||||
accountViewModel.reactToOrDelete(baseNote)
|
||||
} else if (accountViewModel.account.reactionChoices.size > 1) {
|
||||
wantsToReact = true
|
||||
}
|
||||
@@ -896,44 +864,25 @@ fun LikeText(baseNote: Note, grayTint: Color) {
|
||||
private fun likeClick(
|
||||
baseNote: Note,
|
||||
accountViewModel: AccountViewModel,
|
||||
scope: CoroutineScope,
|
||||
context: Context,
|
||||
onMultipleChoices: () -> Unit,
|
||||
onWantsToSignReaction: () -> Unit
|
||||
) {
|
||||
if (accountViewModel.account.reactionChoices.isEmpty()) {
|
||||
scope.launch {
|
||||
Toast
|
||||
.makeText(
|
||||
context,
|
||||
context.getString(R.string.no_reaction_type_setup_long_press_to_change),
|
||||
Toast.LENGTH_SHORT
|
||||
)
|
||||
.show()
|
||||
}
|
||||
accountViewModel.toast(
|
||||
R.string.no_reactions_setup,
|
||||
R.string.no_reaction_type_setup_long_press_to_change
|
||||
)
|
||||
} else if (!accountViewModel.isWriteable()) {
|
||||
if (accountViewModel.loggedInWithExternalSigner()) {
|
||||
onWantsToSignReaction()
|
||||
} else {
|
||||
scope.launch {
|
||||
Toast
|
||||
.makeText(
|
||||
context,
|
||||
context.getString(R.string.login_with_a_private_key_to_like_posts),
|
||||
Toast.LENGTH_SHORT
|
||||
)
|
||||
.show()
|
||||
}
|
||||
accountViewModel.toast(
|
||||
R.string.read_only_user,
|
||||
R.string.login_with_a_private_key_to_like_posts
|
||||
)
|
||||
}
|
||||
} else if (accountViewModel.account.reactionChoices.size == 1) {
|
||||
scope.launch(Dispatchers.IO) {
|
||||
val reaction = accountViewModel.account.reactionChoices.first()
|
||||
if (accountViewModel.hasReactedTo(baseNote, reaction)) {
|
||||
accountViewModel.deleteReactionTo(baseNote, reaction)
|
||||
} else {
|
||||
accountViewModel.reactTo(baseNote, reaction)
|
||||
}
|
||||
}
|
||||
accountViewModel.reactToOrDelete(baseNote)
|
||||
} else if (accountViewModel.account.reactionChoices.size > 1) {
|
||||
onMultipleChoices()
|
||||
}
|
||||
@@ -976,18 +925,19 @@ fun ZapReaction(
|
||||
zapClick(
|
||||
baseNote,
|
||||
accountViewModel,
|
||||
scope,
|
||||
context,
|
||||
onZappingProgress = { progress: Float ->
|
||||
zappingProgress = progress
|
||||
scope.launch {
|
||||
zappingProgress = progress
|
||||
}
|
||||
},
|
||||
onMultipleChoices = {
|
||||
wantsToZap = true
|
||||
},
|
||||
onError = {
|
||||
onError = { title, message ->
|
||||
scope.launch {
|
||||
zappingProgress = 0f
|
||||
showErrorMessageDialog = it
|
||||
showErrorMessageDialog = message
|
||||
}
|
||||
},
|
||||
onPayViaIntent = {
|
||||
@@ -1015,10 +965,10 @@ fun ZapReaction(
|
||||
wantsToZap = false
|
||||
wantsToChangeZapAmount = true
|
||||
},
|
||||
onError = {
|
||||
onError = { title, message ->
|
||||
scope.launch {
|
||||
zappingProgress = 0f
|
||||
showErrorMessageDialog = it
|
||||
showErrorMessageDialog = message
|
||||
}
|
||||
},
|
||||
onProgress = {
|
||||
@@ -1075,10 +1025,10 @@ fun ZapReaction(
|
||||
if (wantsToSetCustomZap) {
|
||||
ZapCustomDialog(
|
||||
onClose = { wantsToSetCustomZap = false },
|
||||
onError = {
|
||||
onError = { title, message ->
|
||||
scope.launch {
|
||||
zappingProgress = 0f
|
||||
showErrorMessageDialog = it
|
||||
showErrorMessageDialog = message
|
||||
}
|
||||
},
|
||||
onProgress = {
|
||||
@@ -1121,33 +1071,22 @@ fun ZapReaction(
|
||||
private fun zapClick(
|
||||
baseNote: Note,
|
||||
accountViewModel: AccountViewModel,
|
||||
scope: CoroutineScope,
|
||||
context: Context,
|
||||
onZappingProgress: (Float) -> Unit,
|
||||
onMultipleChoices: () -> Unit,
|
||||
onError: (String) -> Unit,
|
||||
onError: (String, String) -> Unit,
|
||||
onPayViaIntent: (ImmutableList<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
|
||||
)
|
||||
.show()
|
||||
}
|
||||
accountViewModel.toast(
|
||||
context.getString(R.string.error_dialog_zap_error),
|
||||
context.getString(R.string.no_zap_amount_setup_long_press_to_change)
|
||||
)
|
||||
} else if (!accountViewModel.isWriteable() && !accountViewModel.loggedInWithExternalSigner()) {
|
||||
scope.launch {
|
||||
Toast
|
||||
.makeText(
|
||||
context,
|
||||
context.getString(R.string.login_with_a_private_key_to_be_able_to_send_zaps),
|
||||
Toast.LENGTH_SHORT
|
||||
)
|
||||
.show()
|
||||
}
|
||||
accountViewModel.toast(
|
||||
context.getString(R.string.error_dialog_zap_error),
|
||||
context.getString(R.string.login_with_a_private_key_to_be_able_to_send_zaps)
|
||||
)
|
||||
} else if (accountViewModel.account.zapAmountChoices.size == 1) {
|
||||
accountViewModel.zap(
|
||||
baseNote,
|
||||
@@ -1157,9 +1096,7 @@ private fun zapClick(
|
||||
context,
|
||||
onError = onError,
|
||||
onProgress = {
|
||||
scope.launch(Dispatchers.Main) {
|
||||
onZappingProgress(it)
|
||||
}
|
||||
onZappingProgress(it)
|
||||
},
|
||||
zapType = accountViewModel.account.defaultZapType,
|
||||
onPayViaIntent = onPayViaIntent
|
||||
@@ -1270,7 +1207,7 @@ private fun DrawViewCount(
|
||||
.memoryCachePolicy(CachePolicy.ENABLED)
|
||||
.build()
|
||||
},
|
||||
contentDescription = stringResource(R.string.view_count),
|
||||
contentDescription = context.getString(R.string.view_count),
|
||||
modifier = iconModifier,
|
||||
colorFilter = viewCountColorFilter
|
||||
)
|
||||
@@ -1289,15 +1226,12 @@ private fun BoostTypeChoicePopup(baseNote: Note, iconSize: Dp, accountViewModel:
|
||||
onDismissRequest = { onDismiss() }
|
||||
) {
|
||||
FlowRow {
|
||||
val scope = rememberCoroutineScope()
|
||||
Button(
|
||||
modifier = Modifier.padding(horizontal = 3.dp),
|
||||
onClick = {
|
||||
if (accountViewModel.isWriteable()) {
|
||||
scope.launch(Dispatchers.IO) {
|
||||
accountViewModel.boost(baseNote)
|
||||
onDismiss()
|
||||
}
|
||||
accountViewModel.boost(baseNote)
|
||||
onDismiss()
|
||||
} else {
|
||||
onRepost()
|
||||
onDismiss()
|
||||
@@ -1470,12 +1404,11 @@ fun ZapAmountChoicePopup(
|
||||
accountViewModel: AccountViewModel,
|
||||
onDismiss: () -> Unit,
|
||||
onChangeAmount: () -> Unit,
|
||||
onError: (text: String) -> Unit,
|
||||
onError: (title: String, text: String) -> Unit,
|
||||
onProgress: (percent: Float) -> Unit,
|
||||
onPayViaIntent: (ImmutableList<ZapPaymentHandler.Payable>) -> Unit
|
||||
) {
|
||||
val context = LocalContext.current
|
||||
val scope = rememberCoroutineScope()
|
||||
val accountState by accountViewModel.accountLiveData.observeAsState()
|
||||
val account = accountState?.account ?: return
|
||||
val zapMessage = ""
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
package com.vitorpamplona.amethyst.ui.note
|
||||
|
||||
import android.widget.Toast
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||
@@ -49,7 +48,6 @@ import com.vitorpamplona.amethyst.ui.theme.Size15dp
|
||||
import com.vitorpamplona.amethyst.ui.theme.StdStartPadding
|
||||
import com.vitorpamplona.amethyst.ui.theme.placeholderText
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
@Composable
|
||||
public fun RelayBadgesHorizontal(baseNote: Note, accountViewModel: AccountViewModel, nav: (String) -> Unit) {
|
||||
@@ -159,15 +157,10 @@ fun RenderRelay(relay: RelayBriefInfo, accountViewModel: AccountViewModel, nav:
|
||||
Nip11Retriever.ErrorCode.FAIL_WITH_HTTP_STATUS -> context.getString(R.string.relay_information_document_error_assemble_url, url, exceptionMessage)
|
||||
}
|
||||
|
||||
scope.launch {
|
||||
Toast
|
||||
.makeText(
|
||||
context,
|
||||
msg,
|
||||
Toast.LENGTH_SHORT
|
||||
)
|
||||
.show()
|
||||
}
|
||||
accountViewModel.toast(
|
||||
context.getString(R.string.unable_to_download_relay_document),
|
||||
msg
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
@@ -5,7 +5,6 @@ import android.app.KeyguardManager
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.os.Build
|
||||
import android.widget.Toast
|
||||
import androidx.activity.compose.ManagedActivityResultLauncher
|
||||
import androidx.activity.compose.rememberLauncherForActivityResult
|
||||
import androidx.activity.result.ActivityResult
|
||||
@@ -82,7 +81,6 @@ import com.vitorpamplona.quartz.encoders.decodePublicKey
|
||||
import com.vitorpamplona.quartz.encoders.toHexKey
|
||||
import com.vitorpamplona.quartz.events.LnZapEvent
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.launch
|
||||
import androidx.compose.runtime.rememberCoroutineScope as rememberCoroutineScope
|
||||
|
||||
@@ -228,9 +226,16 @@ fun UpdateZapAmountDialog(
|
||||
try {
|
||||
postViewModel.updateNIP47(nip47uri)
|
||||
} catch (e: IllegalArgumentException) {
|
||||
scope.launch {
|
||||
Toast.makeText(context, e.message, Toast.LENGTH_SHORT)
|
||||
.show()
|
||||
if (e.message != null) {
|
||||
accountViewModel.toast(
|
||||
context.getString(R.string.error_parsing_nip47_title),
|
||||
context.getString(R.string.error_parsing_nip47, nip47uri, e.message!!)
|
||||
)
|
||||
} else {
|
||||
accountViewModel.toast(
|
||||
context.getString(R.string.error_parsing_nip47_title),
|
||||
context.getString(R.string.error_parsing_nip47_no_error, nip47uri)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -444,9 +449,16 @@ fun UpdateZapAmountDialog(
|
||||
try {
|
||||
postViewModel.updateNIP47(it)
|
||||
} catch (e: IllegalArgumentException) {
|
||||
scope.launch {
|
||||
Toast.makeText(context, e.message, Toast.LENGTH_SHORT)
|
||||
.show()
|
||||
if (e.message != null) {
|
||||
accountViewModel.toast(
|
||||
context.getString(R.string.error_parsing_nip47_title),
|
||||
context.getString(R.string.error_parsing_nip47, it, e.message!!)
|
||||
)
|
||||
} else {
|
||||
accountViewModel.toast(
|
||||
context.getString(R.string.error_parsing_nip47_title),
|
||||
context.getString(R.string.error_parsing_nip47_no_error, it)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -505,7 +517,6 @@ fun UpdateZapAmountDialog(
|
||||
mutableStateOf(false)
|
||||
}
|
||||
|
||||
val scope = rememberCoroutineScope()
|
||||
val context = LocalContext.current
|
||||
|
||||
val keyguardLauncher =
|
||||
@@ -544,13 +555,16 @@ fun UpdateZapAmountDialog(
|
||||
IconButton(onClick = {
|
||||
if (!showPassword) {
|
||||
authenticate(
|
||||
authTitle,
|
||||
context,
|
||||
scope,
|
||||
keyguardLauncher
|
||||
) {
|
||||
showPassword = true
|
||||
}
|
||||
title = authTitle,
|
||||
context = context,
|
||||
keyguardLauncher = keyguardLauncher,
|
||||
onApproved = {
|
||||
showPassword = true
|
||||
},
|
||||
onError = { title, message ->
|
||||
accountViewModel.toast(title, message)
|
||||
}
|
||||
)
|
||||
} else {
|
||||
showPassword = false
|
||||
}
|
||||
@@ -580,9 +594,9 @@ fun UpdateZapAmountDialog(
|
||||
fun authenticate(
|
||||
title: String,
|
||||
context: Context,
|
||||
scope: CoroutineScope,
|
||||
keyguardLauncher: ManagedActivityResultLauncher<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,
|
||||
context.getString(R.string.biometric_authentication_failed),
|
||||
Toast.LENGTH_SHORT
|
||||
).show()
|
||||
}
|
||||
onError(
|
||||
context.getString(R.string.biometric_authentication_failed),
|
||||
context.getString(R.string.biometric_authentication_failed_explainer)
|
||||
)
|
||||
}
|
||||
|
||||
override fun onAuthenticationSucceeded(result: BiometricPrompt.AuthenticationResult) {
|
||||
|
||||
@@ -435,10 +435,8 @@ fun NoteDropDownMenu(note: Note, popupExpanded: MutableState<Boolean>, accountVi
|
||||
},
|
||||
onClick = {
|
||||
val author = note.author ?: return@DropdownMenuItem
|
||||
scope.launch(Dispatchers.IO) {
|
||||
accountViewModel.follow(author)
|
||||
onDismiss()
|
||||
}
|
||||
accountViewModel.follow(author)
|
||||
onDismiss()
|
||||
}
|
||||
)
|
||||
Divider()
|
||||
|
||||
@@ -5,6 +5,7 @@ import android.content.Intent
|
||||
import android.net.Uri
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.text.KeyboardOptions
|
||||
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
|
||||
@@ -53,6 +54,7 @@ import com.vitorpamplona.amethyst.ui.theme.ButtonBorder
|
||||
import com.vitorpamplona.amethyst.ui.theme.DoubleHorzSpacer
|
||||
import com.vitorpamplona.amethyst.ui.theme.DoubleVertSpacer
|
||||
import com.vitorpamplona.amethyst.ui.theme.Size10dp
|
||||
import com.vitorpamplona.amethyst.ui.theme.Size16dp
|
||||
import com.vitorpamplona.amethyst.ui.theme.Size55dp
|
||||
import com.vitorpamplona.amethyst.ui.theme.StdHorzSpacer
|
||||
import com.vitorpamplona.amethyst.ui.theme.placeholderText
|
||||
@@ -89,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,
|
||||
@@ -252,7 +254,7 @@ fun ErrorMessageDialog(
|
||||
title: String,
|
||||
textContent: String,
|
||||
buttonColors: ButtonColors = ButtonDefaults.buttonColors(),
|
||||
onClickStartMessage: () -> Unit,
|
||||
onClickStartMessage: (() -> Unit)? = null,
|
||||
onDismiss: () -> Unit
|
||||
) {
|
||||
AlertDialog(
|
||||
@@ -261,24 +263,28 @@ fun ErrorMessageDialog(
|
||||
Text(title)
|
||||
},
|
||||
text = {
|
||||
Text(textContent)
|
||||
SelectionContainer {
|
||||
Text(textContent)
|
||||
}
|
||||
},
|
||||
confirmButton = {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.padding(all = 8.dp)
|
||||
.padding(vertical = 8.dp)
|
||||
.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.SpaceBetween
|
||||
) {
|
||||
TextButton(onClick = onClickStartMessage) {
|
||||
Icon(
|
||||
painter = painterResource(R.drawable.ic_dm),
|
||||
contentDescription = null
|
||||
)
|
||||
Spacer(Modifier.width(8.dp))
|
||||
Text(stringResource(R.string.error_dialog_talk_to_user))
|
||||
onClickStartMessage?.let {
|
||||
TextButton(onClick = onClickStartMessage) {
|
||||
Icon(
|
||||
painter = painterResource(R.drawable.ic_dm),
|
||||
contentDescription = null
|
||||
)
|
||||
Spacer(StdHorzSpacer)
|
||||
Text(stringResource(R.string.error_dialog_talk_to_user))
|
||||
}
|
||||
}
|
||||
Button(onClick = onDismiss, colors = buttonColors) {
|
||||
Button(onClick = onDismiss, colors = buttonColors, contentPadding = PaddingValues(horizontal = Size16dp)) {
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
@@ -286,7 +292,7 @@ fun ErrorMessageDialog(
|
||||
imageVector = Icons.Outlined.Done,
|
||||
contentDescription = null
|
||||
)
|
||||
Spacer(Modifier.width(8.dp))
|
||||
Spacer(StdHorzSpacer)
|
||||
Text(stringResource(R.string.error_dialog_button_ok))
|
||||
}
|
||||
}
|
||||
@@ -306,6 +312,7 @@ fun PayViaIntentDialog(
|
||||
|
||||
if (payingInvoices.size == 1) {
|
||||
payViaIntent(payingInvoices.first().invoice, context, onError)
|
||||
onClose()
|
||||
} else {
|
||||
Dialog(
|
||||
onDismissRequest = onClose,
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
package com.vitorpamplona.amethyst.ui.note
|
||||
|
||||
import android.widget.Toast
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
@@ -20,7 +19,6 @@ import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.dp
|
||||
@@ -182,9 +180,6 @@ fun ShowFollowingOrUnfollowingButton(
|
||||
baseAuthor: User,
|
||||
accountViewModel: AccountViewModel
|
||||
) {
|
||||
val scope = rememberCoroutineScope()
|
||||
val context = LocalContext.current
|
||||
|
||||
var isFollowing by remember { mutableStateOf(false) }
|
||||
val accountFollowsState by accountViewModel.account.userProfile().live().follows.observeAsState()
|
||||
|
||||
@@ -203,48 +198,30 @@ fun ShowFollowingOrUnfollowingButton(
|
||||
UnfollowButton {
|
||||
if (!accountViewModel.isWriteable()) {
|
||||
if (accountViewModel.loggedInWithExternalSigner()) {
|
||||
scope.launch(Dispatchers.IO) {
|
||||
accountViewModel.unfollow(baseAuthor)
|
||||
}
|
||||
accountViewModel.unfollow(baseAuthor)
|
||||
} else {
|
||||
scope.launch {
|
||||
Toast
|
||||
.makeText(
|
||||
context,
|
||||
context.getString(R.string.login_with_a_private_key_to_be_able_to_unfollow),
|
||||
Toast.LENGTH_SHORT
|
||||
)
|
||||
.show()
|
||||
}
|
||||
accountViewModel.toast(
|
||||
R.string.read_only_user,
|
||||
R.string.login_with_a_private_key_to_be_able_to_unfollow
|
||||
)
|
||||
}
|
||||
} else {
|
||||
scope.launch(Dispatchers.IO) {
|
||||
accountViewModel.unfollow(baseAuthor)
|
||||
}
|
||||
accountViewModel.unfollow(baseAuthor)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
FollowButton {
|
||||
if (!accountViewModel.isWriteable()) {
|
||||
if (accountViewModel.loggedInWithExternalSigner()) {
|
||||
scope.launch(Dispatchers.IO) {
|
||||
accountViewModel.account.follow(baseAuthor)
|
||||
}
|
||||
accountViewModel.follow(baseAuthor)
|
||||
} else {
|
||||
scope.launch {
|
||||
Toast
|
||||
.makeText(
|
||||
context,
|
||||
context.getString(R.string.login_with_a_private_key_to_be_able_to_follow),
|
||||
Toast.LENGTH_SHORT
|
||||
)
|
||||
.show()
|
||||
}
|
||||
accountViewModel.toast(
|
||||
R.string.read_only_user,
|
||||
R.string.login_with_a_private_key_to_be_able_to_follow
|
||||
)
|
||||
}
|
||||
} else {
|
||||
scope.launch(Dispatchers.IO) {
|
||||
accountViewModel.follow(baseAuthor)
|
||||
}
|
||||
accountViewModel.follow(baseAuthor)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -30,8 +30,6 @@ import com.vitorpamplona.amethyst.ui.theme.Size25dp
|
||||
import com.vitorpamplona.amethyst.ui.theme.Size55Modifier
|
||||
import com.vitorpamplona.amethyst.ui.theme.Size55dp
|
||||
import com.vitorpamplona.amethyst.ui.theme.newItemBackgroundColor
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
@Composable
|
||||
fun ZapUserSetCompose(zapSetCard: ZapUserSetCard, isInnerNote: Boolean = false, routeForLastRead: String, accountViewModel: AccountViewModel, nav: (String) -> Unit) {
|
||||
@@ -40,11 +38,7 @@ fun ZapUserSetCompose(zapSetCard: ZapUserSetCard, isInnerNote: Boolean = false,
|
||||
val newItemColor = MaterialTheme.colorScheme.newItemBackgroundColor
|
||||
|
||||
LaunchedEffect(key1 = zapSetCard.createdAt()) {
|
||||
launch(Dispatchers.IO) {
|
||||
val isNew = zapSetCard.createdAt > accountViewModel.account.loadLastRead(routeForLastRead)
|
||||
|
||||
accountViewModel.account.markAsRead(routeForLastRead, zapSetCard.createdAt)
|
||||
|
||||
accountViewModel.loadAndMarkAsRead(routeForLastRead, zapSetCard.createdAt) { isNew ->
|
||||
val newBackgroundColor = if (isNew) {
|
||||
newItemColor.compositeOver(defaultBackgroundColor)
|
||||
} else {
|
||||
|
||||
@@ -18,7 +18,6 @@ import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.vitorpamplona.amethyst.ui.note.ChatroomHeaderCompose
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||
import com.vitorpamplona.quartz.events.ChatroomKeyable
|
||||
import kotlin.time.ExperimentalTime
|
||||
import kotlin.time.measureTimedValue
|
||||
|
||||
@@ -83,24 +82,9 @@ private fun FeedLoaded(
|
||||
|
||||
LaunchedEffect(key1 = markAsRead.value) {
|
||||
if (markAsRead.value) {
|
||||
for (note in state.feed.value) {
|
||||
note.event?.let { noteEvent ->
|
||||
val channelHex = note.channelHex()
|
||||
val route = if (channelHex != null) {
|
||||
"Channel/$channelHex"
|
||||
} else if (note.event is ChatroomKeyable) {
|
||||
val withKey = (note.event as ChatroomKeyable).chatroomKey(accountViewModel.userProfile().pubkeyHex)
|
||||
"Room/${withKey.hashCode()}"
|
||||
} else {
|
||||
null
|
||||
}
|
||||
|
||||
route?.let {
|
||||
accountViewModel.account.markAsRead(route, noteEvent.createdAt())
|
||||
}
|
||||
}
|
||||
accountViewModel.markAllAsRead(state.feed.value) {
|
||||
markAsRead.value = false
|
||||
}
|
||||
markAsRead.value = false
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+12
-9
@@ -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(
|
||||
|
||||
+189
-10
@@ -1,8 +1,10 @@
|
||||
package com.vitorpamplona.amethyst.ui.screen.loggedIn
|
||||
|
||||
import android.content.Context
|
||||
import android.util.Log
|
||||
import androidx.compose.runtime.Immutable
|
||||
import androidx.compose.runtime.Stable
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.lifecycle.LiveData
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.ViewModelProvider
|
||||
@@ -30,6 +32,8 @@ import com.vitorpamplona.amethyst.service.checkNotInMainThread
|
||||
import com.vitorpamplona.amethyst.ui.actions.Dao
|
||||
import com.vitorpamplona.amethyst.ui.components.MarkdownParser
|
||||
import com.vitorpamplona.amethyst.ui.components.UrlPreviewState
|
||||
import com.vitorpamplona.amethyst.ui.navigation.Route
|
||||
import com.vitorpamplona.amethyst.ui.navigation.bottomNavigationItems
|
||||
import com.vitorpamplona.amethyst.ui.note.ZapAmountCommentNotification
|
||||
import com.vitorpamplona.amethyst.ui.note.ZapraiserStatus
|
||||
import com.vitorpamplona.amethyst.ui.note.showAmount
|
||||
@@ -38,6 +42,7 @@ import com.vitorpamplona.quartz.encoders.ATag
|
||||
import com.vitorpamplona.quartz.encoders.HexKey
|
||||
import com.vitorpamplona.quartz.encoders.Nip19
|
||||
import com.vitorpamplona.quartz.events.ChatroomKey
|
||||
import com.vitorpamplona.quartz.events.ChatroomKeyable
|
||||
import com.vitorpamplona.quartz.events.Event
|
||||
import com.vitorpamplona.quartz.events.GiftWrapEvent
|
||||
import com.vitorpamplona.quartz.events.ImmutableListOfLists
|
||||
@@ -54,19 +59,35 @@ import kotlinx.collections.immutable.persistentSetOf
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
import kotlinx.collections.immutable.toImmutableSet
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.channels.BufferOverflow
|
||||
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 }
|
||||
val accountLanguagesLiveData: LiveData<AccountState> = account.liveLanguages.map { it }
|
||||
val accountLastReadLiveData: LiveData<AccountState> = account.liveLastRead.map { it }
|
||||
val accountMarkAsReadUpdates = mutableStateOf(0)
|
||||
|
||||
val userFollows: LiveData<UserState> = account.userProfile().live().follows.map { it }
|
||||
val userRelays: LiveData<UserState> = account.userProfile().live().relays.map { it }
|
||||
|
||||
val toasts = MutableSharedFlow<ToastMsg?>(0, 3, onBufferOverflow = BufferOverflow.DROP_OLDEST)
|
||||
|
||||
val discoveryListLiveData = account.live.map {
|
||||
it.account.defaultDiscoveryFollowList
|
||||
}.distinctUntilChanged()
|
||||
@@ -87,6 +108,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
|
||||
) {
|
||||
@@ -144,6 +183,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)
|
||||
@@ -162,7 +212,9 @@ class AccountViewModel(val account: Account) : ViewModel(), Dao {
|
||||
}
|
||||
|
||||
fun deleteBoostsTo(note: Note) {
|
||||
account.delete(account.boostsTo(note))
|
||||
viewModelScope.launch(Dispatchers.IO) {
|
||||
account.delete(account.boostsTo(note))
|
||||
}
|
||||
}
|
||||
|
||||
fun calculateIfNoteWasZappedByAccount(zappedNote: Note, onWasZapped: (Boolean) -> Unit) {
|
||||
@@ -278,7 +330,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
|
||||
@@ -300,7 +352,9 @@ class AccountViewModel(val account: Account) : ViewModel(), Dao {
|
||||
}
|
||||
|
||||
fun boost(note: Note) {
|
||||
account.boost(note)
|
||||
viewModelScope.launch(Dispatchers.IO) {
|
||||
account.boost(note)
|
||||
}
|
||||
}
|
||||
|
||||
fun removeEmojiPack(usersEmojiList: Note, emojiList: Note) {
|
||||
@@ -380,11 +434,51 @@ class AccountViewModel(val account: Account) : ViewModel(), Dao {
|
||||
}
|
||||
|
||||
fun follow(user: User) {
|
||||
account.follow(user)
|
||||
viewModelScope.launch(Dispatchers.IO) {
|
||||
account.follow(user)
|
||||
}
|
||||
}
|
||||
|
||||
fun unfollow(user: User) {
|
||||
account.unfollow(user)
|
||||
viewModelScope.launch(Dispatchers.IO) {
|
||||
account.unfollow(user)
|
||||
}
|
||||
}
|
||||
|
||||
fun followGeohash(tag: String) {
|
||||
viewModelScope.launch(Dispatchers.IO) {
|
||||
account.followGeohash(tag)
|
||||
}
|
||||
}
|
||||
|
||||
fun unfollowGeohash(tag: String) {
|
||||
viewModelScope.launch(Dispatchers.IO) {
|
||||
account.unfollowGeohash(tag)
|
||||
}
|
||||
}
|
||||
|
||||
fun followHashtag(tag: String) {
|
||||
viewModelScope.launch(Dispatchers.IO) {
|
||||
account.followHashtag(tag)
|
||||
}
|
||||
}
|
||||
|
||||
fun unfollowHashtag(tag: String) {
|
||||
viewModelScope.launch(Dispatchers.IO) {
|
||||
account.unfollowHashtag(tag)
|
||||
}
|
||||
}
|
||||
|
||||
fun showWord(word: String) {
|
||||
viewModelScope.launch(Dispatchers.IO) {
|
||||
account.showWord(word)
|
||||
}
|
||||
}
|
||||
|
||||
fun hideWord(word: String) {
|
||||
viewModelScope.launch(Dispatchers.IO) {
|
||||
account.hideWord(word)
|
||||
}
|
||||
}
|
||||
|
||||
fun isLoggedUser(user: User?): Boolean {
|
||||
@@ -727,19 +821,65 @@ class AccountViewModel(val account: Account) : ViewModel(), Dao {
|
||||
}
|
||||
}
|
||||
|
||||
fun loadAndMarkAsRead(routeForLastRead: String, baseNoteCreatedAt: Long?, onIsNew: (Boolean) -> Unit) {
|
||||
fun checkIsOnline(media: String?, onDone: (Boolean) -> Unit) {
|
||||
viewModelScope.launch(Dispatchers.IO) {
|
||||
onDone(OnlineChecker.isOnline(media))
|
||||
}
|
||||
}
|
||||
|
||||
fun refreshMarkAsReadObservers() {
|
||||
updateNotificationDots()
|
||||
accountMarkAsReadUpdates.value++
|
||||
}
|
||||
|
||||
fun loadAndMarkAsRead(routeForLastRead: String, createdAt: Long?, onIsNew: (Boolean) -> Unit) {
|
||||
viewModelScope.launch(Dispatchers.IO) {
|
||||
val lastTime = account.loadLastRead(routeForLastRead)
|
||||
|
||||
if (baseNoteCreatedAt != null) {
|
||||
account.markAsRead(routeForLastRead, baseNoteCreatedAt)
|
||||
onIsNew(baseNoteCreatedAt > lastTime)
|
||||
if (createdAt != null) {
|
||||
if (account.markAsRead(routeForLastRead, createdAt)) {
|
||||
refreshMarkAsReadObservers()
|
||||
}
|
||||
onIsNew(createdAt > lastTime)
|
||||
} else {
|
||||
onIsNew(false)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun markAllAsRead(notes: ImmutableList<Note>, onDone: () -> Unit) {
|
||||
viewModelScope.launch(Dispatchers.IO) {
|
||||
var atLeastOne = false
|
||||
|
||||
for (note in notes) {
|
||||
note.event?.let { noteEvent ->
|
||||
val channelHex = note.channelHex()
|
||||
val route = if (channelHex != null) {
|
||||
"Channel/$channelHex"
|
||||
} else if (note.event is ChatroomKeyable) {
|
||||
val withKey =
|
||||
(note.event as ChatroomKeyable).chatroomKey(userProfile().pubkeyHex)
|
||||
"Room/${withKey.hashCode()}"
|
||||
} else {
|
||||
null
|
||||
}
|
||||
|
||||
route?.let {
|
||||
if (account.markAsRead(route, noteEvent.createdAt())) {
|
||||
atLeastOne = true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (atLeastOne) {
|
||||
refreshMarkAsReadObservers()
|
||||
}
|
||||
|
||||
onDone()
|
||||
}
|
||||
}
|
||||
|
||||
fun createChatRoomFor(user: User, then: (Int) -> Unit) {
|
||||
viewModelScope.launch(Dispatchers.IO) {
|
||||
val withKey = ChatroomKey(persistentSetOf(user.pubkeyHex))
|
||||
@@ -753,6 +893,45 @@ class AccountViewModel(val account: Account) : ViewModel(), Dao {
|
||||
return AccountViewModel(account) as AccountViewModel
|
||||
}
|
||||
}
|
||||
|
||||
private var collectorJob: Job? = null
|
||||
val notificationDots = HasNotificationDot(bottomNavigationItems, account)
|
||||
|
||||
fun updateNotificationDots(newNotes: Set<Note> = emptySet()) {
|
||||
viewModelScope.launch(Dispatchers.Default) {
|
||||
val (value, elapsed) = measureTimedValue {
|
||||
notificationDots.update(newNotes)
|
||||
}
|
||||
Log.d("Rendering Metrics", "Notification Dots Calculation in $elapsed for ${newNotes.size} new notes")
|
||||
}
|
||||
}
|
||||
|
||||
init {
|
||||
Log.d("Init", "AccountViewModel")
|
||||
collectorJob = viewModelScope.launch(Dispatchers.IO) {
|
||||
LocalCache.live.newEventBundles.collect { newNotes ->
|
||||
updateNotificationDots(newNotes)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCleared() {
|
||||
collectorJob?.cancel()
|
||||
super.onCleared()
|
||||
}
|
||||
}
|
||||
|
||||
class HasNotificationDot(bottomNavigationItems: ImmutableList<Route>, val account: Account) {
|
||||
val hasNewItems = bottomNavigationItems.associateWith { MutableStateFlow(false) }
|
||||
|
||||
fun update(newNotes: Set<Note>) {
|
||||
hasNewItems.forEach {
|
||||
val newResult = it.key.hasNewItems(account, newNotes)
|
||||
if (newResult != it.value.value) {
|
||||
it.value.value = newResult
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Immutable
|
||||
|
||||
+2
-14
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
package com.vitorpamplona.amethyst.ui.screen.loggedIn
|
||||
|
||||
import android.widget.Toast
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
@@ -18,7 +17,6 @@ import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.livedata.observeAsState
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
@@ -160,9 +158,6 @@ fun GeoHashActionOptions(
|
||||
tag: String,
|
||||
accountViewModel: AccountViewModel
|
||||
) {
|
||||
val scope = rememberCoroutineScope()
|
||||
val context = LocalContext.current
|
||||
|
||||
val userState by accountViewModel.userProfile().live().follows.observeAsState()
|
||||
val isFollowingTag by remember(userState) {
|
||||
derivedStateOf {
|
||||
@@ -174,48 +169,30 @@ fun GeoHashActionOptions(
|
||||
UnfollowButton {
|
||||
if (!accountViewModel.isWriteable()) {
|
||||
if (accountViewModel.loggedInWithExternalSigner()) {
|
||||
scope.launch(Dispatchers.IO) {
|
||||
accountViewModel.account.unfollowGeohash(tag)
|
||||
}
|
||||
accountViewModel.unfollowGeohash(tag)
|
||||
} else {
|
||||
scope.launch {
|
||||
Toast
|
||||
.makeText(
|
||||
context,
|
||||
context.getString(R.string.login_with_a_private_key_to_be_able_to_unfollow),
|
||||
Toast.LENGTH_SHORT
|
||||
)
|
||||
.show()
|
||||
}
|
||||
accountViewModel.toast(
|
||||
R.string.read_only_user,
|
||||
R.string.login_with_a_private_key_to_be_able_to_unfollow
|
||||
)
|
||||
}
|
||||
} else {
|
||||
scope.launch(Dispatchers.IO) {
|
||||
accountViewModel.account.unfollowGeohash(tag)
|
||||
}
|
||||
accountViewModel.unfollowGeohash(tag)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
FollowButton {
|
||||
if (!accountViewModel.isWriteable()) {
|
||||
if (accountViewModel.loggedInWithExternalSigner()) {
|
||||
scope.launch(Dispatchers.IO) {
|
||||
accountViewModel.account.followGeohash(tag)
|
||||
}
|
||||
accountViewModel.followGeohash(tag)
|
||||
} else {
|
||||
scope.launch {
|
||||
Toast
|
||||
.makeText(
|
||||
context,
|
||||
context.getString(R.string.login_with_a_private_key_to_be_able_to_follow),
|
||||
Toast.LENGTH_SHORT
|
||||
)
|
||||
.show()
|
||||
}
|
||||
accountViewModel.toast(
|
||||
R.string.read_only_user,
|
||||
R.string.login_with_a_private_key_to_be_able_to_follow
|
||||
)
|
||||
}
|
||||
} else {
|
||||
scope.launch(Dispatchers.IO) {
|
||||
accountViewModel.account.followGeohash(tag)
|
||||
}
|
||||
accountViewModel.followGeohash(tag)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
package com.vitorpamplona.amethyst.ui.screen.loggedIn
|
||||
|
||||
import android.widget.Toast
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
@@ -16,10 +15,8 @@ import androidx.compose.runtime.derivedStateOf
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.livedata.observeAsState
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.platform.LocalLifecycleOwner
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.unit.dp
|
||||
@@ -31,8 +28,6 @@ import com.vitorpamplona.amethyst.service.NostrHashtagDataSource
|
||||
import com.vitorpamplona.amethyst.ui.screen.NostrHashtagFeedViewModel
|
||||
import com.vitorpamplona.amethyst.ui.screen.RefresheableFeedView
|
||||
import com.vitorpamplona.amethyst.ui.theme.StdPadding
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
@Composable
|
||||
fun HashtagScreen(tag: String?, accountViewModel: AccountViewModel, nav: (String) -> Unit) {
|
||||
@@ -136,9 +131,6 @@ fun HashtagActionOptions(
|
||||
tag: String,
|
||||
accountViewModel: AccountViewModel
|
||||
) {
|
||||
val scope = rememberCoroutineScope()
|
||||
val context = LocalContext.current
|
||||
|
||||
val userState by accountViewModel.userProfile().live().follows.observeAsState()
|
||||
val isFollowingTag by remember(userState) {
|
||||
derivedStateOf {
|
||||
@@ -150,48 +142,30 @@ fun HashtagActionOptions(
|
||||
UnfollowButton {
|
||||
if (!accountViewModel.isWriteable()) {
|
||||
if (accountViewModel.loggedInWithExternalSigner()) {
|
||||
scope.launch(Dispatchers.IO) {
|
||||
accountViewModel.account.unfollowHashtag(tag)
|
||||
}
|
||||
accountViewModel.unfollowHashtag(tag)
|
||||
} else {
|
||||
scope.launch {
|
||||
Toast
|
||||
.makeText(
|
||||
context,
|
||||
context.getString(R.string.login_with_a_private_key_to_be_able_to_unfollow),
|
||||
Toast.LENGTH_SHORT
|
||||
)
|
||||
.show()
|
||||
}
|
||||
accountViewModel.toast(
|
||||
R.string.read_only_user,
|
||||
R.string.login_with_a_private_key_to_be_able_to_unfollow
|
||||
)
|
||||
}
|
||||
} else {
|
||||
scope.launch(Dispatchers.IO) {
|
||||
accountViewModel.account.unfollowHashtag(tag)
|
||||
}
|
||||
accountViewModel.unfollowHashtag(tag)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
FollowButton {
|
||||
if (!accountViewModel.isWriteable()) {
|
||||
if (accountViewModel.loggedInWithExternalSigner()) {
|
||||
scope.launch(Dispatchers.IO) {
|
||||
accountViewModel.account.followHashtag(tag)
|
||||
}
|
||||
accountViewModel.followHashtag(tag)
|
||||
} else {
|
||||
scope.launch {
|
||||
Toast
|
||||
.makeText(
|
||||
context,
|
||||
context.getString(R.string.login_with_a_private_key_to_be_able_to_follow),
|
||||
Toast.LENGTH_SHORT
|
||||
)
|
||||
.show()
|
||||
}
|
||||
accountViewModel.toast(
|
||||
R.string.read_only_user,
|
||||
R.string.login_with_a_private_key_to_be_able_to_follow
|
||||
)
|
||||
}
|
||||
} else {
|
||||
scope.launch(Dispatchers.IO) {
|
||||
accountViewModel.account.followHashtag(tag)
|
||||
}
|
||||
accountViewModel.followHashtag(tag)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+12
-36
@@ -1,6 +1,5 @@
|
||||
package com.vitorpamplona.amethyst.ui.screen.loggedIn
|
||||
|
||||
import android.widget.Toast
|
||||
import androidx.compose.foundation.ExperimentalFoundationApi
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
@@ -35,7 +34,6 @@ import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.platform.LocalLifecycleOwner
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
@@ -65,7 +63,6 @@ import com.vitorpamplona.amethyst.ui.theme.Size10dp
|
||||
import com.vitorpamplona.amethyst.ui.theme.StdPadding
|
||||
import com.vitorpamplona.amethyst.ui.theme.TabRowHeight
|
||||
import com.vitorpamplona.amethyst.ui.theme.placeholderText
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
@Composable
|
||||
@@ -307,9 +304,6 @@ fun MutedWordActionOptions(
|
||||
word: String,
|
||||
accountViewModel: AccountViewModel
|
||||
) {
|
||||
val scope = rememberCoroutineScope()
|
||||
val context = LocalContext.current
|
||||
|
||||
val isMutedWord by accountViewModel.account.liveHiddenUsers.map {
|
||||
word in it.hiddenWords
|
||||
}.distinctUntilChanged().observeAsState()
|
||||
@@ -318,48 +312,30 @@ fun MutedWordActionOptions(
|
||||
ShowWordButton {
|
||||
if (!accountViewModel.isWriteable()) {
|
||||
if (accountViewModel.loggedInWithExternalSigner()) {
|
||||
scope.launch(Dispatchers.IO) {
|
||||
accountViewModel.account.showWord(word)
|
||||
}
|
||||
accountViewModel.showWord(word)
|
||||
} else {
|
||||
scope.launch {
|
||||
Toast
|
||||
.makeText(
|
||||
context,
|
||||
context.getString(R.string.login_with_a_private_key_to_be_able_to_unfollow),
|
||||
Toast.LENGTH_SHORT
|
||||
)
|
||||
.show()
|
||||
}
|
||||
accountViewModel.toast(
|
||||
R.string.read_only_user,
|
||||
R.string.login_with_a_private_key_to_be_able_to_show_word
|
||||
)
|
||||
}
|
||||
} else {
|
||||
scope.launch(Dispatchers.IO) {
|
||||
accountViewModel.account.showWord(word)
|
||||
}
|
||||
accountViewModel.showWord(word)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
HideWordButton {
|
||||
if (!accountViewModel.isWriteable()) {
|
||||
if (accountViewModel.loggedInWithExternalSigner()) {
|
||||
scope.launch(Dispatchers.IO) {
|
||||
accountViewModel.account.hideWord(word)
|
||||
}
|
||||
accountViewModel.hideWord(word)
|
||||
} else {
|
||||
scope.launch {
|
||||
Toast
|
||||
.makeText(
|
||||
context,
|
||||
context.getString(R.string.login_with_a_private_key_to_be_able_to_follow),
|
||||
Toast.LENGTH_SHORT
|
||||
)
|
||||
.show()
|
||||
}
|
||||
accountViewModel.toast(
|
||||
R.string.read_only_user,
|
||||
R.string.login_with_a_private_key_to_be_able_to_hide_word
|
||||
)
|
||||
}
|
||||
} else {
|
||||
scope.launch(Dispatchers.IO) {
|
||||
accountViewModel.account.hideWord(word)
|
||||
}
|
||||
accountViewModel.hideWord(word)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -1,8 +1,5 @@
|
||||
package com.vitorpamplona.amethyst.ui.screen.loggedIn
|
||||
|
||||
import android.content.Intent
|
||||
import android.net.Uri
|
||||
import android.widget.Toast
|
||||
import androidx.compose.animation.Crossfade
|
||||
import androidx.compose.animation.core.tween
|
||||
import androidx.compose.foundation.*
|
||||
@@ -46,7 +43,6 @@ import androidx.compose.ui.unit.Dp
|
||||
import androidx.compose.ui.unit.IntSize
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.lifecycle.Lifecycle
|
||||
import androidx.lifecycle.LifecycleEventObserver
|
||||
import androidx.lifecycle.distinctUntilChanged
|
||||
@@ -62,6 +58,7 @@ import com.vitorpamplona.amethyst.model.Note
|
||||
import com.vitorpamplona.amethyst.model.User
|
||||
import com.vitorpamplona.amethyst.service.NostrUserProfileDataSource
|
||||
import com.vitorpamplona.amethyst.service.connectivitystatus.ConnectivityStatus
|
||||
import com.vitorpamplona.amethyst.ui.actions.InformationDialog
|
||||
import com.vitorpamplona.amethyst.ui.actions.NewUserMetadataView
|
||||
import com.vitorpamplona.amethyst.ui.components.CreateTextWithEmoji
|
||||
import com.vitorpamplona.amethyst.ui.components.DisplayNip05ProfileStatus
|
||||
@@ -74,8 +71,11 @@ import com.vitorpamplona.amethyst.ui.components.figureOutMimeType
|
||||
import com.vitorpamplona.amethyst.ui.dal.UserProfileReportsFeedFilter
|
||||
import com.vitorpamplona.amethyst.ui.navigation.ShowQRDialog
|
||||
import com.vitorpamplona.amethyst.ui.note.ClickableUserPicture
|
||||
import com.vitorpamplona.amethyst.ui.note.ErrorMessageDialog
|
||||
import com.vitorpamplona.amethyst.ui.note.LightningAddressIcon
|
||||
import com.vitorpamplona.amethyst.ui.note.LoadAddressableNote
|
||||
import com.vitorpamplona.amethyst.ui.note.payViaIntent
|
||||
import com.vitorpamplona.amethyst.ui.note.routeToMessage
|
||||
import com.vitorpamplona.amethyst.ui.screen.FeedState
|
||||
import com.vitorpamplona.amethyst.ui.screen.LnZapFeedView
|
||||
import com.vitorpamplona.amethyst.ui.screen.NostrUserAppRecommendationsFeedViewModel
|
||||
@@ -739,9 +739,6 @@ private fun DisplayFollowUnfollowButton(
|
||||
baseUser: User,
|
||||
accountViewModel: AccountViewModel
|
||||
) {
|
||||
val scope = rememberCoroutineScope()
|
||||
val context = LocalContext.current
|
||||
|
||||
val isLoggedInFollowingUser by accountViewModel.account.userProfile().live().follows.map {
|
||||
it.user.isFollowing(baseUser)
|
||||
}.distinctUntilChanged().observeAsState(initial = accountViewModel.account.isFollowing(baseUser))
|
||||
@@ -754,24 +751,15 @@ private fun DisplayFollowUnfollowButton(
|
||||
UnfollowButton {
|
||||
if (!accountViewModel.isWriteable()) {
|
||||
if (accountViewModel.loggedInWithExternalSigner()) {
|
||||
scope.launch(Dispatchers.IO) {
|
||||
accountViewModel.account.unfollow(baseUser)
|
||||
}
|
||||
accountViewModel.unfollow(baseUser)
|
||||
} else {
|
||||
scope.launch {
|
||||
Toast
|
||||
.makeText(
|
||||
context,
|
||||
context.getString(R.string.login_with_a_private_key_to_be_able_to_unfollow),
|
||||
Toast.LENGTH_SHORT
|
||||
)
|
||||
.show()
|
||||
}
|
||||
accountViewModel.toast(
|
||||
R.string.read_only_user,
|
||||
R.string.login_with_a_private_key_to_be_able_to_unfollow
|
||||
)
|
||||
}
|
||||
} else {
|
||||
scope.launch(Dispatchers.IO) {
|
||||
accountViewModel.account.unfollow(baseUser)
|
||||
}
|
||||
accountViewModel.unfollow(baseUser)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
@@ -779,48 +767,30 @@ private fun DisplayFollowUnfollowButton(
|
||||
FollowButton(R.string.follow_back) {
|
||||
if (!accountViewModel.isWriteable()) {
|
||||
if (accountViewModel.loggedInWithExternalSigner()) {
|
||||
scope.launch(Dispatchers.IO) {
|
||||
accountViewModel.account.follow(baseUser)
|
||||
}
|
||||
accountViewModel.follow(baseUser)
|
||||
} else {
|
||||
scope.launch {
|
||||
Toast
|
||||
.makeText(
|
||||
context,
|
||||
context.getString(R.string.login_with_a_private_key_to_be_able_to_follow),
|
||||
Toast.LENGTH_SHORT
|
||||
)
|
||||
.show()
|
||||
}
|
||||
accountViewModel.toast(
|
||||
R.string.read_only_user,
|
||||
R.string.login_with_a_private_key_to_be_able_to_follow
|
||||
)
|
||||
}
|
||||
} else {
|
||||
scope.launch(Dispatchers.IO) {
|
||||
accountViewModel.account.follow(baseUser)
|
||||
}
|
||||
accountViewModel.follow(baseUser)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
FollowButton(R.string.follow) {
|
||||
if (!accountViewModel.isWriteable()) {
|
||||
if (accountViewModel.loggedInWithExternalSigner()) {
|
||||
scope.launch(Dispatchers.IO) {
|
||||
accountViewModel.account.follow(baseUser)
|
||||
}
|
||||
accountViewModel.follow(baseUser)
|
||||
} else {
|
||||
scope.launch {
|
||||
Toast
|
||||
.makeText(
|
||||
context,
|
||||
context.getString(R.string.login_with_a_private_key_to_be_able_to_follow),
|
||||
Toast.LENGTH_SHORT
|
||||
)
|
||||
.show()
|
||||
}
|
||||
accountViewModel.toast(
|
||||
R.string.read_only_user,
|
||||
R.string.login_with_a_private_key_to_be_able_to_follow
|
||||
)
|
||||
}
|
||||
} else {
|
||||
scope.launch(Dispatchers.IO) {
|
||||
accountViewModel.account.follow(baseUser)
|
||||
}
|
||||
accountViewModel.follow(baseUser)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -974,7 +944,7 @@ private fun DrawAdditionalInfo(
|
||||
|
||||
val lud16 = remember(userState) { user.info?.lud16?.trim() ?: user.info?.lud06?.trim() }
|
||||
val pubkeyHex = remember { baseUser.pubkeyHex }
|
||||
DisplayLNAddress(lud16, pubkeyHex, accountViewModel.account)
|
||||
DisplayLNAddress(lud16, pubkeyHex, accountViewModel, nav)
|
||||
|
||||
val identities = user.info?.latestMetadata?.identityClaims()
|
||||
if (!identities.isNullOrEmpty()) {
|
||||
@@ -1026,12 +996,39 @@ private fun DrawAdditionalInfo(
|
||||
fun DisplayLNAddress(
|
||||
lud16: String?,
|
||||
userHex: String,
|
||||
account: Account
|
||||
accountViewModel: AccountViewModel,
|
||||
nav: (String) -> Unit
|
||||
) {
|
||||
val context = LocalContext.current
|
||||
val scope = rememberCoroutineScope()
|
||||
var zapExpanded by remember { mutableStateOf(false) }
|
||||
|
||||
var showErrorMessageDialog by remember { mutableStateOf<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
|
||||
?: response.error?.code?.toString()
|
||||
?: context.getString(R.string.error_parsing_error_message),
|
||||
Toast.LENGTH_LONG
|
||||
).show()
|
||||
}
|
||||
showErrorMessageDialog = response.error?.message
|
||||
?: response.error?.code?.toString()
|
||||
?: context.getString(R.string.error_parsing_error_message)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
try {
|
||||
val intent = Intent(Intent.ACTION_VIEW, Uri.parse("lightning:$it"))
|
||||
intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK
|
||||
ContextCompat.startActivity(context, intent, null)
|
||||
} catch (e: Exception) {
|
||||
scope.launch {
|
||||
Toast.makeText(
|
||||
context,
|
||||
context.getString(R.string.lightning_wallets_not_found),
|
||||
Toast.LENGTH_LONG
|
||||
).show()
|
||||
}
|
||||
payViaIntent(it, context) {
|
||||
showErrorMessageDialog = it
|
||||
}
|
||||
}
|
||||
},
|
||||
onClose = {
|
||||
zapExpanded = false
|
||||
},
|
||||
onError = { title, message ->
|
||||
accountViewModel.toast(title, message)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
@@ -311,6 +311,15 @@ fun LoginPage(
|
||||
connectOrbotDialogOpen = false
|
||||
useProxy.value = true
|
||||
},
|
||||
onError = {
|
||||
scope.launch {
|
||||
Toast.makeText(
|
||||
context,
|
||||
it,
|
||||
Toast.LENGTH_LONG
|
||||
).show()
|
||||
}
|
||||
},
|
||||
proxyPort
|
||||
)
|
||||
}
|
||||
|
||||
@@ -256,7 +256,6 @@
|
||||
<string name="poll_consensus_threshold_percent">(0–100)%</string>
|
||||
<string name="poll_closing_time">إغلاق بعد</string>
|
||||
<string name="poll_closing_time_days">أيام</string>
|
||||
<string name="poll_is_closed">تم إغلاق الاستطلاع لأي تصويت جديد</string>
|
||||
<string name="one_vote_per_user_on_atomic_votes">يسمح بصوت واحد فقط لكل مستخدم في هذا النوع من الاستطلاع</string>
|
||||
<string name="looking_for_event">"البحث عن الحدث %1$s"</string>
|
||||
<string name="custom_zaps_add_a_message">أضف رسالة عامة</string>
|
||||
|
||||
@@ -270,7 +270,6 @@
|
||||
<string name="poll_consensus_threshold_percent">(০–১০০)%</string>
|
||||
<string name="poll_closing_time">এই সময় পর বন্ধ করুন</string>
|
||||
<string name="poll_closing_time_days">দিনগুলি</string>
|
||||
<string name="poll_is_closed">পোলটি আর নতুন ভোট গ্রহণ করবে না</string>
|
||||
<string name="poll_zap_amount">পরিমাণটি জ্যাপ করুন</string>
|
||||
<string name="one_vote_per_user_on_atomic_votes">এই জাতীয় পোলে একজন ব্যবহারকারী শুধুমাত্র একটি করেই ভোট দিতে পারবেন</string>
|
||||
<string name="looking_for_event">"ইভেন্ট খোঁজা হচ্ছে %1$s"</string>
|
||||
|
||||
@@ -270,7 +270,6 @@
|
||||
<string name="poll_consensus_threshold_percent">(0–100)%</string>
|
||||
<string name="poll_closing_time">Uzavřít po</string>
|
||||
<string name="poll_closing_time_days">dnech</string>
|
||||
<string name="poll_is_closed">Hlasování je uzavřeno pro nové hlasy</string>
|
||||
<string name="poll_zap_amount">Částka zapsu</string>
|
||||
<string name="one_vote_per_user_on_atomic_votes">U tohoto typu hlasování je povolen pouze jeden hlas na uživatele</string>
|
||||
<string name="looking_for_event">"Hledá se událost %1$s"</string>
|
||||
|
||||
@@ -275,7 +275,6 @@ anz der Bedingungen ist erforderlich</string>
|
||||
<string name="poll_consensus_threshold_percent">(0–100)%</string>
|
||||
<string name="poll_closing_time">Schließen nach</string>
|
||||
<string name="poll_closing_time_days">Tagen</string>
|
||||
<string name="poll_is_closed">Umfrage ist für neue Stimmen geschlossen</string>
|
||||
<string name="poll_zap_amount">Zap-Betrag</string>
|
||||
<string name="one_vote_per_user_on_atomic_votes">Es ist nur eine Stimme pro Benutzer für diesen Umfragetyp erlaubt</string>
|
||||
<string name="looking_for_event">"Veranstaltung suchen"</string>
|
||||
|
||||
@@ -270,7 +270,6 @@
|
||||
<string name="poll_consensus_threshold_percent">(0-100)%</string>
|
||||
<string name="poll_closing_time">Fermi post</string>
|
||||
<string name="poll_closing_time_days">tagoj</string>
|
||||
<string name="poll_is_closed">Enketo estas fermita al novaj voĉdonoj</string>
|
||||
<string name="poll_zap_amount">Kvanto de zapoj</string>
|
||||
<string name="one_vote_per_user_on_atomic_votes">Nur po unu voĉdono por uzanto estas permesita en ĉi tia enketo</string>
|
||||
<string name="looking_for_event">"Serĉanta Eventon %1$s"</string>
|
||||
|
||||
@@ -270,7 +270,6 @@
|
||||
<string name="poll_consensus_threshold_percent">(0–100)%</string>
|
||||
<string name="poll_closing_time">بسته شدن پس از</string>
|
||||
<string name="poll_closing_time_days">روز</string>
|
||||
<string name="poll_is_closed">نظر سنجی برای رای جدید بسته است</string>
|
||||
<string name="poll_zap_amount">مبلغ زپ</string>
|
||||
<string name="one_vote_per_user_on_atomic_votes">فقط یک رای به ازای هر کاربر در این نوع نظرسنجی مجاز است</string>
|
||||
<string name="looking_for_event">"جستجوی رویداد %1$s"</string>
|
||||
|
||||
@@ -270,7 +270,6 @@
|
||||
<string name="poll_consensus_threshold_percent">(0–100)%</string>
|
||||
<string name="poll_closing_time">Sulje äänestys</string>
|
||||
<string name="poll_closing_time_days">päivän kuluttua</string>
|
||||
<string name="poll_is_closed">Äänestys on suljettu uusilta ääniltä</string>
|
||||
<string name="poll_zap_amount">Zap-määrä</string>
|
||||
<string name="one_vote_per_user_on_atomic_votes">Vain yksi ääni käyttäjää kohti tällä äänestystyypillä</string>
|
||||
<string name="looking_for_event">"Etsitään tapahtumaa %1$s"</string>
|
||||
|
||||
@@ -270,7 +270,6 @@
|
||||
<string name="poll_consensus_threshold_percent">(0–100)%</string>
|
||||
<string name="poll_closing_time">Clôturer après</string>
|
||||
<string name="poll_closing_time_days">jours</string>
|
||||
<string name="poll_is_closed">Le Sondage est fermé aux nouveaux votes</string>
|
||||
<string name="poll_zap_amount">Montant des Zap</string>
|
||||
<string name="one_vote_per_user_on_atomic_votes">Un seul vote par utilisateur est autorisé sur ce type de sondage</string>
|
||||
<string name="looking_for_event">"Recherche de l'Événement %1$s"</string>
|
||||
|
||||
@@ -270,7 +270,6 @@
|
||||
<string name="poll_consensus_threshold_percent">(0–100)%</string>
|
||||
<string name="poll_closing_time">Szavazás lezárása</string>
|
||||
<string name="poll_closing_time_days">napok</string>
|
||||
<string name="poll_is_closed">A szavazásra nem lehet már új szavazatot leadni</string>
|
||||
<string name="poll_zap_amount">Zap összege</string>
|
||||
<string name="one_vote_per_user_on_atomic_votes">Az ilyen típusú szavazásokon felhasználónként csak egy szavazat engedélyezett</string>
|
||||
<string name="looking_for_event">"%1$s esemény keresése"</string>
|
||||
|
||||
@@ -268,7 +268,6 @@
|
||||
<string name="poll_consensus_threshold_percent">(0–100)%</string>
|
||||
<string name="poll_closing_time">Berakhir pada</string>
|
||||
<string name="poll_closing_time_days">hari</string>
|
||||
<string name="poll_is_closed">Pemungutan suara ditutup untuk voting baru</string>
|
||||
<string name="poll_zap_amount">Jumlah Zap</string>
|
||||
<string name="one_vote_per_user_on_atomic_votes">Hanya satu suara per pengguna yang diperbolehkan pada jenis pemungutan suara ini</string>
|
||||
|
||||
|
||||
@@ -270,7 +270,6 @@
|
||||
<string name="poll_consensus_threshold_percent">(0–100)%</string>
|
||||
<string name="poll_closing_time">Chiudi dopo</string>
|
||||
<string name="poll_closing_time_days">giorni</string>
|
||||
<string name="poll_is_closed">Il sondaggio è chiuso a nuovi voti</string>
|
||||
<string name="poll_zap_amount">Quantità in zap</string>
|
||||
<string name="one_vote_per_user_on_atomic_votes">Per questo tipo di sondaggio è consentito solo un voto per utente</string>
|
||||
<string name="looking_for_event">"Cercando l'evento %1$s"</string>
|
||||
|
||||
@@ -264,7 +264,6 @@
|
||||
<string name="poll_consensus_threshold">コンセンサス</string>
|
||||
<string name="poll_closing_time">終了まで</string>
|
||||
<string name="poll_closing_time_days">日</string>
|
||||
<string name="poll_is_closed">投票は締め切られました</string>
|
||||
<string name="poll_zap_amount">ザップ額</string>
|
||||
<string name="one_vote_per_user_on_atomic_votes">このタイプの投票では、1ユーザにつき1回のみ投票可能です</string>
|
||||
<string name="looking_for_event">"イベント %1$s を探しています"</string>
|
||||
|
||||
@@ -270,7 +270,6 @@
|
||||
<string name="poll_consensus_threshold_percent">(0-100)%</string>
|
||||
<string name="poll_closing_time">Sluit na</string>
|
||||
<string name="poll_closing_time_days">dagen</string>
|
||||
<string name="poll_is_closed">Poll is gesloten voor nieuwe stemmen</string>
|
||||
<string name="poll_zap_amount">Zap bedrag</string>
|
||||
<string name="one_vote_per_user_on_atomic_votes">Slechts één stem per gebruiker is toegestaan bij dit type peiling.</string>
|
||||
<string name="looking_for_event">"Zoeken naar event %1$s"</string>
|
||||
|
||||
@@ -262,7 +262,6 @@
|
||||
<string name="poll_consensus_threshold">Consenso</string>
|
||||
<string name="poll_closing_time">Fechar depois</string>
|
||||
<string name="poll_closing_time_days">dias</string>
|
||||
<string name="poll_is_closed">Enquete está fechada para novos votos</string>
|
||||
<string name="poll_zap_amount">Valor do Zap</string>
|
||||
<string name="one_vote_per_user_on_atomic_votes">Apenas um voto por usuário é permitido neste tipo de enquete</string>
|
||||
<string name="looking_for_event">"Procurando o evento %1$s"</string>
|
||||
|
||||
@@ -262,7 +262,6 @@
|
||||
<string name="poll_consensus_threshold">Консенсус</string>
|
||||
<string name="poll_closing_time">Закрытие после</string>
|
||||
<string name="poll_closing_time_days">дней</string>
|
||||
<string name="poll_is_closed">Опрос закрыт</string>
|
||||
<string name="poll_zap_amount">Сумма запа</string>
|
||||
<string name="one_vote_per_user_on_atomic_votes">В этом опросе можно голосовать только один раз</string>
|
||||
<string name="looking_for_event">"Поиск события %1$s"</string>
|
||||
|
||||
@@ -269,7 +269,6 @@
|
||||
<string name="poll_consensus_threshold_percent">(0–100)%</string>
|
||||
<string name="poll_closing_time">Avsluta efter</string>
|
||||
<string name="poll_closing_time_days">dagar</string>
|
||||
<string name="poll_is_closed">Omröstningen är stängd för nya röster</string>
|
||||
<string name="poll_zap_amount">Zap belopp</string>
|
||||
<string name="one_vote_per_user_on_atomic_votes">Endast en röst per användare tillåts i denna typ av omröstning</string>
|
||||
<string name="looking_for_event">"Letar efter Event %1$s"</string>
|
||||
|
||||
@@ -270,7 +270,6 @@
|
||||
<string name="poll_consensus_threshold_percent">(0–100)%</string>
|
||||
<string name="poll_closing_time">Funga baada ya</string>
|
||||
<string name="poll_closing_time_days">siku</string>
|
||||
<string name="poll_is_closed">Kura imefungwa kwa kura mpya</string>
|
||||
<string name="poll_zap_amount">Kiasi cha Zaps</string>
|
||||
<string name="one_vote_per_user_on_atomic_votes">Kura moja tu kwa mtumiaji inaruhusiwa kwenye aina hii ya kura</string>
|
||||
<string name="looking_for_event">"Inatafuta Tukio %1$s"</string>
|
||||
|
||||
@@ -257,7 +257,6 @@
|
||||
<string name="poll_consensus_threshold">ஒருமித்த கருத்து</string>
|
||||
<string name="poll_closing_time">நிறைவுறும் நேரம்</string>
|
||||
<string name="poll_closing_time_days">நாட்களில்</string>
|
||||
<string name="poll_is_closed">வாக்கெடுப்பு நிறுத்திவைக்கப்பட்டு உள்ளது</string>
|
||||
<string name="poll_zap_amount">ஜாப் தொகை</string>
|
||||
<string name="one_vote_per_user_on_atomic_votes">இந்த வகை வாக்கெடுப்பில் ஒரு பயனருக்கு ஒரு வாக்கு மட்டுமே அனுமதிக்கப்படுகிறது</string>
|
||||
<string name="looking_for_event">"நிகழ்வு %1$s ஐத் தேடி"</string>
|
||||
|
||||
@@ -270,7 +270,6 @@
|
||||
<string name="poll_consensus_threshold_percent">(0–100)%</string>
|
||||
<string name="poll_closing_time">ปิดหลังจาก</string>
|
||||
<string name="poll_closing_time_days">วัน</string>
|
||||
<string name="poll_is_closed">โพลปิดรับการลงคะแนนเพิ่มเติม</string>
|
||||
<string name="poll_zap_amount">จำนวน Zap</string>
|
||||
<string name="one_vote_per_user_on_atomic_votes">อนุญาตให้โหวตได้เพียงครั้งเดียวต่อผู้ใช้หนึ่งคนสำหรับการสำรวจประเภทนี้</string>
|
||||
<string name="looking_for_event">"กำลังมองหา Event %1$s"</string>
|
||||
|
||||
@@ -262,7 +262,6 @@
|
||||
<string name="poll_consensus_threshold">Консенсус</string>
|
||||
<string name="poll_closing_time">Закриття після</string>
|
||||
<string name="poll_closing_time_days">днів</string>
|
||||
<string name="poll_is_closed">Опитування закрито</string>
|
||||
<string name="poll_zap_amount">Сума запу</string>
|
||||
<string name="one_vote_per_user_on_atomic_votes">У цьому опитуванні можна голосувати тільки один раз</string>
|
||||
<string name="looking_for_event">"Пошук події %1$s"</string>
|
||||
|
||||
@@ -269,7 +269,6 @@
|
||||
<string name="poll_consensus_threshold_percent">(0–100)%</string>
|
||||
<string name="poll_closing_time">后关闭</string>
|
||||
<string name="poll_closing_time_days">天</string>
|
||||
<string name="poll_is_closed">投票不再接收新投票</string>
|
||||
<string name="poll_zap_amount">打闪金额</string>
|
||||
<string name="one_vote_per_user_on_atomic_votes">这种投票只允许每个用户一票</string>
|
||||
<string name="looking_for_event">"正在查找事件 %1$s"</string>
|
||||
|
||||
@@ -270,7 +270,6 @@
|
||||
<string name="poll_consensus_threshold_percent">(0–100)%</string>
|
||||
<string name="poll_closing_time">後關閉</string>
|
||||
<string name="poll_closing_time_days">天</string>
|
||||
<string name="poll_is_closed">投票不再接收新投票</string>
|
||||
<string name="poll_zap_amount">打閃金額</string>
|
||||
<string name="one_vote_per_user_on_atomic_votes">這種投票只允許每個用戶一票</string>
|
||||
<string name="looking_for_event">"正在查找事件 %1$s"</string>
|
||||
|
||||
@@ -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">(0–100)%</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>
|
||||
@@ -600,4 +609,39 @@
|
||||
<string name="hide_new_word_label">Hide new word or sentence</string>
|
||||
<string name="automatically_show_profile_picture">Profile Picture</string>
|
||||
<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>
|
||||
|
||||
+1
-1
@@ -4,7 +4,7 @@ buildscript {
|
||||
ext {
|
||||
fragment_version = "1.6.1"
|
||||
lifecycle_version = '2.6.2'
|
||||
compose_ui_version = '1.5.1'
|
||||
compose_ui_version = '1.5.2'
|
||||
nav_version = '2.7.3'
|
||||
room_version = "2.4.3"
|
||||
accompanist_version = '0.30.1'
|
||||
|
||||
+1
-1
@@ -38,7 +38,7 @@ dependencies {
|
||||
implementation "androidx.compose.runtime:runtime:$compose_ui_version"
|
||||
|
||||
// Bitcoin secp256k1 bindings to Android
|
||||
api 'fr.acinq.secp256k1:secp256k1-kmp-jni-android:0.10.1'
|
||||
api 'fr.acinq.secp256k1:secp256k1-kmp-jni-android:0.11.0'
|
||||
|
||||
// LibSodium for XChaCha encryption
|
||||
implementation "com.goterl:lazysodium-android:5.1.0@aar"
|
||||
|
||||
@@ -22,6 +22,10 @@ abstract class GeneralListEvent(
|
||||
fun bookmarkedPosts() = taggedEvents()
|
||||
fun bookmarkedPeople() = taggedUsers()
|
||||
|
||||
fun name() = tags.firstOrNull { it.size > 1 && it[0] == "name" }?.get(1)
|
||||
fun title() = tags.firstOrNull { it.size > 1 && it[0] == "title" }?.get(1)
|
||||
fun nameOrTitle() = name() ?: title()
|
||||
|
||||
fun plainContent(privKey: ByteArray): String? {
|
||||
if (content.isBlank()) return null
|
||||
|
||||
|
||||
@@ -28,6 +28,8 @@ class LiveActivitiesEvent(
|
||||
|
||||
fun participants() = tags.filter { it.size > 1 && it[0] == "p" }.map { Participant(it[1], it.getOrNull(3)) }
|
||||
|
||||
fun host() = tags.firstOrNull { it.size > 3 && it[0] == "p" && it[3].equals("Host", true) }?.get(1)
|
||||
|
||||
fun checkStatus(eventStatus: String?): String? {
|
||||
return if (eventStatus == STATUS_LIVE && createdAt < TimeUtils.eightHoursAgo()) {
|
||||
STATUS_ENDED
|
||||
|
||||
Reference in New Issue
Block a user