Caching total Zaps

This commit is contained in:
Vitor Pamplona
2023-11-09 14:26:11 -05:00
parent 9377c902a1
commit 43d03bc109
6 changed files with 173 additions and 196 deletions
@@ -581,7 +581,7 @@ class Account(
fun calculateZappedAmount(zappedNote: Note?): BigDecimal {
val privKey = zapPaymentRequest?.secret?.hexToByteArray() ?: keyPair.privKey
val pubKey = zapPaymentRequest?.pubKeyHex?.hexToByteArray()
return zappedNote?.zappedAmount(privKey, pubKey) ?: BigDecimal.ZERO
return zappedNote?.zappedAmountWithNWCPayments(privKey, pubKey) ?: BigDecimal.ZERO
}
fun sendZapPaymentRequestFor(bolt11: String, zappedNote: Note?, onResponse: (Response?) -> Unit) {
@@ -86,6 +86,8 @@ open class Note(val idHex: String) {
private set
var zaps = mapOf<Note, Note?>()
private set
var zapsAmount: BigDecimal = BigDecimal.ZERO
var zapPayments = mapOf<Note, Note?>()
private set
@@ -265,6 +267,7 @@ open class Note(val idHex: String) {
reports = mapOf<User, List<Note>>()
zaps = mapOf<Note, Note?>()
zapPayments = mapOf<Note, Note?>()
zapsAmount = BigDecimal.ZERO
relays = listOf<String>()
lastReactionsDownloadTime = emptyMap()
@@ -311,9 +314,11 @@ open class Note(val idHex: String) {
fun removeZap(note: Note) {
if (zaps[note] != null) {
zaps = zaps.minus(note)
updateZapTotal()
liveSet?.innerZaps?.invalidateData()
} else if (zaps.containsValue(note)) {
zaps = zaps.filterValues { it != note }
updateZapTotal()
liveSet?.innerZaps?.invalidateData()
}
}
@@ -354,11 +359,13 @@ open class Note(val idHex: String) {
if (zapRequest !in zaps.keys) {
val inserted = innerAddZap(zapRequest, zap)
if (inserted) {
updateZapTotal()
liveSet?.innerZaps?.invalidateData()
}
} else if (zaps[zapRequest] == null) {
val inserted = innerAddZap(zapRequest, zap)
if (inserted) {
updateZapTotal()
liveSet?.innerZaps?.invalidateData()
}
}
@@ -478,44 +485,63 @@ open class Note(val idHex: String) {
}.flatten()
}
fun zappedAmount(privKey: ByteArray?, walletServicePubkey: ByteArray?): BigDecimal {
// Regular Zap Receipts
val completedZaps = zaps.asSequence()
.mapNotNull { it.value?.event }
.filterIsInstance<LnZapEvent>()
.filter { it.amount != null }
.associate {
it.lnInvoice() to it.amount
}
.toMap()
private fun updateZapTotal() {
var sumOfAmounts = BigDecimal.ZERO
// Regular Zap Receipts
zaps.values.forEach {
val noteEvent = it?.event
if (noteEvent is LnZapEvent) {
sumOfAmounts += noteEvent.amount ?: BigDecimal.ZERO
}
}
zapsAmount = sumOfAmounts
}
fun zappedAmountWithNWCPayments(privKey: ByteArray?, walletServicePubkey: ByteArray?): BigDecimal {
if (zapPayments.isEmpty()) return zapsAmount
var sumOfAmounts = zapsAmount
val invoiceSet = LinkedHashSet<String>(zaps.size + zapPayments.size)
zaps.forEach {
(it.value as? LnZapEvent)?.lnInvoice()?.let {
invoiceSet.add(it)
}
}
if (privKey != null && walletServicePubkey != null) {
zapPayments.forEach {
val noteEvent = (it.value?.event as? LnZapPaymentResponseEvent)?.response(
privKey,
walletServicePubkey
)
if (noteEvent is PayInvoiceSuccessResponse) {
val invoice = (it.key.event as? LnZapPaymentRequestEvent)?.lnInvoice(
privKey,
walletServicePubkey
)
val completedPayments = if (privKey != null && walletServicePubkey != null) {
// Payments confirmed by the User's Wallet
zapPayments
.asSequence()
.filter {
val response = (it.value?.event as? LnZapPaymentResponseEvent)?.response(privKey, walletServicePubkey)
response is PayInvoiceSuccessResponse
}
.associate {
val lnInvoice = (it.key.event as? LnZapPaymentRequestEvent)?.lnInvoice(privKey, walletServicePubkey)
val amount = try {
if (lnInvoice == null) {
if (invoice == null) {
null
} else {
LnInvoiceUtil.getAmountInSats(lnInvoice)
LnInvoiceUtil.getAmountInSats(invoice)
}
} catch (e: java.lang.Exception) {
null
}
lnInvoice to amount
if (invoice != null && amount != null && !invoiceSet.contains(invoice)) {
invoiceSet.add(invoice)
sumOfAmounts += amount
}
}
.toMap()
} else {
emptyMap()
}
}
return (completedZaps + completedPayments).values.filterNotNull().sumOf { it }
return sumOfAmounts
}
fun hasPledgeBy(user: User): Boolean {
@@ -626,6 +652,7 @@ open class Note(val idHex: String) {
boosts = emptyList()
reports = emptyMap()
zaps = emptyMap()
zapsAmount = BigDecimal.ZERO
}
fun clearEOSE() {
@@ -205,11 +205,6 @@ fun CommentIcon(iconSize: Dp, tint: Color) {
)
}
@Composable
fun ViewCountIcon(iconSize: Dp, tint: Color = Color.Unspecified) {
ViewCountIcon(remember { Modifier.size(iconSize) }, tint)
}
@Composable
fun ViewCountIcon(modifier: Modifier, tint: Color = Color.Unspecified) {
Icon(
@@ -12,6 +12,7 @@ import androidx.compose.animation.fadeIn
import androidx.compose.animation.fadeOut
import androidx.compose.animation.slideInVertically
import androidx.compose.animation.slideOutVertically
import androidx.compose.animation.togetherWith
import androidx.compose.animation.with
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.combinedClickable
@@ -44,6 +45,7 @@ import androidx.compose.runtime.MutableState
import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue
import androidx.compose.runtime.livedata.observeAsState
import androidx.compose.runtime.mutableFloatStateOf
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
@@ -112,6 +114,7 @@ import kotlinx.collections.immutable.toImmutableMap
import kotlinx.collections.immutable.toImmutableSet
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import java.math.BigDecimal
import java.math.RoundingMode
import java.text.DecimalFormat
@@ -195,60 +198,35 @@ private fun GenericInnerReactionRow(
six: @Composable () -> Unit
) {
Row(verticalAlignment = CenterVertically, modifier = ReactionRowHeight) {
val fullWeight = remember { Modifier.weight(1f) }
if (showReactionDetail) {
Column(
verticalArrangement = Arrangement.Center,
modifier = ReactionRowExpandButton
Row(
verticalAlignment = CenterVertically,
modifier = remember { ReactionRowExpandButton.then(fullWeight) }
) {
Row(verticalAlignment = CenterVertically) {
one()
}
one()
}
}
Column(
verticalArrangement = Arrangement.Center,
modifier = remember { Modifier.weight(1f) }
) {
Row(verticalAlignment = CenterVertically) {
two()
}
Row(verticalAlignment = CenterVertically, modifier = fullWeight) {
two()
}
Column(
verticalArrangement = Arrangement.Center,
modifier = remember { Modifier.weight(1f) }
) {
Row(verticalAlignment = CenterVertically) {
three()
}
Row(verticalAlignment = CenterVertically, modifier = fullWeight) {
three()
}
Column(
verticalArrangement = Arrangement.Center,
modifier = remember { Modifier.weight(1f) }
) {
Row(verticalAlignment = CenterVertically) {
four()
}
Row(verticalAlignment = CenterVertically, modifier = fullWeight) {
four()
}
Column(
verticalArrangement = Arrangement.Center,
modifier = remember { Modifier.weight(1f) }
) {
Row(verticalAlignment = CenterVertically) {
five()
}
Row(verticalAlignment = CenterVertically, modifier = fullWeight) {
five()
}
Column(
verticalArrangement = Arrangement.Center,
modifier = remember { Modifier.weight(1f) }
) {
Row(verticalAlignment = CenterVertically) {
six()
}
Row(verticalAlignment = CenterVertically, modifier = fullWeight) {
six()
}
}
}
@@ -590,17 +568,12 @@ fun ReplyCounter(baseNote: Note, textColor: Color) {
SlidingAnimationCount(repliesState, textColor)
}
@Composable
private fun SlidingAnimationCount(baseCount: MutableState<Int>, textColor: Color) {
SlidingAnimationCount(baseCount.value, textColor)
}
@OptIn(ExperimentalAnimationApi::class)
@Composable
private fun SlidingAnimationCount(baseCount: Int, textColor: Color) {
AnimatedContent<Int>(
targetState = baseCount,
transitionSpec = AnimatedContentTransitionScope<Int>::transitionSpec
transitionSpec = AnimatedContentTransitionScope<Int>::transitionSpec,
label = "SlidingAnimationCount"
) { count ->
TextCount(count, textColor)
}
@@ -612,11 +585,16 @@ private fun <S> AnimatedContentTransitionScope<S>.transitionSpec(): ContentTrans
}
@ExperimentalAnimationApi
val slideAnimation: ContentTransform = slideInVertically(animationSpec = tween(durationMillis = 100)) { height -> height } + fadeIn(
animationSpec = tween(durationMillis = 100)
) with slideOutVertically(animationSpec = tween(durationMillis = 100)) { height -> -height } + fadeOut(
animationSpec = tween(durationMillis = 100)
)
val slideAnimation: ContentTransform =
(
slideInVertically(animationSpec = tween(durationMillis = 100)) { height -> height } + fadeIn(
animationSpec = tween(durationMillis = 100)
)
).togetherWith(
slideOutVertically(animationSpec = tween(durationMillis = 100)) { height -> -height } + fadeOut(
animationSpec = tween(durationMillis = 100)
)
)
@Composable
private fun TextCount(count: Int, textColor: Color) {
@@ -630,11 +608,11 @@ private fun TextCount(count: Int, textColor: Color) {
}
@Composable
@OptIn(ExperimentalAnimationApi::class)
private fun SlidingAnimationAmount(amount: MutableState<String>, textColor: Color) {
AnimatedContent(
targetState = amount.value,
transitionSpec = AnimatedContentTransitionScope<String>::transitionSpec
transitionSpec = AnimatedContentTransitionScope<String>::transitionSpec,
label = "SlidingAnimationAmount"
) { count ->
Text(
text = count,
@@ -667,7 +645,9 @@ fun BoostReaction(
}
}
) {
BoostIcon(baseNote, iconSize, grayTint, accountViewModel)
ObserveBoostIcon(baseNote, accountViewModel) { hasBoosted ->
RepostedIcon(iconButtonModifier, if (hasBoosted) Color.Unspecified else grayTint)
}
if (wantsToBoost) {
BoostTypeChoicePopup(
@@ -692,20 +672,16 @@ fun BoostReaction(
}
@Composable
fun BoostIcon(baseNote: Note, iconSize: Dp = Size20dp, grayTint: Color, accountViewModel: AccountViewModel) {
val iconTint by remember(baseNote) {
fun ObserveBoostIcon(baseNote: Note, accountViewModel: AccountViewModel, inner: @Composable (Boolean) -> Unit) {
val hasBoosted by remember(baseNote) {
baseNote.live().boosts.map {
if (it.note.isBoostedBy(accountViewModel.userProfile())) Color.Unspecified else grayTint
it.note.isBoostedBy(accountViewModel.userProfile())
}.distinctUntilChanged()
}.observeAsState(
if (baseNote.isBoostedBy(accountViewModel.userProfile())) Color.Unspecified else grayTint
baseNote.isBoostedBy(accountViewModel.userProfile())
)
val iconModifier = remember {
Modifier.size(iconSize)
}
RepostedIcon(iconModifier, iconTint)
inner(hasBoosted)
}
@Composable
@@ -760,10 +736,20 @@ fun LikeReaction(
}
)
) {
LikeIcon(baseNote, iconFontSize, heartSize, grayTint, accountViewModel)
ObserveLikeIcon(baseNote, accountViewModel) { reactionType ->
Crossfade(targetState = reactionType.value, label = "LikeIcon") {
if (it != null) {
RenderReactionType(it, heartSize, iconFontSize)
} else {
LikeIcon(heartSize, grayTint)
}
}
}
}
LikeText(baseNote, grayTint)
ObserveLikeText(baseNote) { reactionCount ->
SlidingAnimationCount(reactionCount, grayTint)
}
if (wantsToChangeReactionSymbol) {
UpdateReactionTypeDialog(
@@ -790,44 +776,26 @@ fun LikeReaction(
}
@Composable
fun LikeIcon(
fun ObserveLikeIcon(
baseNote: Note,
iconFontSize: TextUnit = Font14SP,
iconSize: Dp = Size20dp,
grayTint: Color,
accountViewModel: AccountViewModel
accountViewModel: AccountViewModel,
inner: @Composable (MutableState<String?>) -> Unit
) {
val reactionType = remember(baseNote) {
mutableStateOf<String?>(null)
}
val scope = rememberCoroutineScope()
val reactionsState by baseNote.live().reactions.observeAsState()
WatchReactionTypeForNote(baseNote, accountViewModel) { newReactionType ->
if (reactionType.value != newReactionType) {
scope.launch(Dispatchers.Main) {
LaunchedEffect(key1 = reactionsState) {
accountViewModel.loadReactionTo(reactionsState?.note) { newReactionType ->
if (reactionType.value != newReactionType) {
reactionType.value = newReactionType
}
}
}
Crossfade(targetState = reactionType) {
val value = it.value
if (value != null) {
RenderReactionType(value, iconSize, iconFontSize)
} else {
LikeIcon(iconSize, grayTint)
}
}
}
@Composable
private fun WatchReactionTypeForNote(baseNote: Note, accountViewModel: AccountViewModel, onNewReactionType: (String?) -> Unit) {
val reactionsState by baseNote.live().reactions.observeAsState()
LaunchedEffect(key1 = reactionsState) {
accountViewModel.loadReactionTo(reactionsState?.note, onNewReactionType)
}
inner(reactionType)
}
@Composable
@@ -863,10 +831,10 @@ private fun RenderReactionType(
}
@Composable
fun LikeText(baseNote: Note, grayTint: Color) {
fun ObserveLikeText(baseNote: Note, inner: @Composable (Int) -> Unit) {
val reactionCount by baseNote.live().reactionCount.observeAsState(0)
SlidingAnimationCount(reactionCount, grayTint)
inner(reactionCount)
}
private fun likeClick(
@@ -919,7 +887,7 @@ fun ZapReaction(
val context = LocalContext.current
val scope = rememberCoroutineScope()
var zappingProgress by remember { mutableStateOf(0f) }
var zappingProgress by remember { mutableFloatStateOf(0f) }
Row(
verticalAlignment = CenterVertically,
@@ -942,7 +910,7 @@ fun ZapReaction(
onMultipleChoices = {
wantsToZap = true
},
onError = { title, message ->
onError = { _, message ->
scope.launch {
zappingProgress = 0f
showErrorMessageDialog = message
@@ -973,7 +941,7 @@ fun ZapReaction(
wantsToZap = false
wantsToChangeZapAmount = true
},
onError = { title, message ->
onError = { _, message ->
scope.launch {
zappingProgress = 0f
showErrorMessageDialog = message
@@ -1033,7 +1001,7 @@ fun ZapReaction(
if (wantsToSetCustomZap) {
ZapCustomDialog(
onClose = { wantsToSetCustomZap = false },
onError = { title, message ->
onError = { _, message ->
scope.launch {
zappingProgress = 0f
showErrorMessageDialog = message
@@ -1058,22 +1026,31 @@ fun ZapReaction(
CircularProgressIndicator(
progress = animateFloatAsState(
targetValue = zappingProgress,
animationSpec = ProgressIndicatorDefaults.ProgressAnimationSpec
animationSpec = ProgressIndicatorDefaults.ProgressAnimationSpec,
label = "ZapIconIndicator"
).value,
modifier = remember { Modifier.size(animationSize) },
strokeWidth = 2.dp
)
} else {
ZapIcon(
ObserveZapIcon(
baseNote,
iconSize,
grayTint,
accountViewModel
)
) { wasZappedByLoggedInUser ->
Crossfade(targetState = wasZappedByLoggedInUser.value, label = "ZapIcon") {
if (it) {
ZappedIcon(iconSize)
} else {
ZapIcon(iconSize, grayTint)
}
}
}
}
}
ZapAmountText(baseNote, grayTint, accountViewModel)
ObserveZapAmountText(baseNote, accountViewModel) { zapAmountTxt ->
SlidingAnimationAmount(zapAmountTxt, grayTint)
}
}
private fun zapClick(
@@ -1115,98 +1092,72 @@ private fun zapClick(
}
@Composable
private fun ZapIcon(
private fun ObserveZapIcon(
baseNote: Note,
iconSize: Dp,
grayTint: Color,
accountViewModel: AccountViewModel
accountViewModel: AccountViewModel,
inner: @Composable (MutableState<Boolean>) -> Unit
) {
val wasZappedByLoggedInUser = remember { mutableStateOf(false) }
val scope = rememberCoroutineScope()
if (!wasZappedByLoggedInUser.value) {
WatchZapsForNote(baseNote, accountViewModel) { newWasZapped ->
if (wasZappedByLoggedInUser.value != newWasZapped) {
scope.launch(Dispatchers.Main) {
val zapsState by baseNote.live().zaps.observeAsState()
LaunchedEffect(key1 = zapsState) {
accountViewModel.calculateIfNoteWasZappedByAccount(baseNote) { newWasZapped ->
if (wasZappedByLoggedInUser.value != newWasZapped) {
wasZappedByLoggedInUser.value = newWasZapped
}
}
}
}
Crossfade(targetState = wasZappedByLoggedInUser) {
if (it.value) {
ZappedIcon(iconSize)
} else {
ZapIcon(iconSize, grayTint)
}
}
inner(wasZappedByLoggedInUser)
}
@Composable
private fun WatchZapsForNote(baseNote: Note, accountViewModel: AccountViewModel, onWasZapped: (Boolean) -> Unit) {
private fun ObserveZapAmountText(
baseNote: Note,
accountViewModel: AccountViewModel,
inner: @Composable (MutableState<String>) -> Unit
) {
val zapAmountTxt = remember(baseNote) {
mutableStateOf(showAmount(baseNote.zapsAmount))
}
val zapsState by baseNote.live().zaps.observeAsState()
LaunchedEffect(key1 = zapsState) {
accountViewModel.calculateIfNoteWasZappedByAccount(baseNote, onWasZapped)
}
}
@Composable
private fun ZapAmountText(
baseNote: Note,
grayTint: Color,
accountViewModel: AccountViewModel
) {
val zapAmountTxt = remember(baseNote) { mutableStateOf("") }
val scope = rememberCoroutineScope()
WatchZapAmountsForNote(baseNote, accountViewModel) { newZapAmount ->
if (zapAmountTxt.value != newZapAmount) {
scope.launch(Dispatchers.Main) {
zapAmountTxt.value = newZapAmount
accountViewModel.calculateZapAmount(baseNote) { newZapAmount ->
if (zapAmountTxt.value != newZapAmount) {
withContext(Dispatchers.Main) {
zapAmountTxt.value = newZapAmount
}
}
}
}
SlidingAnimationAmount(zapAmountTxt, grayTint)
}
@Composable
fun WatchZapAmountsForNote(baseNote: Note, accountViewModel: AccountViewModel, onZapAmount: (String) -> Unit) {
val zapsState by baseNote.live().zaps.observeAsState()
LaunchedEffect(key1 = zapsState) {
accountViewModel.calculateZapAmount(baseNote, onZapAmount)
}
inner(zapAmountTxt)
}
@Composable
fun ViewCountReaction(
note: Note,
grayTint: Color,
barChartSize: Dp = Size19dp,
numberSize: Dp = Size24dp,
barChartModifier: Modifier = Modifier.size(Size19dp),
numberSizeModifier: Modifier = Modifier.height(Size24dp),
viewCountColorFilter: ColorFilter
) {
ViewCountIcon(barChartSize, grayTint)
DrawViewCount(note, numberSize, viewCountColorFilter)
ViewCountIcon(barChartModifier, grayTint)
DrawViewCount(note, numberSizeModifier, viewCountColorFilter)
}
@Composable
private fun DrawViewCount(
note: Note,
numberSize: Dp = Size24dp,
iconModifier: Modifier = Modifier,
viewCountColorFilter: ColorFilter
) {
val context = LocalContext.current
val iconModifier = remember {
Modifier.height(numberSize)
}
AsyncImage(
model = remember(note) {
ImageRequest.Builder(context)
@@ -201,9 +201,13 @@ class AccountViewModel(val account: Account, val settings: SettingsState) : View
return account.calculateZappedAmount(zappedNote)
}
fun calculateZapAmount(zappedNote: Note, onZapAmount: (String) -> Unit) {
viewModelScope.launch(Dispatchers.IO) {
onZapAmount(showAmount(account.calculateZappedAmount(zappedNote)))
suspend fun calculateZapAmount(zappedNote: Note, onZapAmount: suspend (String) -> Unit) {
if (zappedNote.zapPayments.isNotEmpty()) {
viewModelScope.launch(Dispatchers.IO) {
onZapAmount(showAmount(account.calculateZappedAmount(zappedNote)))
}
} else {
onZapAmount(showAmount(zappedNote.zapsAmount))
}
}
@@ -437,6 +437,6 @@ fun ReactionsColumn(baseNote: Note, accountViewModel: AccountViewModel, nav: (St
}
LikeReaction(baseNote, grayTint = MaterialTheme.colorScheme.onBackground, accountViewModel, nav, iconSize = 40.dp, heartSize = Size35dp, 28.sp)
ZapReaction(baseNote, grayTint = MaterialTheme.colorScheme.onBackground, accountViewModel, iconSize = 40.dp, animationSize = Size35dp, nav = nav)
ViewCountReaction(baseNote, grayTint = MaterialTheme.colorScheme.onBackground, barChartSize = 39.dp, viewCountColorFilter = MaterialTheme.colorScheme.onBackgroundColorFilter)
ViewCountReaction(baseNote, grayTint = MaterialTheme.colorScheme.onBackground, barChartModifier = Modifier.size(39.dp), viewCountColorFilter = MaterialTheme.colorScheme.onBackgroundColorFilter)
}
}