Performance Improvements for Zaps in Polls.

This commit is contained in:
Vitor Pamplona
2023-04-19 10:33:02 -04:00
parent 5afa333fab
commit 4e09d9e54a
8 changed files with 79 additions and 96 deletions

View File

@@ -595,7 +595,7 @@ object LocalCache {
// Already processed this event. // Already processed this event.
if (note.event != null) return if (note.event != null) return
val zapRequest = event.containedPost()?.id?.let { getOrCreateNote(it) } val zapRequest = event.zapRequest?.id?.let { getOrCreateNote(it) }
val author = getOrCreateUser(event.pubKey) val author = getOrCreateUser(event.pubKey)
val mentions = event.zappedAuthor().mapNotNull { checkGetOrCreateUser(it) } val mentions = event.zappedAuthor().mapNotNull { checkGetOrCreateUser(it) }

View File

@@ -76,7 +76,7 @@ abstract class NostrDataSource(val debugName: String) {
is DeletionEvent -> LocalCache.consume(event) is DeletionEvent -> LocalCache.consume(event)
is LnZapEvent -> { is LnZapEvent -> {
event.containedPost()?.let { onEvent(it, subscriptionId, relay) } event.zapRequest?.let { onEvent(it, subscriptionId, relay) }
LocalCache.consume(event) LocalCache.consume(event)
} }
is LnZapRequestEvent -> LocalCache.consume(event) is LnZapRequestEvent -> LocalCache.consume(event)

View File

@@ -4,7 +4,6 @@ import android.util.Log
import com.vitorpamplona.amethyst.model.HexKey import com.vitorpamplona.amethyst.model.HexKey
import com.vitorpamplona.amethyst.service.lnurl.LnInvoiceUtil import com.vitorpamplona.amethyst.service.lnurl.LnInvoiceUtil
import com.vitorpamplona.amethyst.service.relays.Client import com.vitorpamplona.amethyst.service.relays.Client
import java.math.BigDecimal
class LnZapEvent( class LnZapEvent(
id: HexKey, id: HexKey,
@@ -14,25 +13,37 @@ class LnZapEvent(
content: String, content: String,
sig: HexKey sig: HexKey
) : LnZapEventInterface, Event(id, pubKey, createdAt, kind, tags, content, sig) { ) : LnZapEventInterface, Event(id, pubKey, createdAt, kind, tags, content, sig) {
// This event is also kept in LocalCache (same object)
@Transient val zapRequest: LnZapRequestEvent?
override fun zappedPost() = tags private fun containedPost(): LnZapRequestEvent? = try {
.filter { it.firstOrNull() == "e" } description()?.ifBlank { null }?.let {
.mapNotNull { it.getOrNull(1) } fromJson(it, Client.lenient)
} as? LnZapRequestEvent
override fun zappedPollOption(): Int? = containedPost()?.tags } catch (e: Exception) {
?.filter { it.firstOrNull() == POLL_OPTION } Log.e("LnZapEvent", "Failed to Parse Contained Post ${description()}", e)
?.getOrNull(0)?.getOrNull(1)?.toInt() null
override fun zappedAuthor() = tags
.filter { it.firstOrNull() == "p" }
.mapNotNull { it.getOrNull(1) }
override fun zappedRequestAuthor(): String? = containedPost()?.pubKey()
override fun amount(): BigDecimal? {
return amount
} }
init {
zapRequest = containedPost()
}
override fun zappedPost() = tags.filter { it.size > 1 && it[0] == "e" }.map { it[1] }
override fun zappedAuthor() = tags.filter { it.size > 1 && it[0] == "p" }.map { it[1] }
override fun zappedPollOption(): Int? = try {
zapRequest?.tags?.firstOrNull { it.size > 1 && it[0] == POLL_OPTION }?.get(1)?.toInt()
} catch (e: Exception) {
Log.e("LnZapEvent", "ZappedPollOption failed to parse", e)
null
}
override fun zappedRequestAuthor(): String? = zapRequest?.pubKey()
override fun amount() = amount
// Keeps this as a field because it's a heavier function used everywhere. // Keeps this as a field because it's a heavier function used everywhere.
val amount by lazy { val amount by lazy {
try { try {
@@ -42,29 +53,14 @@ class LnZapEvent(
null null
} }
} }
override fun message(): String { override fun message(): String {
return message return content
}
val message = content
override fun containedPost(): Event? = try {
description()?.ifBlank { null }?.let {
fromJson(it, Client.lenient)
}
} catch (e: Exception) {
Log.e("LnZapEvent", "Failed to Parse Contained Post ${description()}", e)
null
} }
private fun lnInvoice(): String? = tags private fun lnInvoice() = tags.firstOrNull { it.size > 1 && it[0] == "bolt11" }?.get(1)
.filter { it.firstOrNull() == "bolt11" }
.mapNotNull { it.getOrNull(1) }
.firstOrNull()
private fun description(): String? = tags private fun description() = tags.firstOrNull { it.size > 1 && it[0] == "description" }?.get(1)
.filter { it.firstOrNull() == "description" }
.mapNotNull { it.getOrNull(1) }
.firstOrNull()
companion object { companion object {
const val kind = 9735 const val kind = 9735

View File

@@ -16,7 +16,5 @@ interface LnZapEventInterface : EventInterface {
fun amount(): BigDecimal? fun amount(): BigDecimal?
fun containedPost(): Event?
fun message(): String fun message(): String
} }

View File

@@ -13,8 +13,10 @@ class LnZapRequestEvent(
content: String, content: String,
sig: HexKey sig: HexKey
) : Event(id, pubKey, createdAt, kind, tags, content, sig) { ) : Event(id, pubKey, createdAt, kind, tags, content, sig) {
fun zappedPost() = tags.filter { it.firstOrNull() == "e" }.mapNotNull { it.getOrNull(1) }
fun zappedAuthor() = tags.filter { it.firstOrNull() == "p" }.mapNotNull { it.getOrNull(1) } fun zappedPost() = tags.filter { it.size > 1 && it[0] == "e" }.map { it[1] }
fun zappedAuthor() = tags.filter { it.size > 1 && it[0] == "p" }.map { it[1] }
companion object { companion object {
const val kind = 9734 const val kind = 9734

View File

@@ -20,24 +20,17 @@ class PollNoteEvent(
content: String, content: String,
sig: HexKey sig: HexKey
) : BaseTextNoteEvent(id, pubKey, createdAt, kind, tags, content, sig) { ) : BaseTextNoteEvent(id, pubKey, createdAt, kind, tags, content, sig) {
fun pollOptions(): Map<Int, String> { fun pollOptions() =
val map = mutableMapOf<Int, String>() tags.filter { it.size > 2 && it[0] == POLL_OPTION }
tags.filter { it.first() == POLL_OPTION } .associate { it[1].toInt() to it[2] }
.forEach { map[it[1].toInt()] = it[2] }
return map
}
fun getTagInt(property: String): Int? { fun getTagInt(property: String): Int? {
val tagList = tags.filter { val number = tags.firstOrNull() { it.size > 1 && it[0] == property }?.get(1)
it.firstOrNull() == property
}
val tag = tagList.getOrNull(0)
val s = tag?.getOrNull(1)
return if (s.isNullOrBlank() || s == "null") { return if (number.isNullOrBlank() || number == "null") {
null null
} else { } else {
s.toInt() number.toInt()
} }
} }

View File

@@ -48,9 +48,6 @@ fun PollNote(
accountViewModel: AccountViewModel, accountViewModel: AccountViewModel,
navController: NavController navController: NavController
) { ) {
val accountState by accountViewModel.accountLiveData.observeAsState()
val account = accountState?.account ?: return
val zapsState by baseNote.live().zaps.observeAsState() val zapsState by baseNote.live().zaps.observeAsState()
val zappedNote = zapsState?.note ?: return val zappedNote = zapsState?.note ?: return
@@ -72,7 +69,7 @@ fun PollNote(
verticalAlignment = Alignment.CenterVertically, verticalAlignment = Alignment.CenterVertically,
modifier = Modifier.padding(vertical = 3.dp) modifier = Modifier.padding(vertical = 3.dp)
) { ) {
if (zappedNote.author == account.userProfile() || zappedNote.isZappedBy(account.userProfile())) { if (accountViewModel.isLoggedUser(zappedNote.author) || zappedNote.isZappedBy(accountViewModel.userProfile())) {
ZapVote( ZapVote(
baseNote, baseNote,
accountViewModel, accountViewModel,
@@ -90,7 +87,7 @@ fun PollNote(
LinearProgressIndicator( LinearProgressIndicator(
modifier = Modifier.matchParentSize(), modifier = Modifier.matchParentSize(),
color = color, color = color,
progress = optionTally progress = optionTally.toFloat()
) )
Row( Row(
@@ -101,7 +98,7 @@ fun PollNote(
modifier = Modifier.padding(horizontal = 10.dp).width(40.dp) modifier = Modifier.padding(horizontal = 10.dp).width(40.dp)
) { ) {
Text( Text(
text = "${(optionTally * 100).roundToInt()}%", text = "${(optionTally.toFloat() * 100).roundToInt()}%",
fontWeight = FontWeight.Bold fontWeight = FontWeight.Bold
) )
} }
@@ -210,7 +207,7 @@ fun ZapVote(
) )
.show() .show()
} }
} else if (zappedNote?.author == account.userProfile()) { } else if (accountViewModel.isLoggedUser(zappedNote?.author)) {
scope.launch { scope.launch {
Toast Toast
.makeText( .makeText(

View File

@@ -5,6 +5,7 @@ import com.vitorpamplona.amethyst.model.Note
import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.model.User
import com.vitorpamplona.amethyst.service.model.* import com.vitorpamplona.amethyst.service.model.*
import java.math.BigDecimal import java.math.BigDecimal
import java.math.RoundingMode
import java.util.* import java.util.*
class PollNoteViewModel { class PollNoteViewModel {
@@ -16,7 +17,9 @@ class PollNoteViewModel {
var valueMaximum: Int? = null var valueMaximum: Int? = null
var valueMinimum: Int? = null var valueMinimum: Int? = null
private var closedAt: Int? = null private var closedAt: Int? = null
var consensusThreshold: Float? = null var consensusThreshold: BigDecimal? = null
var totalZapped: BigDecimal = BigDecimal.ZERO
fun load(note: Note?) { fun load(note: Note?) {
pollNote = note pollNote = note
@@ -24,8 +27,10 @@ class PollNoteViewModel {
pollOptions = pollEvent?.pollOptions() pollOptions = pollEvent?.pollOptions()
valueMaximum = pollEvent?.getTagInt(VALUE_MAXIMUM) valueMaximum = pollEvent?.getTagInt(VALUE_MAXIMUM)
valueMinimum = pollEvent?.getTagInt(VALUE_MINIMUM) valueMinimum = pollEvent?.getTagInt(VALUE_MINIMUM)
consensusThreshold = pollEvent?.getTagInt(CONSENSUS_THRESHOLD)?.toFloat()?.div(100) consensusThreshold = pollEvent?.getTagInt(CONSENSUS_THRESHOLD)?.toFloat()?.div(100)?.toBigDecimal()
closedAt = pollEvent?.getTagInt(CLOSED_AT) closedAt = pollEvent?.getTagInt(CLOSED_AT)
totalZapped = totalZapped()
} }
fun isVoteAmountAtomic() = valueMaximum != null && valueMinimum != null && valueMinimum == valueMaximum fun isVoteAmountAtomic() = valueMaximum != null && valueMinimum != null && valueMinimum == valueMaximum
@@ -73,47 +78,39 @@ class PollNoteViewModel {
return false return false
} }
fun optionVoteTally(op: Int): Float { fun optionVoteTally(op: Int): BigDecimal {
val tally = zappedPollOptionAmount(op).toFloat().div(zappedVoteTotal()) return if (totalZapped.compareTo(BigDecimal.ZERO) > 0) {
return if (tally.isNaN()) { // catch div by 0 zappedPollOptionAmount(op).divide(totalZapped, 2, RoundingMode.HALF_UP)
0f } else {
} else { tally } BigDecimal.ZERO
} }
private fun zappedVoteTotal(): Float {
var total = 0f
pollOptions?.keys?.forEach {
total += zappedPollOptionAmount(it).toFloat()
}
return total
} }
fun isPollOptionZappedBy(option: Int, user: User): Boolean { fun isPollOptionZappedBy(option: Int, user: User): Boolean {
if (pollNote?.zaps?.any { it.key.author == user } == true) { if (pollNote?.zaps?.any { it.key.author === user } == true) {
pollNote!!.zaps.mapNotNull { it.value?.event } pollNote!!.zaps
.filterIsInstance<LnZapEvent>() .any {
.map { val event = it.value?.event as? LnZapEvent
val zappedOption = it.zappedPollOption() event?.zappedPollOption() == option && event.zappedRequestAuthor() == user.pubkeyHex
if (zappedOption == option && it.zappedRequestAuthor() == user.pubkeyHex) {
return true
}
} }
} }
return false return false
} }
fun zappedPollOptionAmount(option: Int): BigDecimal { fun zappedPollOptionAmount(option: Int): BigDecimal {
return if (pollNote != null) { return pollNote?.zaps?.values?.sumOf {
pollNote!!.zaps.mapNotNull { it.value?.event } val event = it?.event as? LnZapEvent
.filterIsInstance<LnZapEvent>() if (event?.zappedPollOption() == option) {
.mapNotNull { event.amount ?: BigDecimal(0)
val zappedOption = it.zappedPollOption()
if (zappedOption == option) {
it.amount
} else { null }
}.sumOf { it }
} else { } else {
BigDecimal(0) BigDecimal(0)
} }
} ?: BigDecimal(0)
}
fun totalZapped(): BigDecimal {
return pollNote?.zaps?.values?.sumOf {
(it?.event as? LnZapEvent)?.amount ?: BigDecimal(0)
} ?: BigDecimal(0)
} }
} }