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.
|
// 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
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+92
-5
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user