Increases the performance of the Bottom bar's Notification dot calculations

This commit is contained in:
Vitor Pamplona
2023-09-30 16:24:51 -04:00
parent dffb49b071
commit ff9e1556d5
12 changed files with 181 additions and 204 deletions
@@ -98,7 +98,6 @@ class Account(
// Observers line up here. // Observers line up here.
val live: AccountLiveData = AccountLiveData(this) val live: AccountLiveData = AccountLiveData(this)
val liveLanguages: AccountLiveData = AccountLiveData(this) val liveLanguages: AccountLiveData = AccountLiveData(this)
val liveLastRead: AccountLiveData = AccountLiveData(this)
val saveable: AccountLiveData = AccountLiveData(this) val saveable: AccountLiveData = AccountLiveData(this)
@Immutable @Immutable
@@ -3113,12 +3112,14 @@ class Account(
live.invalidateData() live.invalidateData()
} }
fun markAsRead(route: String, timestampInSecs: Long) { fun markAsRead(route: String, timestampInSecs: Long): Boolean {
val lastTime = lastReadPerRoute[route] val lastTime = lastReadPerRoute[route]
if (lastTime == null || timestampInSecs > lastTime) { return if (lastTime == null || timestampInSecs > lastTime) {
lastReadPerRoute = lastReadPerRoute + Pair(route, timestampInSecs) lastReadPerRoute = lastReadPerRoute + Pair(route, timestampInSecs)
saveable.invalidateData() saveable.invalidateData()
liveLastRead.invalidateData() true
} else {
false
} }
} }
@@ -8,7 +8,6 @@ import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.RowScope import androidx.compose.foundation.layout.RowScope
import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.layout.wrapContentHeight import androidx.compose.foundation.layout.wrapContentHeight
import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.shape.CircleShape
import androidx.compose.material3.Divider import androidx.compose.material3.Divider
@@ -19,11 +18,10 @@ import androidx.compose.material3.NavigationBarItem
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.State import androidx.compose.runtime.State
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.livedata.observeAsState
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue 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.dp
import androidx.compose.ui.unit.sp import androidx.compose.ui.unit.sp
import androidx.navigation.NavBackStackEntry import androidx.navigation.NavBackStackEntry
import com.vitorpamplona.amethyst.model.LocalCache
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
import com.vitorpamplona.amethyst.ui.theme.BottomTopHeight import com.vitorpamplona.amethyst.ui.theme.BottomTopHeight
import com.vitorpamplona.amethyst.ui.theme.DividerThickness import com.vitorpamplona.amethyst.ui.theme.DividerThickness
import kotlinx.coroutines.Dispatchers import com.vitorpamplona.amethyst.ui.theme.Size0dp
import kotlinx.coroutines.launch import com.vitorpamplona.amethyst.ui.theme.Size10dp
import kotlinx.collections.immutable.persistentListOf
val bottomNavigationItems = listOf( val bottomNavigationItems = persistentListOf(
Route.Home, Route.Home,
Route.Message, Route.Message,
Route.Video, Route.Video,
@@ -61,6 +59,9 @@ enum class Keyboard {
fun keyboardAsState(): State<Keyboard> { fun keyboardAsState(): State<Keyboard> {
val keyboardState = remember { mutableStateOf(Keyboard.Closed) } val keyboardState = remember { mutableStateOf(Keyboard.Closed) }
val view = LocalView.current val view = LocalView.current
println("AAA - KeyboardState")
DisposableEffect(view) { DisposableEffect(view) {
val onGlobalListener = ViewTreeObserver.OnGlobalLayoutListener { val onGlobalListener = ViewTreeObserver.OnGlobalLayoutListener {
val rect = Rect() val rect = Rect()
@@ -106,9 +107,7 @@ private fun RenderBottomMenu(
Divider( Divider(
thickness = DividerThickness thickness = DividerThickness
) )
NavigationBar( NavigationBar(tonalElevation = Size0dp) {
tonalElevation = 0.dp
) {
bottomNavigationItems.forEach { item -> bottomNavigationItems.forEach { item ->
HasNewItemsIcon(item, accountViewModel, navEntryState, nav) HasNewItemsIcon(item, accountViewModel, navEntryState, nav)
} }
@@ -123,14 +122,14 @@ private fun RowScope.HasNewItemsIcon(
navEntryState: State<NavBackStackEntry?>, navEntryState: State<NavBackStackEntry?>,
nav: (Route, Boolean) -> Unit nav: (Route, Boolean) -> Unit
) { ) {
var hasNewItems by remember { mutableStateOf(false) } val selected by remember(navEntryState.value) {
derivedStateOf {
WatchPossibleNotificationChanges(route, accountViewModel) { navEntryState.value?.destination?.route?.substringBefore("?") == route.base
if (it != hasNewItems) {
hasNewItems = it
} }
} }
println("AAA HasNewItemsIcon")
val size = remember { val size = remember {
if ("Home" == route.base) 25.dp else 23.dp 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 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<NavBackStackEntry?>,
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( NavigationBarItem(
icon = { icon = {
val hasNewItems = accountViewModel.notificationDots.hasNewItems[route]?.collectAsState()
NotifiableIcon( NotifiableIcon(
icon, route.icon,
size, size,
iconSize, iconSize,
selected, selected,
@@ -215,12 +150,18 @@ private fun RowScope.NavigationIcon(
) )
}, },
selected = selected, selected = selected,
onClick = { onClick(selected) } onClick = { nav(route, selected) }
) )
} }
@Composable @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<Boolean>?
) {
Box(remember { Modifier.size(size) }) { Box(remember { Modifier.size(size) }) {
Icon( Icon(
painter = painterResource(id = 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 tint = if (selected) MaterialTheme.colorScheme.primary else Color.Unspecified
) )
if (hasNewItems) { if (hasNewItems?.value == true) {
Box( NotificationDotIcon(
remember { Modifier.align(Alignment.TopEnd)
Modifier )
.width(10.dp) }
.height(10.dp) }
.align(Alignment.TopEnd) }
}
) { @Composable
Box( private fun NotificationDotIcon(modifier: Modifier) {
modifier = remember { Box(modifier.size(Size10dp)) {
Modifier Box(
.width(10.dp) modifier = remember {
.height(10.dp) Modifier
.clip(shape = CircleShape) .size(Size10dp)
}.background(MaterialTheme.colorScheme.primary), .clip(shape = CircleShape)
contentAlignment = Alignment.TopEnd }.background(MaterialTheme.colorScheme.primary),
) { contentAlignment = Alignment.TopEnd
Text( ) {
"", Text(
color = Color.White, "",
textAlign = TextAlign.Center, color = Color.White,
fontSize = 12.sp, textAlign = TextAlign.Center,
modifier = remember { fontSize = 12.sp,
Modifier modifier = remember {
.wrapContentHeight() Modifier
.align(Alignment.TopEnd) .wrapContentHeight()
} .align(Alignment.TopEnd)
) }
} )
}
} }
} }
} }
@@ -38,7 +38,6 @@ import com.vitorpamplona.amethyst.ui.screen.BadgeCard
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
import com.vitorpamplona.amethyst.ui.theme.newItemBackgroundColor import com.vitorpamplona.amethyst.ui.theme.newItemBackgroundColor
import com.vitorpamplona.amethyst.ui.theme.placeholderText import com.vitorpamplona.amethyst.ui.theme.placeholderText
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
@OptIn(ExperimentalFoundationApi::class) @OptIn(ExperimentalFoundationApi::class)
@@ -64,11 +63,7 @@ fun BadgeCompose(likeSetCard: BadgeCard, isInnerNote: Boolean = false, routeForL
val newItemColor = MaterialTheme.colorScheme.newItemBackgroundColor val newItemColor = MaterialTheme.colorScheme.newItemBackgroundColor
LaunchedEffect(key1 = likeSetCard) { LaunchedEffect(key1 = likeSetCard) {
scope.launch(Dispatchers.IO) { accountViewModel.loadAndMarkAsRead(routeForLastRead, likeSetCard.createdAt()) { isNew ->
val isNew = likeSetCard.createdAt() > accountViewModel.account.loadLastRead(routeForLastRead)
accountViewModel.account.markAsRead(routeForLastRead, likeSetCard.createdAt())
val newBackgroundColor = if (isNew) { val newBackgroundColor = if (isNew) {
newItemColor.compositeOver(defaultBackgroundColor) newItemColor.compositeOver(defaultBackgroundColor)
} else { } else {
@@ -277,37 +277,24 @@ private fun CheckNewAndRenderChannelCard(
) { ) {
val newItemColor = MaterialTheme.colorScheme.newItemBackgroundColor val newItemColor = MaterialTheme.colorScheme.newItemBackgroundColor
val defaultBackgroundColor = MaterialTheme.colorScheme.background val defaultBackgroundColor = MaterialTheme.colorScheme.background
val backgroundColor = remember { mutableStateOf<Color>(defaultBackgroundColor) } val backgroundColor = remember {
mutableStateOf<Color>(
parentBackgroundColor?.value ?: defaultBackgroundColor
)
}
LaunchedEffect(key1 = routeForLastRead, key2 = parentBackgroundColor?.value) { LaunchedEffect(key1 = routeForLastRead, key2 = parentBackgroundColor?.value) {
launch(Dispatchers.IO) { routeForLastRead?.let {
routeForLastRead?.let { accountViewModel.loadAndMarkAsRead(routeForLastRead, baseNote.createdAt()) { isNew ->
val lastTime = accountViewModel.account.loadLastRead(it) val newBackgroundColor = if (isNew) {
if (parentBackgroundColor != null) {
val createdAt = baseNote.createdAt() newItemColor.compositeOver(parentBackgroundColor.value)
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)
}
} else { } else {
parentBackgroundColor?.value ?: defaultBackgroundColor newItemColor.compositeOver(defaultBackgroundColor)
}
if (newBackgroundColor != backgroundColor.value) {
launch(Dispatchers.Main) {
backgroundColor.value = newBackgroundColor
}
} }
} else {
parentBackgroundColor?.value ?: defaultBackgroundColor
} }
} ?: run {
val newBackgroundColor = parentBackgroundColor?.value ?: defaultBackgroundColor
if (newBackgroundColor != backgroundColor.value) { if (newBackgroundColor != backgroundColor.value) {
launch(Dispatchers.Main) { 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
}
}
} }
} }
@@ -415,9 +415,7 @@ private fun WatchNotificationChanges(
accountViewModel: AccountViewModel, accountViewModel: AccountViewModel,
onNewStatus: (Boolean) -> Unit onNewStatus: (Boolean) -> Unit
) { ) {
val cacheState by accountViewModel.accountLastReadLiveData.observeAsState() LaunchedEffect(key1 = note, accountViewModel.accountMarkAsReadUpdates.value) {
LaunchedEffect(key1 = note, cacheState) {
launch(Dispatchers.IO) { launch(Dispatchers.IO) {
note.event?.createdAt()?.let { note.event?.createdAt()?.let {
val lastTime = accountViewModel.account.loadLastRead(route) val lastTime = accountViewModel.account.loadLastRead(route)
@@ -73,8 +73,6 @@ import com.vitorpamplona.quartz.events.EmptyTagList
import com.vitorpamplona.quartz.events.ImmutableListOfLists import com.vitorpamplona.quartz.events.ImmutableListOfLists
import com.vitorpamplona.quartz.events.PrivateDmEvent import com.vitorpamplona.quartz.events.PrivateDmEvent
import com.vitorpamplona.quartz.events.toImmutableListOfLists import com.vitorpamplona.quartz.events.toImmutableListOfLists
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
@OptIn(ExperimentalFoundationApi::class) @OptIn(ExperimentalFoundationApi::class)
@Composable @Composable
@@ -260,12 +258,7 @@ fun NormalChatNote(
if (routeForLastRead != null) { if (routeForLastRead != null) {
LaunchedEffect(key1 = routeForLastRead) { LaunchedEffect(key1 = routeForLastRead) {
launch(Dispatchers.IO) { accountViewModel.loadAndMarkAsRead(routeForLastRead, note.createdAt()) { }
val createdAt = note.createdAt()
if (createdAt != null) {
accountViewModel.account.markAsRead(routeForLastRead, createdAt)
}
}
} }
} }
@@ -28,7 +28,6 @@ import com.vitorpamplona.amethyst.ui.screen.MessageSetCard
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
import com.vitorpamplona.amethyst.ui.theme.DividerThickness import com.vitorpamplona.amethyst.ui.theme.DividerThickness
import com.vitorpamplona.amethyst.ui.theme.newItemBackgroundColor import com.vitorpamplona.amethyst.ui.theme.newItemBackgroundColor
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
@OptIn(ExperimentalFoundationApi::class) @OptIn(ExperimentalFoundationApi::class)
@@ -48,11 +47,7 @@ fun MessageSetCompose(messageSetCard: MessageSetCard, routeForLastRead: String,
val newItemColor = MaterialTheme.colorScheme.newItemBackgroundColor val newItemColor = MaterialTheme.colorScheme.newItemBackgroundColor
LaunchedEffect(key1 = messageSetCard) { LaunchedEffect(key1 = messageSetCard) {
launch(Dispatchers.IO) { accountViewModel.loadAndMarkAsRead(routeForLastRead, messageSetCard.createdAt()) { isNew ->
val isNew = messageSetCard.createdAt() > accountViewModel.account.loadLastRead(routeForLastRead)
accountViewModel.account.markAsRead(routeForLastRead, messageSetCard.createdAt())
val newBackgroundColor = if (isNew) { val newBackgroundColor = if (isNew) {
newItemColor.compositeOver(defaultBackgroundColor) newItemColor.compositeOver(defaultBackgroundColor)
} else { } else {
@@ -96,11 +96,7 @@ fun MultiSetCompose(multiSetCard: MultiSetCard, routeForLastRead: String, showHi
val newItemColor = MaterialTheme.colorScheme.newItemBackgroundColor val newItemColor = MaterialTheme.colorScheme.newItemBackgroundColor
LaunchedEffect(key1 = multiSetCard) { LaunchedEffect(key1 = multiSetCard) {
launch(Dispatchers.IO) { accountViewModel.loadAndMarkAsRead(routeForLastRead, multiSetCard.maxCreatedAt) { isNew ->
val isNew = multiSetCard.maxCreatedAt > accountViewModel.account.loadLastRead(routeForLastRead)
accountViewModel.account.markAsRead(routeForLastRead, multiSetCard.maxCreatedAt)
val newBackgroundColor = if (isNew) { val newBackgroundColor = if (isNew) {
newItemColor.compositeOver(defaultBackgroundColor) newItemColor.compositeOver(defaultBackgroundColor)
} else { } else {
@@ -837,7 +837,7 @@ private fun CheckNewAndRenderNote(
) { ) {
val newItemColor = MaterialTheme.colorScheme.newItemBackgroundColor val newItemColor = MaterialTheme.colorScheme.newItemBackgroundColor
val defaultBackgroundColor = MaterialTheme.colorScheme.background val defaultBackgroundColor = MaterialTheme.colorScheme.background
val backgroundColor = remember { mutableStateOf<Color>(defaultBackgroundColor) } val backgroundColor = remember(baseNote) { mutableStateOf<Color>(parentBackgroundColor?.value ?: defaultBackgroundColor) }
LaunchedEffect(key1 = routeForLastRead, key2 = parentBackgroundColor?.value) { LaunchedEffect(key1 = routeForLastRead, key2 = parentBackgroundColor?.value) {
routeForLastRead?.let { routeForLastRead?.let {
@@ -30,8 +30,6 @@ import com.vitorpamplona.amethyst.ui.theme.Size25dp
import com.vitorpamplona.amethyst.ui.theme.Size55Modifier import com.vitorpamplona.amethyst.ui.theme.Size55Modifier
import com.vitorpamplona.amethyst.ui.theme.Size55dp import com.vitorpamplona.amethyst.ui.theme.Size55dp
import com.vitorpamplona.amethyst.ui.theme.newItemBackgroundColor import com.vitorpamplona.amethyst.ui.theme.newItemBackgroundColor
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
@Composable @Composable
fun ZapUserSetCompose(zapSetCard: ZapUserSetCard, isInnerNote: Boolean = false, routeForLastRead: String, accountViewModel: AccountViewModel, nav: (String) -> Unit) { 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 val newItemColor = MaterialTheme.colorScheme.newItemBackgroundColor
LaunchedEffect(key1 = zapSetCard.createdAt()) { LaunchedEffect(key1 = zapSetCard.createdAt()) {
launch(Dispatchers.IO) { accountViewModel.loadAndMarkAsRead(routeForLastRead, zapSetCard.createdAt) { isNew ->
val isNew = zapSetCard.createdAt > accountViewModel.account.loadLastRead(routeForLastRead)
accountViewModel.account.markAsRead(routeForLastRead, zapSetCard.createdAt)
val newBackgroundColor = if (isNew) { val newBackgroundColor = if (isNew) {
newItemColor.compositeOver(defaultBackgroundColor) newItemColor.compositeOver(defaultBackgroundColor)
} else { } else {
@@ -18,7 +18,6 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import com.vitorpamplona.amethyst.ui.note.ChatroomHeaderCompose import com.vitorpamplona.amethyst.ui.note.ChatroomHeaderCompose
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
import com.vitorpamplona.quartz.events.ChatroomKeyable
import kotlin.time.ExperimentalTime import kotlin.time.ExperimentalTime
import kotlin.time.measureTimedValue import kotlin.time.measureTimedValue
@@ -83,24 +82,9 @@ private fun FeedLoaded(
LaunchedEffect(key1 = markAsRead.value) { LaunchedEffect(key1 = markAsRead.value) {
if (markAsRead.value) { if (markAsRead.value) {
for (note in state.feed.value) { accountViewModel.markAllAsRead(state.feed.value) {
note.event?.let { noteEvent -> markAsRead.value = false
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())
}
}
} }
markAsRead.value = false
} }
} }
@@ -1,8 +1,10 @@
package com.vitorpamplona.amethyst.ui.screen.loggedIn package com.vitorpamplona.amethyst.ui.screen.loggedIn
import android.content.Context import android.content.Context
import android.util.Log
import androidx.compose.runtime.Immutable import androidx.compose.runtime.Immutable
import androidx.compose.runtime.Stable import androidx.compose.runtime.Stable
import androidx.compose.runtime.mutableStateOf
import androidx.lifecycle.LiveData import androidx.lifecycle.LiveData
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider 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.actions.Dao
import com.vitorpamplona.amethyst.ui.components.MarkdownParser import com.vitorpamplona.amethyst.ui.components.MarkdownParser
import com.vitorpamplona.amethyst.ui.components.UrlPreviewState 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.ZapAmountCommentNotification
import com.vitorpamplona.amethyst.ui.note.ZapraiserStatus import com.vitorpamplona.amethyst.ui.note.ZapraiserStatus
import com.vitorpamplona.amethyst.ui.note.showAmount 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.HexKey
import com.vitorpamplona.quartz.encoders.Nip19 import com.vitorpamplona.quartz.encoders.Nip19
import com.vitorpamplona.quartz.events.ChatroomKey import com.vitorpamplona.quartz.events.ChatroomKey
import com.vitorpamplona.quartz.events.ChatroomKeyable
import com.vitorpamplona.quartz.events.Event import com.vitorpamplona.quartz.events.Event
import com.vitorpamplona.quartz.events.GiftWrapEvent import com.vitorpamplona.quartz.events.GiftWrapEvent
import com.vitorpamplona.quartz.events.ImmutableListOfLists import com.vitorpamplona.quartz.events.ImmutableListOfLists
@@ -54,15 +59,18 @@ import kotlinx.collections.immutable.persistentSetOf
import kotlinx.collections.immutable.toImmutableList import kotlinx.collections.immutable.toImmutableList
import kotlinx.collections.immutable.toImmutableSet import kotlinx.collections.immutable.toImmutableSet
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import java.math.BigDecimal import java.math.BigDecimal
import java.util.Locale import java.util.Locale
import kotlin.time.measureTimedValue
@Stable @Stable
class AccountViewModel(val account: Account) : ViewModel(), Dao { class AccountViewModel(val account: Account) : ViewModel(), Dao {
val accountLiveData: LiveData<AccountState> = account.live.map { it } val accountLiveData: LiveData<AccountState> = account.live.map { it }
val accountLanguagesLiveData: LiveData<AccountState> = account.liveLanguages.map { it } val accountLanguagesLiveData: LiveData<AccountState> = account.liveLanguages.map { it }
val accountLastReadLiveData: LiveData<AccountState> = account.liveLastRead.map { it } val accountMarkAsReadUpdates = mutableStateOf(0)
val userFollows: LiveData<UserState> = account.userProfile().live().follows.map { it } val userFollows: LiveData<UserState> = account.userProfile().live().follows.map { it }
val userRelays: LiveData<UserState> = account.userProfile().live().relays.map { it } val userRelays: LiveData<UserState> = 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) { viewModelScope.launch(Dispatchers.IO) {
val lastTime = account.loadLastRead(routeForLastRead) val lastTime = account.loadLastRead(routeForLastRead)
if (baseNoteCreatedAt != null) { if (createdAt != null) {
account.markAsRead(routeForLastRead, baseNoteCreatedAt) if (account.markAsRead(routeForLastRead, createdAt)) {
onIsNew(baseNoteCreatedAt > lastTime) refreshMarkAsReadObservers()
}
onIsNew(createdAt > lastTime)
} else { } else {
onIsNew(false) onIsNew(false)
} }
} }
} }
fun markAllAsRead(notes: ImmutableList<Note>, 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) { fun createChatRoomFor(user: User, then: (Int) -> Unit) {
viewModelScope.launch(Dispatchers.IO) { viewModelScope.launch(Dispatchers.IO) {
val withKey = ChatroomKey(persistentSetOf(user.pubkeyHex)) val withKey = ChatroomKey(persistentSetOf(user.pubkeyHex))
@@ -753,6 +801,45 @@ class AccountViewModel(val account: Account) : ViewModel(), Dao {
return AccountViewModel(account) as AccountViewModel return AccountViewModel(account) as AccountViewModel
} }
} }
private var collectorJob: Job? = null
val notificationDots = HasNotificationDot(bottomNavigationItems, account)
fun updateNotificationDots(newNotes: Set<Note> = 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<Route>, val account: Account) {
val hasNewItems = bottomNavigationItems.associateWith { MutableStateFlow(false) }
fun update(newNotes: Set<Note>) {
hasNewItems.forEach {
val newResult = it.key.hasNewItems(account, newNotes)
if (newResult != it.value.value) {
it.value.value = newResult
}
}
}
} }
@Immutable @Immutable