Merge branch 'vitorpamplona:main' into NIP90-ContentDiscovery

This commit is contained in:
believethehype
2024-05-14 06:29:19 +02:00
committed by GitHub
19 changed files with 392 additions and 77 deletions
@@ -148,7 +148,7 @@ val DefaultReactions =
"\uD83D\uDE31",
)
val DefaultZapAmounts = listOf(500L, 1000L, 5000L)
val DefaultZapAmounts = listOf(100L, 500L, 1000L)
fun getLanguagesSpokenByUser(): Set<String> {
val languageList = ConfigurationCompat.getLocales(Resources.getSystem().getConfiguration())
@@ -2435,6 +2435,10 @@ class Account(
return (activeRelays() ?: convertLocalRelays()).filter { it.write }
}
fun activeAllRelays(): List<Relay> {
return ((activeRelays() ?: convertLocalRelays()).toList())
}
fun isAllHidden(users: Set<HexKey>): Boolean {
return users.all { isHidden(it) }
}
@@ -103,7 +103,7 @@ class ZapPaymentHandler(val account: Account) {
}
if (account.hasWalletConnectSetup()) {
payViaNWC(it.values.map { it.second }, note, onError, onProgress = {
payViaNWC(it.values.map { it.invoice }, note, onError, onProgress = {
onProgress(it * 0.25f + 0.75f) // keeps within range.
}, context) {
// onProgress(1f)
@@ -113,9 +113,9 @@ class ZapPaymentHandler(val account: Account) {
it.map {
Payable(
info = it.key.first,
user = null,
amountMilliSats = it.value.first,
invoice = it.value.second,
user = it.key.second.user,
amountMilliSats = it.value.zapValue,
invoice = it.value.invoice,
)
}.toImmutableList(),
)
@@ -136,28 +136,33 @@ class ZapPaymentHandler(val account: Account) {
return roundedZapValue
}
class SignAllZapRequestsReturn(
val zapRequestJson: String,
val user: User? = null,
)
suspend fun signAllZapRequests(
note: Note,
pollOption: Int?,
message: String,
zapType: LnZapEvent.ZapType,
zapsToSend: List<ZapSplitSetup>,
onAllDone: suspend (MutableMap<ZapSplitSetup, String>) -> Unit,
onAllDone: suspend (MutableMap<ZapSplitSetup, SignAllZapRequestsReturn>) -> Unit,
) {
collectSuccessfulSigningOperations<ZapSplitSetup, String>(
collectSuccessfulSigningOperations<ZapSplitSetup, SignAllZapRequestsReturn>(
operationsInput = zapsToSend,
runRequestFor = { next: ZapSplitSetup, onReady ->
if (next.isLnAddress) {
prepareZapRequestIfNeeded(note, pollOption, message, zapType) { zapRequestJson ->
if (zapRequestJson != null) {
onReady(zapRequestJson)
onReady(SignAllZapRequestsReturn(zapRequestJson))
}
}
} else {
val user = LocalCache.getUserIfExists(next.lnAddressOrPubKeyHex)
prepareZapRequestIfNeeded(note, pollOption, message, zapType, user) { zapRequestJson ->
if (zapRequestJson != null) {
onReady(zapRequestJson)
onReady(SignAllZapRequestsReturn(zapRequestJson, user))
}
}
}
@@ -167,23 +172,23 @@ class ZapPaymentHandler(val account: Account) {
}
suspend fun assembleAllInvoices(
invoices: List<Pair<ZapSplitSetup, String>>,
invoices: List<Pair<ZapSplitSetup, SignAllZapRequestsReturn>>,
totalAmountMilliSats: Long,
message: String,
onError: (String, String) -> Unit,
onProgress: (percent: Float) -> Unit,
context: Context,
onAllDone: suspend (MutableMap<Pair<ZapSplitSetup, String>, Pair<Long, String>>) -> Unit,
onAllDone: suspend (MutableMap<Pair<ZapSplitSetup, SignAllZapRequestsReturn>, AssembleInvoiceReturn>) -> Unit,
) {
var progressAllPayments = 0.00f
val totalWeight = invoices.sumOf { it.first.weight }
collectSuccessfulSigningOperations<Pair<ZapSplitSetup, String>, Pair<Long, String>>(
collectSuccessfulSigningOperations<Pair<ZapSplitSetup, SignAllZapRequestsReturn>, AssembleInvoiceReturn>(
operationsInput = invoices,
runRequestFor = { splitZapRequestPair: Pair<ZapSplitSetup, String>, onReady ->
runRequestFor = { splitZapRequestPair: Pair<ZapSplitSetup, SignAllZapRequestsReturn>, onReady ->
assembleInvoice(
splitSetup = splitZapRequestPair.first,
nostrZapRequest = splitZapRequestPair.second,
nostrZapRequest = splitZapRequestPair.second.zapRequestJson,
zapValue = calculateZapValue(totalAmountMilliSats, splitZapRequestPair.first.weight, totalWeight),
message = message,
onError = onError,
@@ -243,6 +248,11 @@ class ZapPaymentHandler(val account: Account) {
)
}
class AssembleInvoiceReturn(
val zapValue: Long,
val invoice: String,
)
private fun assembleInvoice(
splitSetup: ZapSplitSetup,
nostrZapRequest: String,
@@ -251,7 +261,7 @@ class ZapPaymentHandler(val account: Account) {
onError: (String, String) -> Unit,
onProgressStep: (percent: Float) -> Unit,
context: Context,
onReady: (Pair<Long, String>) -> Unit,
onReady: (AssembleInvoiceReturn) -> Unit,
) {
var progressThisPayment = 0.00f
@@ -280,7 +290,7 @@ class ZapPaymentHandler(val account: Account) {
context = context,
onSuccess = {
onProgressStep(1 - progressThisPayment)
onReady(Pair(zapValue, it))
onReady(AssembleInvoiceReturn(zapValue, it))
},
)
} else {
@@ -449,6 +449,38 @@ class Relay(
}
}
// This function sends the event regardless of the relay being write or not.
fun sendOverride(signedEvent: EventInterface) {
checkNotInMainThread()
if (signedEvent is RelayAuthEvent) {
authResponse.put(signedEvent.id, false)
// specific protocol for this event.
val event = """["AUTH",${signedEvent.toJson()}]"""
socket?.send(event)
eventUploadCounterInBytes += event.bytesUsedInMemory()
} else {
val event = """["EVENT",${signedEvent.toJson()}]"""
if (isConnected()) {
if (isReady) {
socket?.send(event)
eventUploadCounterInBytes += event.bytesUsedInMemory()
}
} else {
// sends all filters after connection is successful.
connectAndRun {
checkNotInMainThread()
socket?.send(event)
eventUploadCounterInBytes += event.bytesUsedInMemory()
// Sends everything.
renewFilters()
}
}
}
}
fun send(signedEvent: EventInterface) {
checkNotInMainThread()
@@ -150,13 +150,17 @@ object RelayPool : Relay.Listener {
list: List<Relay>,
signedEvent: EventInterface,
) {
list.forEach { relay -> relays.filter { it.url == relay.url }.forEach { it.send(signedEvent) } }
list.forEach { relay -> relays.filter { it.url == relay.url }.forEach { it.sendOverride(signedEvent) } }
}
fun send(signedEvent: EventInterface) {
relays.forEach { it.send(signedEvent) }
}
fun sendOverride(signedEvent: EventInterface) {
relays.forEach { it.sendOverride(signedEvent) }
}
fun close(subscriptionId: String) {
relays.forEach { it.close(subscriptionId) }
}
@@ -78,7 +78,7 @@ fun RelaySelectionDialog(
var relays by remember {
mutableStateOf(
accountViewModel.account.activeWriteRelays().map {
accountViewModel.account.activeAllRelays().map {
RelayList(
relay = it,
relayInfo = RelayBriefInfoCache.RelayBriefInfo(it.url),
@@ -79,7 +79,7 @@ fun ClickableWithdrawal(withdrawalString: String) {
ClickableText(
text = withdraw,
onClick = { payViaIntent(withdrawalString, context) { showErrorMessageDialog = it } },
onClick = { payViaIntent(withdrawalString, context, { }) { showErrorMessageDialog = it } },
style = LocalTextStyle.current.copy(color = MaterialTheme.colorScheme.primary),
)
}
@@ -26,7 +26,6 @@ import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.Button
import androidx.compose.material3.ButtonDefaults
import androidx.compose.material3.MaterialTheme
@@ -42,13 +41,13 @@ import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import com.vitorpamplona.amethyst.R
import com.vitorpamplona.amethyst.commons.richtext.ExpandableTextCutOffCalculator
import com.vitorpamplona.amethyst.ui.note.getGradient
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
import com.vitorpamplona.amethyst.ui.theme.ButtonBorder
import com.vitorpamplona.amethyst.ui.theme.ButtonPadding
import com.vitorpamplona.amethyst.ui.theme.StdTopPadding
import com.vitorpamplona.amethyst.ui.theme.secondaryButtonBackground
import com.vitorpamplona.quartz.events.ImmutableListOfLists
@@ -125,7 +124,7 @@ fun ExpandableRichTextViewer(
@Composable
fun ShowMoreButton(onClick: () -> Unit) {
Button(
modifier = Modifier.padding(top = 10.dp),
modifier = StdTopPadding,
onClick = onClick,
shape = ButtonBorder,
colors =
@@ -169,7 +169,7 @@ fun InvoicePreview(
Modifier
.fillMaxWidth()
.padding(vertical = 10.dp),
onClick = { payViaIntent(lnInvoice, context) { showErrorMessageDialog = it } },
onClick = { payViaIntent(lnInvoice, context, { }) { showErrorMessageDialog = it } },
shape = QuoteBorder,
colors =
ButtonDefaults.buttonColors(
@@ -46,12 +46,12 @@ class ChatroomListNewFeedFilter(val account: Account) : AdditiveFeedFilter<Note>
val privateMessages =
newChatrooms.mapNotNull { it ->
it.value.roomMessages.sortedWith(compareBy({ it.createdAt() }, { it.idHex })).lastOrNull {
it.value.roomMessages.sortedWith(DefaultFeedOrder).firstOrNull {
it.event != null
}
}
return privateMessages.sortedWith(compareBy({ it.createdAt() }, { it.idHex })).reversed()
return privateMessages.sortedWith(DefaultFeedOrder)
}
override fun updateListWith(
@@ -21,5 +21,23 @@
package com.vitorpamplona.amethyst.ui.dal
import com.vitorpamplona.amethyst.model.Note
import com.vitorpamplona.quartz.events.Event
val DefaultFeedOrder = compareBy<Note>({ it.createdAt() }, { it.idHex }).reversed()
val DefaultFeedOrder: Comparator<Note> =
compareBy<Note>(
{
val noteEvent = it.event
if (noteEvent == null) {
null
} else {
if (noteEvent is Event) {
noteEvent.createdAt
} else {
null
}
}
},
{
it.idHex
},
).reversed()
@@ -439,6 +439,15 @@ fun ZapVote(
)
}
},
justShowError = {
scope.launch {
showErrorMessageDialog =
StringToastMsg(
context.getString(R.string.error_dialog_zap_error),
it,
)
}
},
)
}
@@ -140,9 +140,6 @@ import kotlinx.collections.immutable.toImmutableMap
import kotlinx.collections.immutable.toImmutableSet
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import java.math.BigDecimal
import java.math.RoundingMode
import java.text.DecimalFormat
import kotlin.math.roundToInt
@Composable
@@ -985,7 +982,8 @@ fun ZapReaction(
if (wantsToZap) {
ZapAmountChoicePopup(
baseNote = baseNote,
iconSize = iconSize,
zapAmountChoices = accountViewModel.account.zapAmountChoices,
popupYOffset = iconSize,
accountViewModel = accountViewModel,
onDismiss = {
wantsToZap = false
@@ -1042,6 +1040,11 @@ fun ZapReaction(
showErrorMessageDialog = showErrorMessageDialog + it
}
},
justShowError = {
scope.launch {
showErrorMessageDialog = showErrorMessageDialog + it
}
},
)
}
@@ -1430,8 +1433,9 @@ private fun ActionableReactionButton(
@Composable
fun ZapAmountChoicePopup(
baseNote: Note,
zapAmountChoices: List<Long>,
accountViewModel: AccountViewModel,
iconSize: Dp,
popupYOffset: Dp,
onDismiss: () -> Unit,
onChangeAmount: () -> Unit,
onError: (title: String, text: String) -> Unit,
@@ -1441,15 +1445,15 @@ fun ZapAmountChoicePopup(
val context = LocalContext.current
val zapMessage = ""
val iconSizePx = with(LocalDensity.current) { -iconSize.toPx().toInt() }
val yOffset = with(LocalDensity.current) { -popupYOffset.toPx().toInt() }
Popup(
alignment = Alignment.BottomCenter,
offset = IntOffset(0, iconSizePx),
offset = IntOffset(0, yOffset),
onDismissRequest = { onDismiss() },
) {
FlowRow(horizontalArrangement = Arrangement.Center) {
accountViewModel.account.zapAmountChoices.forEach { amountInSats ->
zapAmountChoices.forEach { amountInSats ->
Button(
modifier = Modifier.padding(horizontal = 3.dp),
onClick = {
@@ -1512,25 +1516,3 @@ fun showCount(count: Int?): String {
else -> "$count"
}
}
val OneGiga = BigDecimal(1000000000)
val OneMega = BigDecimal(1000000)
val TenKilo = BigDecimal(10000)
val OneKilo = BigDecimal(1000)
var dfG: DecimalFormat = DecimalFormat("#.0G")
var dfM: DecimalFormat = DecimalFormat("#.0M")
var dfK: DecimalFormat = DecimalFormat("#.0k")
var dfN: DecimalFormat = DecimalFormat("#")
fun showAmount(amount: BigDecimal?): String {
if (amount == null) return ""
if (amount.abs() < BigDecimal(0.01)) return ""
return when {
amount >= OneGiga -> dfG.format(amount.div(OneGiga).setScale(0, RoundingMode.HALF_UP))
amount >= OneMega -> dfM.format(amount.div(OneMega).setScale(0, RoundingMode.HALF_UP))
amount >= TenKilo -> dfK.format(amount.div(OneKilo).setScale(0, RoundingMode.HALF_UP))
else -> dfN.format(amount)
}
}
@@ -347,12 +347,12 @@ fun PayViaIntentDialog(
accountViewModel: AccountViewModel,
onClose: () -> Unit,
onError: (String) -> Unit,
justShowError: (String) -> Unit,
) {
val context = LocalContext.current
if (payingInvoices.size == 1) {
payViaIntent(payingInvoices.first().invoice, context, onError)
onClose()
payViaIntent(payingInvoices.first().invoice, context, onClose, onError)
} else {
Dialog(
onDismissRequest = onClose,
@@ -422,9 +422,7 @@ fun PayViaIntentDialog(
Spacer(modifier = DoubleHorzSpacer)
PayButton(isActive = !paid.value) {
paid.value = true
payViaIntent(it.invoice, context, onError)
payViaIntent(it.invoice, context, { paid.value = true }, justShowError)
}
}
}
@@ -437,6 +435,7 @@ fun PayViaIntentDialog(
fun payViaIntent(
invoice: String,
context: Context,
onPaid: () -> Unit,
onError: (String) -> Unit,
) {
try {
@@ -444,6 +443,7 @@ fun payViaIntent(
intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK
ContextCompat.startActivity(context, intent, null)
onPaid()
} catch (e: Exception) {
if (e is CancellationException) throw e
// don't display ugly error messages
@@ -0,0 +1,76 @@
/**
* Copyright (c) 2024 Vitor Pamplona
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
* the Software without restriction, including without limitation the rights to use,
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
* Software, and to permit persons to whom the Software is furnished to do so,
* subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
package com.vitorpamplona.amethyst.ui.note
import java.math.BigDecimal
import java.math.RoundingMode
import java.text.DecimalFormat
val TenGiga = BigDecimal(10000000000)
val OneGiga = BigDecimal(1000000000)
val TenMega = BigDecimal(10000000)
val OneMega = BigDecimal(1000000)
val TenKilo = BigDecimal(10000)
val OneKilo = BigDecimal(1000)
private val dfGBig =
object : ThreadLocal<DecimalFormat>() {
override fun initialValue() = DecimalFormat("#.#G")
}
private val dfGSmall =
object : ThreadLocal<DecimalFormat>() {
override fun initialValue() = DecimalFormat("#.0G")
}
private val dfMBig =
object : ThreadLocal<DecimalFormat>() {
override fun initialValue() = DecimalFormat("#.#M")
}
private val dfMSmall =
object : ThreadLocal<DecimalFormat>() {
override fun initialValue() = DecimalFormat("#.0M")
}
private val dfK =
object : ThreadLocal<DecimalFormat>() {
override fun initialValue() = DecimalFormat("#.#k")
}
private val dfN =
object : ThreadLocal<DecimalFormat>() {
override fun initialValue() = DecimalFormat("#")
}
fun showAmount(amount: BigDecimal?): String {
if (amount == null) return ""
if (amount.abs() < BigDecimal(0.01)) return ""
return when {
amount >= TenGiga -> dfGBig.get().format(amount.div(OneGiga).setScale(0, RoundingMode.HALF_UP))
amount >= OneGiga -> dfGSmall.get().format(amount.div(OneGiga).setScale(0, RoundingMode.HALF_UP))
amount >= TenMega -> dfMBig.get().format(amount.div(OneMega).setScale(0, RoundingMode.HALF_UP))
amount >= OneMega -> dfMSmall.get().format(amount.div(OneMega).setScale(0, RoundingMode.HALF_UP))
amount >= TenKilo -> dfK.get().format(amount.div(OneKilo).setScale(0, RoundingMode.HALF_UP))
else -> dfN.get().format(amount)
}
}
@@ -20,6 +20,7 @@
*/
package com.vitorpamplona.amethyst.ui.note
import android.content.Context
import androidx.compose.animation.Crossfade
import androidx.compose.animation.core.animateFloatAsState
import androidx.compose.foundation.layout.Arrangement
@@ -77,7 +78,7 @@ import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
import com.vitorpamplona.amethyst.ui.theme.ModifierWidth3dp
import com.vitorpamplona.amethyst.ui.theme.Size10dp
import com.vitorpamplona.amethyst.ui.theme.Size20Modifier
import com.vitorpamplona.amethyst.ui.theme.Size20dp
import com.vitorpamplona.amethyst.ui.theme.Size35dp
import com.vitorpamplona.amethyst.ui.theme.StdVertSpacer
import com.vitorpamplona.amethyst.ui.theme.ThemeComparisonColumn
import com.vitorpamplona.amethyst.ui.theme.imageModifier
@@ -301,12 +302,12 @@ fun ZapDonationButton(
baseNote: Note,
grayTint: Color,
accountViewModel: AccountViewModel,
iconSize: Dp = Size20dp,
iconSize: Dp = Size35dp,
iconSizeModifier: Modifier = Size20Modifier,
animationSize: Dp = 14.dp,
nav: (String) -> Unit,
) {
var wantsToZap by remember { mutableStateOf(false) }
var wantsToZap by remember { mutableStateOf<List<Long>?>(null) }
var showErrorMessageDialog by remember { mutableStateOf<String?>(null) }
var wantsToPay by
remember(baseNote) {
@@ -323,14 +324,14 @@ fun ZapDonationButton(
Button(
onClick = {
zapClick(
customZapClick(
baseNote,
accountViewModel,
context,
onZappingProgress = { progress: Float ->
scope.launch { zappingProgress = progress }
},
onMultipleChoices = { wantsToZap = true },
onMultipleChoices = { options -> wantsToZap = options },
onError = { _, message ->
scope.launch {
zappingProgress = 0f
@@ -342,17 +343,18 @@ fun ZapDonationButton(
},
modifier = Modifier.fillMaxWidth(),
) {
if (wantsToZap) {
if (wantsToZap != null) {
ZapAmountChoicePopup(
baseNote = baseNote,
iconSize = iconSize,
zapAmountChoices = wantsToZap ?: accountViewModel.account.zapAmountChoices,
popupYOffset = iconSize,
accountViewModel = accountViewModel,
onDismiss = {
wantsToZap = false
wantsToZap = null
zappingProgress = 0f
},
onChangeAmount = {
wantsToZap = false
wantsToZap = null
},
onError = { _, message ->
scope.launch {
@@ -395,6 +397,11 @@ fun ZapDonationButton(
showErrorMessageDialog = it
}
},
justShowError = {
scope.launch {
showErrorMessageDialog = it
}
},
)
}
@@ -448,3 +455,58 @@ fun ZapDonationButton(
}
}
}
fun customZapClick(
baseNote: Note,
accountViewModel: AccountViewModel,
context: Context,
onZappingProgress: (Float) -> Unit,
onMultipleChoices: (List<Long>) -> Unit,
onError: (String, String) -> Unit,
onPayViaIntent: (ImmutableList<ZapPaymentHandler.Payable>) -> Unit,
) {
if (baseNote.isDraft()) {
accountViewModel.toast(
R.string.draft_note,
R.string.it_s_not_possible_to_zap_to_a_draft_note,
)
return
}
if (accountViewModel.account.zapAmountChoices.isEmpty()) {
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.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) {
val amount = accountViewModel.account.zapAmountChoices.first()
if (amount > 600) {
accountViewModel.zap(
baseNote,
amount * 1000,
null,
"",
context,
onError = onError,
onProgress = { onZappingProgress(it) },
zapType = accountViewModel.account.defaultZapType,
onPayViaIntent = onPayViaIntent,
)
} else {
onMultipleChoices(listOf(1000L, 5_000L, 10_000L))
// recommends amounts for a monthly release.
}
} else if (accountViewModel.account.zapAmountChoices.size > 1) {
if (accountViewModel.account.zapAmountChoices.any { it > 600 }) {
onMultipleChoices(accountViewModel.account.zapAmountChoices)
} else {
onMultipleChoices(listOf(1000L, 5_000L, 10_000L))
}
}
}
@@ -1193,7 +1193,7 @@ fun DisplayLNAddress(
}
}
} else {
payViaIntent(it, context) { showErrorMessageDialog = it }
payViaIntent(it, context, { zapExpanded = false }, { showErrorMessageDialog = it })
}
},
onClose = { zapExpanded = false },