- Moves zaps from a Map<Request,Response> to a List<CustomClass> 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.
This commit is contained in:
Vitor Pamplona
2023-06-13 16:33:52 -04:00
parent a01a060abc
commit 9b5c7ad23f
4 changed files with 147 additions and 93 deletions
@@ -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<Color>,
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<Note>,
@@ -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<Note, Note?>,
zapEvents: ImmutableList<CombinedZap>,
backgroundColor: MutableState<Color>,
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<Note, Note?>,
authorNotes: ImmutableList<CombinedZap>,
backgroundColor: MutableState<Color>,
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<User?, String?, String?>>(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)
}
}
}
}
@@ -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() } }
@@ -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<Note, Note>) : Card() {
val createdAt = zapEvents.maxOf { it.value.createdAt() ?: 0 }
class ZapUserSetCard(val user: User, val zapEvents: ImmutableList<CombinedZap>) : 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<Note, Note>) :
}
@Immutable
class MultiSetCard(val note: Note, val boostEvents: ImmutableList<Note>, val likeEvents: ImmutableList<Note>, val zapEvents: ImmutableMap<Note, Note>) : Card() {
class MultiSetCard(val note: Note, val boostEvents: ImmutableList<Note>, val likeEvents: ImmutableList<Note>, val zapEvents: ImmutableList<CombinedZap>) : 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
)
@@ -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<Note>) : ViewModel() {
}
// val reactionCards = reactionsPerEvent.map { LikeSetCard(it.key, it.value) }
val zapsPerUser = mutableMapOf<User, MutableMap<Note, Note>>()
val zapsPerEvent = mutableMapOf<Note, MutableMap<Note, Note>>()
val zapsPerUser = mutableMapOf<User, MutableList<CombinedZap>>()
val zapsPerEvent = mutableMapOf<Note, MutableList<CombinedZap>>()
notes
.filter { it.event is LnZapEvent }
.forEach { zapEvent ->
@@ -143,27 +143,23 @@ open class CardFeedViewModel(val localFilter: FeedFilter<Note>) : 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<Note, MutableList<Note>>()
notes
.filter { it.event is RepostEvent }
@@ -178,9 +174,9 @@ open class CardFeedViewModel(val localFilter: FeedFilter<Note>) : 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<Note>) : 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<Note>) : ViewModel() {
val userZaps = zapsPerUser.map {
ZapUserSetCard(
it.key,
it.value.toImmutableMap()
it.value.toImmutableList()
)
}
@@ -346,3 +342,9 @@ fun <T> equalImmutableLists(list1: ImmutableList<T>, list2: ImmutableList<T>): B
}
return true
}
@Immutable
data class CombinedZap(val request: Note, val response: Note) {
fun createdAt() = response.createdAt()
fun idHex() = response.idHex
}