From 9b5c7ad23f1deeeb51dd46b0c238a05ad1010a70 Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Tue, 13 Jun 2023 16:33:52 -0400 Subject: [PATCH] - Moves zaps from a Map to a List to avoid recompositions due to the use of the unstable Pair object - Creates the class ZapAmountCommentNotification to be used instead of the unstable Triple class. --- .../amethyst/ui/note/MultiSetCompose.kt | 189 +++++++++++------- .../amethyst/ui/note/ReactionsRow.kt | 4 +- .../amethyst/ui/screen/CardFeedState.kt | 11 +- .../amethyst/ui/screen/CardFeedViewModel.kt | 36 ++-- 4 files changed, 147 insertions(+), 93 deletions(-) diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/MultiSetCompose.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/MultiSetCompose.kt index 451a96912..7a0a0853e 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/MultiSetCompose.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/MultiSetCompose.kt @@ -22,6 +22,7 @@ import androidx.compose.material.Text import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.Bolt import androidx.compose.runtime.Composable +import androidx.compose.runtime.Immutable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.MutableState import androidx.compose.runtime.derivedStateOf @@ -47,10 +48,12 @@ import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.model.User +import com.vitorpamplona.amethyst.model.UserMetadata import com.vitorpamplona.amethyst.service.model.LnZapEvent import com.vitorpamplona.amethyst.service.model.LnZapRequestEvent import com.vitorpamplona.amethyst.ui.actions.ImmutableListOfLists import com.vitorpamplona.amethyst.ui.components.TranslatableRichTextViewer +import com.vitorpamplona.amethyst.ui.screen.CombinedZap import com.vitorpamplona.amethyst.ui.screen.MultiSetCard import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.amethyst.ui.screen.loggedIn.showAmountAxis @@ -58,7 +61,6 @@ import com.vitorpamplona.amethyst.ui.theme.BitcoinOrange import com.vitorpamplona.amethyst.ui.theme.newItemBackgroundColor import com.vitorpamplona.amethyst.ui.theme.overPictureBackground import kotlinx.collections.immutable.ImmutableList -import kotlinx.collections.immutable.ImmutableMap import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch @@ -92,48 +94,32 @@ fun MultiSetCompose(multiSetCard: MultiSetCard, routeForLastRead: String, accoun } } - val columnModifier = Modifier - .drawBehind { - drawRect(backgroundColor.value) - } - .padding( - start = 12.dp, - end = 12.dp, - top = 10.dp - ) - .combinedClickable( - onClick = { - scope.launch { - routeFor(baseNote, accountViewModel.userProfile())?.let { nav(it) } - } - }, - onLongClick = { popupExpanded = true } - ) - .fillMaxWidth() - - val zapEvents by remember { derivedStateOf { multiSetCard.zapEvents } } - val boostEvents by remember { derivedStateOf { multiSetCard.boostEvents } } - val likeEvents by remember { derivedStateOf { multiSetCard.likeEvents } } - - val hasZapEvents by remember { derivedStateOf { multiSetCard.zapEvents.isNotEmpty() } } - val hasBoostEvents by remember { derivedStateOf { multiSetCard.boostEvents.isNotEmpty() } } - val hasLikeEvents by remember { derivedStateOf { multiSetCard.likeEvents.isNotEmpty() } } + val columnModifier = remember(backgroundColor.value) { + Modifier + .drawBehind { + drawRect(backgroundColor.value) + } + .padding( + start = 12.dp, + end = 12.dp, + top = 10.dp + ) + .combinedClickable( + onClick = { + scope.launch { + routeFor(baseNote, accountViewModel.userProfile())?.let { nav(it) } + } + }, + onLongClick = { popupExpanded = true } + ) + .fillMaxWidth() + } Column(modifier = columnModifier) { - if (hasZapEvents) { - RenderZapGallery(zapEvents, backgroundColor, nav, accountViewModel) - } + Galeries(multiSetCard, backgroundColor, nav, accountViewModel) - if (hasBoostEvents) { - RenderBoostGallery(boostEvents, backgroundColor, nav, accountViewModel) - } - - if (hasLikeEvents) { - RenderLikeGallery(likeEvents, backgroundColor, nav, accountViewModel) - } - - Row(Modifier.fillMaxWidth()) { - Spacer(modifier = Modifier.width(65.dp)) + Row(remember { Modifier.fillMaxWidth() }) { + Spacer(modifier = remember { Modifier.width(65.dp) }) NoteCompose( baseNote = baseNote, @@ -150,6 +136,34 @@ fun MultiSetCompose(multiSetCard: MultiSetCard, routeForLastRead: String, accoun } } +@Composable +private fun Galeries( + multiSetCard: MultiSetCard, + backgroundColor: MutableState, + nav: (String) -> Unit, + accountViewModel: AccountViewModel +) { + val zapEvents by remember { derivedStateOf { multiSetCard.zapEvents } } + val boostEvents by remember { derivedStateOf { multiSetCard.boostEvents } } + val likeEvents by remember { derivedStateOf { multiSetCard.likeEvents } } + + val hasZapEvents by remember { derivedStateOf { multiSetCard.zapEvents.isNotEmpty() } } + val hasBoostEvents by remember { derivedStateOf { multiSetCard.boostEvents.isNotEmpty() } } + val hasLikeEvents by remember { derivedStateOf { multiSetCard.likeEvents.isNotEmpty() } } + + if (hasZapEvents) { + RenderZapGallery(zapEvents, backgroundColor, nav, accountViewModel) + } + + if (hasBoostEvents) { + RenderBoostGallery(boostEvents, backgroundColor, nav, accountViewModel) + } + + if (hasLikeEvents) { + RenderLikeGallery(likeEvents, backgroundColor, nav, accountViewModel) + } +} + @Composable fun RenderLikeGallery( likeEvents: ImmutableList, @@ -157,7 +171,7 @@ fun RenderLikeGallery( nav: (String) -> Unit, accountViewModel: AccountViewModel ) { - Row(Modifier.fillMaxWidth()) { + Row(remember { Modifier.fillMaxWidth() }) { Box( modifier = remember { Modifier @@ -183,12 +197,12 @@ fun RenderLikeGallery( @Composable fun RenderZapGallery( - zapEvents: ImmutableMap, + zapEvents: ImmutableList, backgroundColor: MutableState, nav: (String) -> Unit, accountViewModel: AccountViewModel ) { - Row(Modifier.fillMaxWidth()) { + Row(remember { Modifier.fillMaxWidth() }) { Box( modifier = remember { Modifier @@ -250,7 +264,7 @@ fun RenderBoostGallery( @OptIn(ExperimentalLayoutApi::class) @Composable fun AuthorGalleryZaps( - authorNotes: ImmutableMap, + authorNotes: ImmutableList, backgroundColor: MutableState, nav: (String) -> Unit, accountViewModel: AccountViewModel @@ -258,12 +272,19 @@ fun AuthorGalleryZaps( Column(modifier = Modifier.padding(start = 10.dp)) { FlowRow() { authorNotes.forEach { - AuthorPictureAndComment(it.key, it.value, backgroundColor, nav, accountViewModel) + AuthorPictureAndComment(it.request, it.response, backgroundColor, nav, accountViewModel) } } } } +@Immutable +data class ZapAmountCommentNotification( + val user: User?, + val comment: String?, + val amount: String? +) + @Composable private fun AuthorPictureAndComment( zapRequest: Note, @@ -272,7 +293,15 @@ private fun AuthorPictureAndComment( nav: (String) -> Unit, accountViewModel: AccountViewModel ) { - var content by remember { mutableStateOf>(Triple(zapRequest.author, null, null)) } + var content by remember { + mutableStateOf( + ZapAmountCommentNotification( + user = zapRequest.author, + comment = null, + amount = null + ) + ) + } LaunchedEffect(key1 = zapRequest.idHex, key2 = zapEvent?.idHex) { launch(Dispatchers.Default) { @@ -281,32 +310,34 @@ private fun AuthorPictureAndComment( val amount = (zapEvent?.event as? LnZapEvent)?.amount if (decryptedContent != null) { val newAuthor = LocalCache.getOrCreateUser(decryptedContent.pubKey) - content = Triple(newAuthor, decryptedContent.content.ifBlank { null }, showAmountAxis(amount)) + content = ZapAmountCommentNotification(newAuthor, decryptedContent.content.ifBlank { null }, showAmountAxis(amount)) } else { if (!zapRequest.event?.content().isNullOrBlank() || amount != null) { - content = Triple(zapRequest.author, zapRequest.event?.content()?.ifBlank { null }, showAmountAxis(amount)) + content = ZapAmountCommentNotification(zapRequest.author, zapRequest.event?.content()?.ifBlank { null }, showAmountAxis(amount)) } } } } } - content.first?.let { + content.let { val route by remember { derivedStateOf { - "User/${it.pubkeyHex}" + "User/${it.user?.pubkeyHex}" } } - AuthorPictureAndComment( - author = it, - comment = content.second, - amount = content.third, - route = route, - backgroundColor = backgroundColor, - nav = nav, - accountViewModel = accountViewModel - ) + it.user?.let { user -> + AuthorPictureAndComment( + author = user, + comment = it.comment, + amount = it.amount, + route = route, + backgroundColor = backgroundColor, + nav = nav, + accountViewModel = accountViewModel + ) + } } } @@ -341,12 +372,19 @@ private fun AuthorPictureAndComment( ) amount?.let { - Box(modifier = Modifier.fillMaxSize().clip(shape = CircleShape), contentAlignment = Alignment.BottomCenter) { + Box( + modifier = Modifier + .fillMaxSize() + .clip(shape = CircleShape), + contentAlignment = Alignment.BottomCenter + ) { val backgroundColor = MaterialTheme.colors.overPictureBackground Box( - modifier = Modifier - .fillMaxWidth() - .drawBehind { drawRect(backgroundColor) }, + modifier = remember { + Modifier + .fillMaxWidth() + .drawBehind { drawRect(backgroundColor) } + }, contentAlignment = Alignment.BottomCenter ) { Text( @@ -424,17 +462,20 @@ fun FastNoteAuthorPicture( pictureModifier: Modifier = Modifier, accountViewModel: AccountViewModel ) { - val userState by author.live().metadata.observeAsState() - val profilePicture by remember(userState) { - derivedStateOf { - userState?.user?.profilePicture() - } + var profilePicture by remember { + mutableStateOf(author.info?.picture) } val authorPubKey = remember { author.pubkeyHex } + WatchUserMetadata(author) { + if (it.picture != profilePicture) { + profilePicture = it.picture + } + } + UserPicture( userHex = authorPubKey, userPicture = profilePicture, @@ -443,3 +484,15 @@ fun FastNoteAuthorPicture( accountViewModel = accountViewModel ) } + +@Composable +fun WatchUserMetadata(userBase: User, onMetadataChanges: (UserMetadata) -> Unit) { + val userState by userBase.live().metadata.observeAsState() + LaunchedEffect(key1 = userState) { + launch(Dispatchers.Default) { + userState?.user?.info?.let { + onMetadataChanges(it) + } + } + } +} diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/ReactionsRow.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/ReactionsRow.kt index 00b2016da..2cc5b9f61 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/ReactionsRow.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/ReactionsRow.kt @@ -64,12 +64,12 @@ import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.ui.actions.NewPostView +import com.vitorpamplona.amethyst.ui.screen.CombinedZap import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.amethyst.ui.theme.BitcoinOrange import com.vitorpamplona.amethyst.ui.theme.ButtonBorder import com.vitorpamplona.amethyst.ui.theme.placeholderText import kotlinx.collections.immutable.toImmutableList -import kotlinx.collections.immutable.toImmutableMap import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch @@ -188,7 +188,7 @@ private fun ReactionDetailGallery( if (hasReactions) { Row(verticalAlignment = CenterVertically, modifier = Modifier.padding(start = 10.dp, top = 5.dp)) { Column() { - val zapEvents by remember(zapsState) { derivedStateOf { baseNote.zaps.toImmutableMap() } } + val zapEvents by remember(zapsState) { derivedStateOf { baseNote.zaps.mapNotNull { it.value?.let { zapEvent -> CombinedZap(it.key, zapEvent) } }.toImmutableList() } } val boostEvents by remember(boostsState) { derivedStateOf { baseNote.boosts.toImmutableList() } } val likeEvents by remember(reactionsState) { derivedStateOf { baseNote.reactions.toImmutableList() } } diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/CardFeedState.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/CardFeedState.kt index 4b32446a1..8a3d57e3d 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/CardFeedState.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/CardFeedState.kt @@ -6,7 +6,6 @@ import androidx.compose.runtime.Stable import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.model.User import kotlinx.collections.immutable.ImmutableList -import kotlinx.collections.immutable.ImmutableMap @Immutable abstract class Card() { @@ -33,8 +32,8 @@ class NoteCard(val note: Note) : Card() { } @Immutable -class ZapUserSetCard(val user: User, val zapEvents: ImmutableMap) : Card() { - val createdAt = zapEvents.maxOf { it.value.createdAt() ?: 0 } +class ZapUserSetCard(val user: User, val zapEvents: ImmutableList) : Card() { + val createdAt = zapEvents.maxOf { it.createdAt() ?: 0 } override fun createdAt(): Long { return createdAt } @@ -42,15 +41,15 @@ class ZapUserSetCard(val user: User, val zapEvents: ImmutableMap) : } @Immutable -class MultiSetCard(val note: Note, val boostEvents: ImmutableList, val likeEvents: ImmutableList, val zapEvents: ImmutableMap) : Card() { +class MultiSetCard(val note: Note, val boostEvents: ImmutableList, val likeEvents: ImmutableList, val zapEvents: ImmutableList) : Card() { val maxCreatedAt = maxOf( - zapEvents.maxOfOrNull { it.value.createdAt() ?: 0 } ?: 0, + zapEvents.maxOfOrNull { it.createdAt() ?: 0 } ?: 0, likeEvents.maxOfOrNull { it.createdAt() ?: 0 } ?: 0, boostEvents.maxOfOrNull { it.createdAt() ?: 0 } ?: 0 ) val minCreatedAt = minOf( - zapEvents.minOfOrNull { it.value.createdAt() ?: Long.MAX_VALUE } ?: Long.MAX_VALUE, + zapEvents.minOfOrNull { it.createdAt() ?: Long.MAX_VALUE } ?: Long.MAX_VALUE, likeEvents.minOfOrNull { it.createdAt() ?: Long.MAX_VALUE } ?: Long.MAX_VALUE, boostEvents.minOfOrNull { it.createdAt() ?: Long.MAX_VALUE } ?: Long.MAX_VALUE ) diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/CardFeedViewModel.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/CardFeedViewModel.kt index 340196657..2ef773e10 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/CardFeedViewModel.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/CardFeedViewModel.kt @@ -1,6 +1,7 @@ package com.vitorpamplona.amethyst.ui.screen import android.util.Log +import androidx.compose.runtime.Immutable import androidx.compose.runtime.Stable import androidx.compose.runtime.mutableStateOf import androidx.lifecycle.ViewModel @@ -25,7 +26,6 @@ import com.vitorpamplona.amethyst.ui.dal.FeedFilter import com.vitorpamplona.amethyst.ui.dal.NotificationFeedFilter import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.toImmutableList -import kotlinx.collections.immutable.toImmutableMap import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Job @@ -132,8 +132,8 @@ open class CardFeedViewModel(val localFilter: FeedFilter) : ViewModel() { } // val reactionCards = reactionsPerEvent.map { LikeSetCard(it.key, it.value) } - val zapsPerUser = mutableMapOf>() - val zapsPerEvent = mutableMapOf>() + val zapsPerUser = mutableMapOf>() + val zapsPerEvent = mutableMapOf>() notes .filter { it.event is LnZapEvent } .forEach { zapEvent -> @@ -143,27 +143,23 @@ open class CardFeedViewModel(val localFilter: FeedFilter) : ViewModel() { if (zapRequest != null) { // var newZapRequestEvent = LocalCache.checkPrivateZap(zapRequest.event as Event) // zapRequest.event = newZapRequestEvent - zapsPerEvent.getOrPut(zappedPost, { mutableMapOf() }).put(zapRequest, zapEvent) + zapsPerEvent.getOrPut(zappedPost, { mutableListOf() }).add(CombinedZap(zapRequest, zapEvent)) } } else { val event = (zapEvent.event as LnZapEvent) - val author = event.zappedAuthor().mapNotNull { - LocalCache.checkGetOrCreateUser( - it - ) - }.firstOrNull() + val author = event.zappedAuthor().firstNotNullOfOrNull { + LocalCache.users[it] // don't create user if it doesn't exist + } if (author != null) { val zapRequest = author.zaps.filter { it.value == zapEvent }.keys.firstOrNull() if (zapRequest != null) { - zapsPerUser.getOrPut(author, { mutableMapOf() }) - .put(zapRequest, zapEvent) + zapsPerUser.getOrPut(author, { mutableListOf() }) + .add(CombinedZap(zapRequest, zapEvent)) } } } } - // val zapCards = zapsPerEvent.map { ZapSetCard(it.key, it.value) } - val boostsPerEvent = mutableMapOf>() notes .filter { it.event is RepostEvent } @@ -178,9 +174,9 @@ open class CardFeedViewModel(val localFilter: FeedFilter) : ViewModel() { val multiCards = allBaseNotes.map { baseNote -> val boostsInCard = boostsPerEvent[baseNote] ?: emptyList() val reactionsInCard = reactionsPerEvent[baseNote] ?: emptyList() - val zapsInCard = zapsPerEvent[baseNote] ?: emptyMap() + val zapsInCard = zapsPerEvent[baseNote] ?: emptyList() - val singleList = (boostsInCard + zapsInCard.values + reactionsInCard) + val singleList = (boostsInCard + zapsInCard.map { it.response } + reactionsInCard) .sortedWith(compareBy({ it.createdAt() }, { it.idHex })) .reversed() @@ -189,7 +185,7 @@ open class CardFeedViewModel(val localFilter: FeedFilter) : ViewModel() { baseNote, boostsInCard.filter { it in chunk }.toImmutableList(), reactionsInCard.filter { it in chunk }.toImmutableList(), - zapsInCard.filter { it.value in chunk }.toImmutableMap() + zapsInCard.filter { it.response in chunk }.toImmutableList() ) } }.flatten() @@ -197,7 +193,7 @@ open class CardFeedViewModel(val localFilter: FeedFilter) : ViewModel() { val userZaps = zapsPerUser.map { ZapUserSetCard( it.key, - it.value.toImmutableMap() + it.value.toImmutableList() ) } @@ -346,3 +342,9 @@ fun equalImmutableLists(list1: ImmutableList, list2: ImmutableList): B } return true } + +@Immutable +data class CombinedZap(val request: Note, val response: Note) { + fun createdAt() = response.createdAt() + fun idHex() = response.idHex +}