From ff9e1556d57f3aac164271cb4a16902119414f99 Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Sat, 30 Sep 2023 16:24:51 -0400 Subject: [PATCH] Increases the performance of the Bottom bar's Notification dot calculations --- .../vitorpamplona/amethyst/model/Account.kt | 9 +- .../amethyst/ui/navigation/AppBottomBar.kt | 164 ++++++------------ .../amethyst/ui/note/BadgeCompose.kt | 7 +- .../amethyst/ui/note/ChannelCardCompose.kt | 46 +++-- .../amethyst/ui/note/ChatroomHeaderCompose.kt | 4 +- .../ui/note/ChatroomMessageCompose.kt | 9 +- .../amethyst/ui/note/MessageSetCompose.kt | 7 +- .../amethyst/ui/note/MultiSetCompose.kt | 6 +- .../amethyst/ui/note/NoteCompose.kt | 2 +- .../amethyst/ui/note/ZapUserSetCompose.kt | 8 +- .../ui/screen/ChatroomListFeedView.kt | 20 +-- .../ui/screen/loggedIn/AccountViewModel.kt | 97 ++++++++++- 12 files changed, 178 insertions(+), 201 deletions(-) diff --git a/app/src/main/java/com/vitorpamplona/amethyst/model/Account.kt b/app/src/main/java/com/vitorpamplona/amethyst/model/Account.kt index 664418268..397a6cf11 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/model/Account.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/model/Account.kt @@ -98,7 +98,6 @@ class Account( // Observers line up here. val live: AccountLiveData = AccountLiveData(this) val liveLanguages: AccountLiveData = AccountLiveData(this) - val liveLastRead: AccountLiveData = AccountLiveData(this) val saveable: AccountLiveData = AccountLiveData(this) @Immutable @@ -3113,12 +3112,14 @@ class Account( live.invalidateData() } - fun markAsRead(route: String, timestampInSecs: Long) { + fun markAsRead(route: String, timestampInSecs: Long): Boolean { val lastTime = lastReadPerRoute[route] - if (lastTime == null || timestampInSecs > lastTime) { + return if (lastTime == null || timestampInSecs > lastTime) { lastReadPerRoute = lastReadPerRoute + Pair(route, timestampInSecs) saveable.invalidateData() - liveLastRead.invalidateData() + true + } else { + false } } diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/navigation/AppBottomBar.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/navigation/AppBottomBar.kt index 71309adc3..1290bb749 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/navigation/AppBottomBar.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/navigation/AppBottomBar.kt @@ -8,7 +8,6 @@ import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.RowScope import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.size -import androidx.compose.foundation.layout.width import androidx.compose.foundation.layout.wrapContentHeight import androidx.compose.foundation.shape.CircleShape import androidx.compose.material3.Divider @@ -19,11 +18,10 @@ import androidx.compose.material3.NavigationBarItem import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.DisposableEffect -import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.State +import androidx.compose.runtime.collectAsState import androidx.compose.runtime.derivedStateOf 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 @@ -38,14 +36,14 @@ import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import androidx.navigation.NavBackStackEntry -import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.amethyst.ui.theme.BottomTopHeight import com.vitorpamplona.amethyst.ui.theme.DividerThickness -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.launch +import com.vitorpamplona.amethyst.ui.theme.Size0dp +import com.vitorpamplona.amethyst.ui.theme.Size10dp +import kotlinx.collections.immutable.persistentListOf -val bottomNavigationItems = listOf( +val bottomNavigationItems = persistentListOf( Route.Home, Route.Message, Route.Video, @@ -61,6 +59,9 @@ enum class Keyboard { fun keyboardAsState(): State { val keyboardState = remember { mutableStateOf(Keyboard.Closed) } val view = LocalView.current + + println("AAA - KeyboardState") + DisposableEffect(view) { val onGlobalListener = ViewTreeObserver.OnGlobalLayoutListener { val rect = Rect() @@ -106,9 +107,7 @@ private fun RenderBottomMenu( Divider( thickness = DividerThickness ) - NavigationBar( - tonalElevation = 0.dp - ) { + NavigationBar(tonalElevation = Size0dp) { bottomNavigationItems.forEach { item -> HasNewItemsIcon(item, accountViewModel, navEntryState, nav) } @@ -123,14 +122,14 @@ private fun RowScope.HasNewItemsIcon( navEntryState: State, nav: (Route, Boolean) -> Unit ) { - var hasNewItems by remember { mutableStateOf(false) } - - WatchPossibleNotificationChanges(route, accountViewModel) { - if (it != hasNewItems) { - hasNewItems = it + val selected by remember(navEntryState.value) { + derivedStateOf { + navEntryState.value?.destination?.route?.substringBefore("?") == route.base } } + println("AAA HasNewItemsIcon") + val size = remember { if ("Home" == route.base) 25.dp else 23.dp } @@ -138,76 +137,12 @@ private fun RowScope.HasNewItemsIcon( if ("Home" == route.base) 24.dp else 20.dp } - BottomIcon( - icon = route.icon, - size = size, - iconSize = iconSize, - base = route.base, - hasNewItems = hasNewItems, - navEntryState = navEntryState - ) { selected -> - nav(route, selected) - } -} - -@Composable -fun WatchPossibleNotificationChanges( - route: Route, - accountViewModel: AccountViewModel, - onChange: (Boolean) -> Unit -) { - val accountState by accountViewModel.accountLiveData.observeAsState() - val notifState by accountViewModel.accountLastReadLiveData.observeAsState() - - LaunchedEffect(key1 = notifState, key2 = accountState) { - launch(Dispatchers.IO) { - onChange(route.hasNewItems(accountViewModel.account, emptySet())) - } - } - - LaunchedEffect(Unit) { - launch(Dispatchers.IO) { - LocalCache.live.newEventBundles.collect { - launch(Dispatchers.IO) { - onChange(route.hasNewItems(accountViewModel.account, it)) - } - } - } - } -} - -@Composable -private fun RowScope.BottomIcon( - icon: Int, - size: Dp, - iconSize: Dp, - base: String, - hasNewItems: Boolean, - navEntryState: State, - onClick: (Boolean) -> Unit -) { - val selected by remember(navEntryState.value) { - derivedStateOf { - navEntryState.value?.destination?.route?.substringBefore("?") == base - } - } - - NavigationIcon(icon, size, iconSize, selected, hasNewItems, onClick) -} - -@Composable -private fun RowScope.NavigationIcon( - icon: Int, - size: Dp, - iconSize: Dp, - selected: Boolean, - hasNewItems: Boolean, - onClick: (Boolean) -> Unit -) { NavigationBarItem( icon = { + val hasNewItems = accountViewModel.notificationDots.hasNewItems[route]?.collectAsState() + NotifiableIcon( - icon, + route.icon, size, iconSize, selected, @@ -215,12 +150,18 @@ private fun RowScope.NavigationIcon( ) }, selected = selected, - onClick = { onClick(selected) } + onClick = { nav(route, selected) } ) } @Composable -private fun NotifiableIcon(icon: Int, size: Dp, iconSize: Dp, selected: Boolean, hasNewItems: Boolean) { +private fun NotifiableIcon( + icon: Int, + size: Dp, + iconSize: Dp, + selected: Boolean, + hasNewItems: State? +) { Box(remember { Modifier.size(size) }) { Icon( painter = painterResource(id = icon), @@ -229,37 +170,36 @@ private fun NotifiableIcon(icon: Int, size: Dp, iconSize: Dp, selected: Boolean, tint = if (selected) MaterialTheme.colorScheme.primary else Color.Unspecified ) - if (hasNewItems) { - Box( - remember { + if (hasNewItems?.value == true) { + NotificationDotIcon( + Modifier.align(Alignment.TopEnd) + ) + } + } +} + +@Composable +private fun NotificationDotIcon(modifier: Modifier) { + Box(modifier.size(Size10dp)) { + Box( + modifier = remember { + Modifier + .size(Size10dp) + .clip(shape = CircleShape) + }.background(MaterialTheme.colorScheme.primary), + contentAlignment = Alignment.TopEnd + ) { + Text( + "", + color = Color.White, + textAlign = TextAlign.Center, + fontSize = 12.sp, + modifier = remember { Modifier - .width(10.dp) - .height(10.dp) + .wrapContentHeight() .align(Alignment.TopEnd) } - ) { - Box( - modifier = remember { - Modifier - .width(10.dp) - .height(10.dp) - .clip(shape = CircleShape) - }.background(MaterialTheme.colorScheme.primary), - contentAlignment = Alignment.TopEnd - ) { - Text( - "", - color = Color.White, - textAlign = TextAlign.Center, - fontSize = 12.sp, - modifier = remember { - Modifier - .wrapContentHeight() - .align(Alignment.TopEnd) - } - ) - } - } + ) } } } diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/BadgeCompose.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/BadgeCompose.kt index 57fe154cf..3b1f786b7 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/BadgeCompose.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/BadgeCompose.kt @@ -38,7 +38,6 @@ import com.vitorpamplona.amethyst.ui.screen.BadgeCard import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.amethyst.ui.theme.newItemBackgroundColor import com.vitorpamplona.amethyst.ui.theme.placeholderText -import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch @OptIn(ExperimentalFoundationApi::class) @@ -64,11 +63,7 @@ fun BadgeCompose(likeSetCard: BadgeCard, isInnerNote: Boolean = false, routeForL val newItemColor = MaterialTheme.colorScheme.newItemBackgroundColor LaunchedEffect(key1 = likeSetCard) { - scope.launch(Dispatchers.IO) { - val isNew = likeSetCard.createdAt() > accountViewModel.account.loadLastRead(routeForLastRead) - - accountViewModel.account.markAsRead(routeForLastRead, likeSetCard.createdAt()) - + accountViewModel.loadAndMarkAsRead(routeForLastRead, likeSetCard.createdAt()) { isNew -> val newBackgroundColor = if (isNew) { newItemColor.compositeOver(defaultBackgroundColor) } else { diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/ChannelCardCompose.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/ChannelCardCompose.kt index e84da6ef8..bc389312a 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/ChannelCardCompose.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/ChannelCardCompose.kt @@ -277,37 +277,24 @@ private fun CheckNewAndRenderChannelCard( ) { val newItemColor = MaterialTheme.colorScheme.newItemBackgroundColor val defaultBackgroundColor = MaterialTheme.colorScheme.background - val backgroundColor = remember { mutableStateOf(defaultBackgroundColor) } + val backgroundColor = remember { + mutableStateOf( + parentBackgroundColor?.value ?: defaultBackgroundColor + ) + } LaunchedEffect(key1 = routeForLastRead, key2 = parentBackgroundColor?.value) { - launch(Dispatchers.IO) { - routeForLastRead?.let { - val lastTime = accountViewModel.account.loadLastRead(it) - - val createdAt = baseNote.createdAt() - if (createdAt != null) { - accountViewModel.account.markAsRead(it, createdAt) - - val isNew = createdAt > lastTime - - val newBackgroundColor = if (isNew) { - if (parentBackgroundColor != null) { - newItemColor.compositeOver(parentBackgroundColor.value) - } else { - newItemColor.compositeOver(defaultBackgroundColor) - } + routeForLastRead?.let { + accountViewModel.loadAndMarkAsRead(routeForLastRead, baseNote.createdAt()) { isNew -> + val newBackgroundColor = if (isNew) { + if (parentBackgroundColor != null) { + newItemColor.compositeOver(parentBackgroundColor.value) } else { - parentBackgroundColor?.value ?: defaultBackgroundColor - } - - if (newBackgroundColor != backgroundColor.value) { - launch(Dispatchers.Main) { - backgroundColor.value = newBackgroundColor - } + newItemColor.compositeOver(defaultBackgroundColor) } + } else { + parentBackgroundColor?.value ?: defaultBackgroundColor } - } ?: run { - val newBackgroundColor = parentBackgroundColor?.value ?: defaultBackgroundColor if (newBackgroundColor != backgroundColor.value) { launch(Dispatchers.Main) { @@ -315,6 +302,13 @@ private fun CheckNewAndRenderChannelCard( } } } + } ?: run { + val newBackgroundColor = parentBackgroundColor?.value ?: defaultBackgroundColor + if (newBackgroundColor != backgroundColor.value) { + launch(Dispatchers.Main) { + backgroundColor.value = newBackgroundColor + } + } } } diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/ChatroomHeaderCompose.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/ChatroomHeaderCompose.kt index 06938aad8..d8ad31b7b 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/ChatroomHeaderCompose.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/ChatroomHeaderCompose.kt @@ -415,9 +415,7 @@ private fun WatchNotificationChanges( accountViewModel: AccountViewModel, onNewStatus: (Boolean) -> Unit ) { - val cacheState by accountViewModel.accountLastReadLiveData.observeAsState() - - LaunchedEffect(key1 = note, cacheState) { + LaunchedEffect(key1 = note, accountViewModel.accountMarkAsReadUpdates.value) { launch(Dispatchers.IO) { note.event?.createdAt()?.let { val lastTime = accountViewModel.account.loadLastRead(route) diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/ChatroomMessageCompose.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/ChatroomMessageCompose.kt index dee028edd..712d0536d 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/ChatroomMessageCompose.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/ChatroomMessageCompose.kt @@ -73,8 +73,6 @@ import com.vitorpamplona.quartz.events.EmptyTagList import com.vitorpamplona.quartz.events.ImmutableListOfLists import com.vitorpamplona.quartz.events.PrivateDmEvent import com.vitorpamplona.quartz.events.toImmutableListOfLists -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.launch @OptIn(ExperimentalFoundationApi::class) @Composable @@ -260,12 +258,7 @@ fun NormalChatNote( if (routeForLastRead != null) { LaunchedEffect(key1 = routeForLastRead) { - launch(Dispatchers.IO) { - val createdAt = note.createdAt() - if (createdAt != null) { - accountViewModel.account.markAsRead(routeForLastRead, createdAt) - } - } + accountViewModel.loadAndMarkAsRead(routeForLastRead, note.createdAt()) { } } } diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/MessageSetCompose.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/MessageSetCompose.kt index 937acc83d..d6a6e222d 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/MessageSetCompose.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/MessageSetCompose.kt @@ -28,7 +28,6 @@ import com.vitorpamplona.amethyst.ui.screen.MessageSetCard import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.amethyst.ui.theme.DividerThickness import com.vitorpamplona.amethyst.ui.theme.newItemBackgroundColor -import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch @OptIn(ExperimentalFoundationApi::class) @@ -48,11 +47,7 @@ fun MessageSetCompose(messageSetCard: MessageSetCard, routeForLastRead: String, val newItemColor = MaterialTheme.colorScheme.newItemBackgroundColor LaunchedEffect(key1 = messageSetCard) { - launch(Dispatchers.IO) { - val isNew = messageSetCard.createdAt() > accountViewModel.account.loadLastRead(routeForLastRead) - - accountViewModel.account.markAsRead(routeForLastRead, messageSetCard.createdAt()) - + accountViewModel.loadAndMarkAsRead(routeForLastRead, messageSetCard.createdAt()) { isNew -> val newBackgroundColor = if (isNew) { newItemColor.compositeOver(defaultBackgroundColor) } else { 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 51117d9f6..53f653af0 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 @@ -96,11 +96,7 @@ fun MultiSetCompose(multiSetCard: MultiSetCard, routeForLastRead: String, showHi val newItemColor = MaterialTheme.colorScheme.newItemBackgroundColor LaunchedEffect(key1 = multiSetCard) { - launch(Dispatchers.IO) { - val isNew = multiSetCard.maxCreatedAt > accountViewModel.account.loadLastRead(routeForLastRead) - - accountViewModel.account.markAsRead(routeForLastRead, multiSetCard.maxCreatedAt) - + accountViewModel.loadAndMarkAsRead(routeForLastRead, multiSetCard.maxCreatedAt) { isNew -> val newBackgroundColor = if (isNew) { newItemColor.compositeOver(defaultBackgroundColor) } else { diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/NoteCompose.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/NoteCompose.kt index 755daba5e..d528e4726 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/NoteCompose.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/NoteCompose.kt @@ -837,7 +837,7 @@ private fun CheckNewAndRenderNote( ) { val newItemColor = MaterialTheme.colorScheme.newItemBackgroundColor val defaultBackgroundColor = MaterialTheme.colorScheme.background - val backgroundColor = remember { mutableStateOf(defaultBackgroundColor) } + val backgroundColor = remember(baseNote) { mutableStateOf(parentBackgroundColor?.value ?: defaultBackgroundColor) } LaunchedEffect(key1 = routeForLastRead, key2 = parentBackgroundColor?.value) { routeForLastRead?.let { diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/ZapUserSetCompose.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/ZapUserSetCompose.kt index 51e40ec09..c65e666e8 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/ZapUserSetCompose.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/ZapUserSetCompose.kt @@ -30,8 +30,6 @@ import com.vitorpamplona.amethyst.ui.theme.Size25dp import com.vitorpamplona.amethyst.ui.theme.Size55Modifier import com.vitorpamplona.amethyst.ui.theme.Size55dp import com.vitorpamplona.amethyst.ui.theme.newItemBackgroundColor -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.launch @Composable fun ZapUserSetCompose(zapSetCard: ZapUserSetCard, isInnerNote: Boolean = false, routeForLastRead: String, accountViewModel: AccountViewModel, nav: (String) -> Unit) { @@ -40,11 +38,7 @@ fun ZapUserSetCompose(zapSetCard: ZapUserSetCard, isInnerNote: Boolean = false, val newItemColor = MaterialTheme.colorScheme.newItemBackgroundColor LaunchedEffect(key1 = zapSetCard.createdAt()) { - launch(Dispatchers.IO) { - val isNew = zapSetCard.createdAt > accountViewModel.account.loadLastRead(routeForLastRead) - - accountViewModel.account.markAsRead(routeForLastRead, zapSetCard.createdAt) - + accountViewModel.loadAndMarkAsRead(routeForLastRead, zapSetCard.createdAt) { isNew -> val newBackgroundColor = if (isNew) { newItemColor.compositeOver(defaultBackgroundColor) } else { diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/ChatroomListFeedView.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/ChatroomListFeedView.kt index c0b42e5fe..d5e2d9105 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/ChatroomListFeedView.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/ChatroomListFeedView.kt @@ -18,7 +18,6 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp import com.vitorpamplona.amethyst.ui.note.ChatroomHeaderCompose import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel -import com.vitorpamplona.quartz.events.ChatroomKeyable import kotlin.time.ExperimentalTime import kotlin.time.measureTimedValue @@ -83,24 +82,9 @@ private fun FeedLoaded( LaunchedEffect(key1 = markAsRead.value) { if (markAsRead.value) { - for (note in state.feed.value) { - note.event?.let { noteEvent -> - val channelHex = note.channelHex() - val route = if (channelHex != null) { - "Channel/$channelHex" - } else if (note.event is ChatroomKeyable) { - val withKey = (note.event as ChatroomKeyable).chatroomKey(accountViewModel.userProfile().pubkeyHex) - "Room/${withKey.hashCode()}" - } else { - null - } - - route?.let { - accountViewModel.account.markAsRead(route, noteEvent.createdAt()) - } - } + accountViewModel.markAllAsRead(state.feed.value) { + markAsRead.value = false } - markAsRead.value = false } } diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/AccountViewModel.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/AccountViewModel.kt index 1f655684c..0cf0c98bd 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/AccountViewModel.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/AccountViewModel.kt @@ -1,8 +1,10 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn import android.content.Context +import android.util.Log import androidx.compose.runtime.Immutable import androidx.compose.runtime.Stable +import androidx.compose.runtime.mutableStateOf import androidx.lifecycle.LiveData import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModelProvider @@ -30,6 +32,8 @@ import com.vitorpamplona.amethyst.service.checkNotInMainThread import com.vitorpamplona.amethyst.ui.actions.Dao import com.vitorpamplona.amethyst.ui.components.MarkdownParser import com.vitorpamplona.amethyst.ui.components.UrlPreviewState +import com.vitorpamplona.amethyst.ui.navigation.Route +import com.vitorpamplona.amethyst.ui.navigation.bottomNavigationItems import com.vitorpamplona.amethyst.ui.note.ZapAmountCommentNotification import com.vitorpamplona.amethyst.ui.note.ZapraiserStatus import com.vitorpamplona.amethyst.ui.note.showAmount @@ -38,6 +42,7 @@ import com.vitorpamplona.quartz.encoders.ATag import com.vitorpamplona.quartz.encoders.HexKey import com.vitorpamplona.quartz.encoders.Nip19 import com.vitorpamplona.quartz.events.ChatroomKey +import com.vitorpamplona.quartz.events.ChatroomKeyable import com.vitorpamplona.quartz.events.Event import com.vitorpamplona.quartz.events.GiftWrapEvent import com.vitorpamplona.quartz.events.ImmutableListOfLists @@ -54,15 +59,18 @@ import kotlinx.collections.immutable.persistentSetOf import kotlinx.collections.immutable.toImmutableList import kotlinx.collections.immutable.toImmutableSet import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.Job +import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.launch import java.math.BigDecimal import java.util.Locale +import kotlin.time.measureTimedValue @Stable class AccountViewModel(val account: Account) : ViewModel(), Dao { val accountLiveData: LiveData = account.live.map { it } val accountLanguagesLiveData: LiveData = account.liveLanguages.map { it } - val accountLastReadLiveData: LiveData = account.liveLastRead.map { it } + val accountMarkAsReadUpdates = mutableStateOf(0) val userFollows: LiveData = account.userProfile().live().follows.map { it } val userRelays: LiveData = account.userProfile().live().relays.map { it } @@ -727,19 +735,59 @@ class AccountViewModel(val account: Account) : ViewModel(), Dao { } } - fun loadAndMarkAsRead(routeForLastRead: String, baseNoteCreatedAt: Long?, onIsNew: (Boolean) -> Unit) { + fun refreshMarkAsReadObservers() { + updateNotificationDots() + accountMarkAsReadUpdates.value++ + } + + fun loadAndMarkAsRead(routeForLastRead: String, createdAt: Long?, onIsNew: (Boolean) -> Unit) { viewModelScope.launch(Dispatchers.IO) { val lastTime = account.loadLastRead(routeForLastRead) - if (baseNoteCreatedAt != null) { - account.markAsRead(routeForLastRead, baseNoteCreatedAt) - onIsNew(baseNoteCreatedAt > lastTime) + if (createdAt != null) { + if (account.markAsRead(routeForLastRead, createdAt)) { + refreshMarkAsReadObservers() + } + onIsNew(createdAt > lastTime) } else { onIsNew(false) } } } + fun markAllAsRead(notes: ImmutableList, onDone: () -> Unit) { + viewModelScope.launch(Dispatchers.IO) { + var atLeastOne = false + + for (note in notes) { + note.event?.let { noteEvent -> + val channelHex = note.channelHex() + val route = if (channelHex != null) { + "Channel/$channelHex" + } else if (note.event is ChatroomKeyable) { + val withKey = + (note.event as ChatroomKeyable).chatroomKey(userProfile().pubkeyHex) + "Room/${withKey.hashCode()}" + } else { + null + } + + route?.let { + if (account.markAsRead(route, noteEvent.createdAt())) { + atLeastOne = true + } + } + } + } + + if (atLeastOne) { + refreshMarkAsReadObservers() + } + + onDone() + } + } + fun createChatRoomFor(user: User, then: (Int) -> Unit) { viewModelScope.launch(Dispatchers.IO) { val withKey = ChatroomKey(persistentSetOf(user.pubkeyHex)) @@ -753,6 +801,45 @@ class AccountViewModel(val account: Account) : ViewModel(), Dao { return AccountViewModel(account) as AccountViewModel } } + + private var collectorJob: Job? = null + val notificationDots = HasNotificationDot(bottomNavigationItems, account) + + fun updateNotificationDots(newNotes: Set = emptySet()) { + viewModelScope.launch(Dispatchers.Default) { + val (value, elapsed) = measureTimedValue { + notificationDots.update(newNotes) + } + Log.d("Rendering Metrics", "Notification Dots Calculation in $elapsed for ${newNotes.size} new notes") + } + } + + init { + Log.d("Init", "AccountViewModel") + collectorJob = viewModelScope.launch(Dispatchers.IO) { + LocalCache.live.newEventBundles.collect { newNotes -> + updateNotificationDots(newNotes) + } + } + } + + override fun onCleared() { + collectorJob?.cancel() + super.onCleared() + } +} + +class HasNotificationDot(bottomNavigationItems: ImmutableList, val account: Account) { + val hasNewItems = bottomNavigationItems.associateWith { MutableStateFlow(false) } + + fun update(newNotes: Set) { + hasNewItems.forEach { + val newResult = it.key.hasNewItems(account, newNotes) + if (newResult != it.value.value) { + it.value.value = newResult + } + } + } } @Immutable