Increases the performance of the Bottom bar's Notification dot calculations
This commit is contained in:
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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<Keyboard> {
|
||||
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<NavBackStackEntry?>,
|
||||
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<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(
|
||||
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<Boolean>?
|
||||
) {
|
||||
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 {
|
||||
Modifier
|
||||
.width(10.dp)
|
||||
.height(10.dp)
|
||||
.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)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
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
|
||||
.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.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 {
|
||||
|
||||
@@ -277,37 +277,24 @@ private fun CheckNewAndRenderChannelCard(
|
||||
) {
|
||||
val newItemColor = MaterialTheme.colorScheme.newItemBackgroundColor
|
||||
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) {
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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()) { }
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -837,7 +837,7 @@ private fun CheckNewAndRenderNote(
|
||||
) {
|
||||
val newItemColor = MaterialTheme.colorScheme.newItemBackgroundColor
|
||||
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) {
|
||||
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.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 {
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+92
-5
@@ -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<AccountState> = account.live.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 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) {
|
||||
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<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) {
|
||||
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<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
|
||||
|
||||
Reference in New Issue
Block a user