From a7e17c810d2867c50351ef212833d2ac824c74be Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Mon, 27 Feb 2023 21:04:29 -0500 Subject: [PATCH] Merging all notification reactions in one card. --- .../amethyst/ui/note/MultiSetCompose.kt | 216 ++++++++++++++++++ .../amethyst/ui/screen/CardFeedState.kt | 13 ++ .../amethyst/ui/screen/CardFeedView.kt | 7 + .../amethyst/ui/screen/CardFeedViewModel.kt | 17 +- 4 files changed, 249 insertions(+), 4 deletions(-) create mode 100644 app/src/main/java/com/vitorpamplona/amethyst/ui/note/MultiSetCompose.kt 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 new file mode 100644 index 000000000..eb4e9d74c --- /dev/null +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/MultiSetCompose.kt @@ -0,0 +1,216 @@ +package com.vitorpamplona.amethyst.ui.note + +import androidx.compose.foundation.ExperimentalFoundationApi +import androidx.compose.foundation.background +import androidx.compose.foundation.combinedClickable +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width +import androidx.compose.material.Icon +import androidx.compose.material.MaterialTheme +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Bolt +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.livedata.observeAsState +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.compositeOver +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.unit.dp +import androidx.navigation.NavController +import com.google.accompanist.flowlayout.FlowRow +import com.vitorpamplona.amethyst.NotificationCache +import com.vitorpamplona.amethyst.R +import com.vitorpamplona.amethyst.service.model.ChannelMessageEvent +import com.vitorpamplona.amethyst.ui.screen.MultiSetCard +import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel +import com.vitorpamplona.amethyst.ui.theme.BitcoinOrange + +@OptIn(ExperimentalFoundationApi::class) +@Composable +fun MultiSetCompose(multiSetCard: MultiSetCard, modifier: Modifier = Modifier, routeForLastRead: String, accountViewModel: AccountViewModel, navController: NavController) { + val noteState by multiSetCard.note.live().metadata.observeAsState() + val note = noteState?.note + + val accountState by accountViewModel.accountLiveData.observeAsState() + val account = accountState?.account ?: return + + val context = LocalContext.current.applicationContext + + val noteEvent = note?.event + var popupExpanded by remember { mutableStateOf(false) } + + if (note == null) { + BlankNote(Modifier, false) + } else { + var isNew by remember { mutableStateOf(false) } + + LaunchedEffect(key1 = multiSetCard) { + isNew = multiSetCard.createdAt > NotificationCache.load(routeForLastRead, context) + + NotificationCache.markAsRead(routeForLastRead, multiSetCard.createdAt, context) + } + + var backgroundColor = if (isNew) { + MaterialTheme.colors.primary.copy(0.12f).compositeOver(MaterialTheme.colors.background) + } else { + MaterialTheme.colors.background + } + + Column( + modifier = Modifier + .background(backgroundColor) + .combinedClickable( + onClick = { + if (noteEvent !is ChannelMessageEvent) { + navController.navigate("Note/${note.idHex}") { + launchSingleTop = true + } + } else { + note.channel?.let { + navController.navigate("Channel/${it.idHex}") + } + } + }, + onLongClick = { popupExpanded = true } + ) + ) { + Row(modifier = Modifier + .padding( + start = 12.dp, + end = 12.dp, + top = 10.dp) + ) { + Column(Modifier.fillMaxWidth()) { + + if (multiSetCard.zapEvents.isNotEmpty()) { + Row(Modifier.fillMaxWidth()) { + // Draws the like picture outside the boosted card. + Box( + modifier = Modifier + .width(55.dp) + .padding(0.dp) + ) { + Icon( + imageVector = Icons.Default.Bolt, + contentDescription = "Zaps", + tint = BitcoinOrange, + modifier = Modifier + .size(25.dp) + .align(Alignment.TopEnd) + ) + } + + Column(modifier = Modifier.padding(start = 10.dp)) { + FlowRow() { + multiSetCard.zapEvents.forEach { + NoteAuthorPicture( + note = it.key, + navController = navController, + userAccount = account.userProfile(), + size = 35.dp + ) + } + } + } + } + } + + if (multiSetCard.boostEvents.isNotEmpty()) { + Row(Modifier.fillMaxWidth()) { + Box( + modifier = Modifier + .width(55.dp) + .padding(end = 4.dp) + ) { + Icon( + painter = painterResource(R.drawable.ic_retweeted), + null, + modifier = Modifier + .size(18.dp) + .align(Alignment.TopEnd), + tint = Color.Unspecified + ) + } + + Column(modifier = Modifier.padding(start = 10.dp)) { + FlowRow() { + multiSetCard.boostEvents.forEach { + NoteAuthorPicture( + note = it, + navController = navController, + userAccount = account.userProfile(), + size = 35.dp + ) + } + } + } + } + } + + if (multiSetCard.likeEvents.isNotEmpty()) { + Row(Modifier.fillMaxWidth()) { + Box( + modifier = Modifier + .width(55.dp) + .padding(end = 5.dp) + ) { + Icon( + painter = painterResource(R.drawable.ic_liked), + null, + modifier = Modifier + .size(16.dp) + .align(Alignment.TopEnd), + tint = Color.Unspecified + ) + } + + Column(modifier = Modifier.padding(start = 10.dp)) { + FlowRow() { + multiSetCard.likeEvents.forEach { + NoteAuthorPicture( + note = it, + navController = navController, + userAccount = account.userProfile(), + size = 35.dp + ) + } + } + } + } + } + + Row(Modifier.fillMaxWidth()) { + Box(modifier = Modifier + .width(55.dp) + .padding(0.dp)) { + } + + NoteCompose( + baseNote = note, + routeForLastRead = null, + modifier = Modifier.padding(top = 5.dp), + isBoostedNote = true, + parentBackgroundColor = backgroundColor, + accountViewModel = accountViewModel, + navController = navController + ) + + NoteDropDownMenu(note, popupExpanded, { popupExpanded = false }, accountViewModel) + } + } + } + } + } +} \ No newline at end of file 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 a99372071..7b636ef14 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 @@ -32,6 +32,19 @@ class ZapSetCard(val note: Note, val zapEvents: Map): Card() { override fun id() = note.idHex + "Z" + createdAt } +class MultiSetCard(val note: Note, val boostEvents: List, val likeEvents: List, val zapEvents: Map): Card() { + val createdAt = maxOf( + zapEvents.maxOfOrNull { it.value.event?.createdAt ?: 0 } ?: 0 , + likeEvents.maxOfOrNull { it.event?.createdAt ?: 0 } ?: 0 , + boostEvents.maxOfOrNull { it.event?.createdAt ?: 0 } ?: 0 + ) + + override fun createdAt(): Long { + return createdAt + } + override fun id() = note.idHex + "X" + createdAt +} + class BoostSetCard(val note: Note, val boostEvents: List): Card() { val createdAt = boostEvents.maxOf { it.event?.createdAt ?: 0 } diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/CardFeedView.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/CardFeedView.kt index d58204861..5b3d2e432 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/CardFeedView.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/CardFeedView.kt @@ -20,6 +20,7 @@ import com.google.accompanist.swiperefresh.SwipeRefresh import com.google.accompanist.swiperefresh.rememberSwipeRefreshState import com.vitorpamplona.amethyst.ui.note.BoostSetCompose import com.vitorpamplona.amethyst.ui.note.LikeSetCompose +import com.vitorpamplona.amethyst.ui.note.MultiSetCompose import com.vitorpamplona.amethyst.ui.note.NoteCompose import com.vitorpamplona.amethyst.ui.note.ZapSetCompose import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel @@ -120,6 +121,12 @@ private fun FeedLoaded( navController = navController, routeForLastRead = routeForLastRead ) + is MultiSetCard -> MultiSetCompose( + item, + accountViewModel = accountViewModel, + navController = navController, + routeForLastRead = routeForLastRead + ) } } } 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 cc79b8f07..ec40daec2 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 @@ -69,7 +69,7 @@ open class CardFeedViewModel(val dataSource: FeedFilter): ViewModel() { reactionsPerEvent.getOrPut(reactedPost, { mutableListOf() }).add(it) } - val reactionCards = reactionsPerEvent.map { LikeSetCard(it.key, it.value) } + //val reactionCards = reactionsPerEvent.map { LikeSetCard(it.key, it.value) } val zapsPerEvent = mutableMapOf>() notes @@ -84,7 +84,7 @@ open class CardFeedViewModel(val dataSource: FeedFilter): ViewModel() { } } - val zapCards = zapsPerEvent.map { ZapSetCard(it.key, it.value) } + //val zapCards = zapsPerEvent.map { ZapSetCard(it.key, it.value) } val boostsPerEvent = mutableMapOf>() notes @@ -95,11 +95,20 @@ open class CardFeedViewModel(val dataSource: FeedFilter): ViewModel() { boostsPerEvent.getOrPut(boostedPost, { mutableListOf() }).add(it) } - val boostCards = boostsPerEvent.map { BoostSetCard(it.key, it.value) } + //val boostCards = boostsPerEvent.map { BoostSetCard(it.key, it.value) } + + val allBaseNotes = zapsPerEvent.keys + boostsPerEvent.keys + reactionsPerEvent.keys + val multiCards = allBaseNotes.map { + MultiSetCard(it, + boostsPerEvent.get(it) ?: emptyList(), + reactionsPerEvent.get(it) ?: emptyList(), + zapsPerEvent.get(it) ?: emptyMap() + ) + } val textNoteCards = notes.filter { it.event !is ReactionEvent && it.event !is RepostEvent && it.event !is LnZapEvent }.map { NoteCard(it) } - return (reactionCards + boostCards + zapCards + textNoteCards).sortedBy { it.createdAt() }.reversed() + return (multiCards + textNoteCards).sortedBy { it.createdAt() }.reversed() } private fun updateFeed(notes: List) {