Moves lastread routes to mutableStateFlow

This commit is contained in:
Vitor Pamplona
2024-08-16 16:42:00 -04:00
parent 6c81fbbcf9
commit 239f973e89
6 changed files with 51 additions and 66 deletions

View File

@ -349,9 +349,15 @@ object LocalPreferences {
putInt(PrefKeys.PROXY_PORT, account.proxyPort) putInt(PrefKeys.PROXY_PORT, account.proxyPort)
putBoolean(PrefKeys.WARN_ABOUT_REPORTS, account.warnAboutPostsWithReports) putBoolean(PrefKeys.WARN_ABOUT_REPORTS, account.warnAboutPostsWithReports)
putBoolean(PrefKeys.FILTER_SPAM_FROM_STRANGERS, account.filterSpamFromStrangers) putBoolean(PrefKeys.FILTER_SPAM_FROM_STRANGERS, account.filterSpamFromStrangers)
val regularMap =
account.lastReadPerRoute.value.mapValues {
it.value.value
}
putString( putString(
PrefKeys.LAST_READ_PER_ROUTE, PrefKeys.LAST_READ_PER_ROUTE,
Event.mapper.writeValueAsString(account.lastReadPerRoute), Event.mapper.writeValueAsString(regularMap),
) )
putStringSet(PrefKeys.HAS_DONATED_IN_VERSION, account.hasDonatedInVersion) putStringSet(PrefKeys.HAS_DONATED_IN_VERSION, account.hasDonatedInVersion)
@ -611,9 +617,10 @@ object LocalPreferences {
val lastReadPerRoute = val lastReadPerRoute =
try { try {
getString(PrefKeys.LAST_READ_PER_ROUTE, null)?.let { getString(PrefKeys.LAST_READ_PER_ROUTE, null)?.let {
Event.mapper.readValue<Map<String, Long>?>(it) Event.mapper.readValue<Map<String, Long>?>(it)?.mapValues {
} MutableStateFlow(it.value)
?: mapOf() }
} ?: mapOf()
} catch (e: Throwable) { } catch (e: Throwable) {
if (e is CancellationException) throw e if (e is CancellationException) throw e
Log.w( Log.w(
@ -669,7 +676,7 @@ object LocalPreferences {
showSensitiveContent = MutableStateFlow(showSensitiveContent), showSensitiveContent = MutableStateFlow(showSensitiveContent),
warnAboutPostsWithReports = warnAboutReports, warnAboutPostsWithReports = warnAboutReports,
filterSpamFromStrangers = filterSpam, filterSpamFromStrangers = filterSpam,
lastReadPerRoute = lastReadPerRoute, lastReadPerRoute = MutableStateFlow(lastReadPerRoute),
hasDonatedInVersion = hasDonatedInVersion, hasDonatedInVersion = hasDonatedInVersion,
pendingAttestations = MutableStateFlow(pendingAttestations ?: emptyMap()), pendingAttestations = MutableStateFlow(pendingAttestations ?: emptyMap()),
) )

View File

@ -202,7 +202,7 @@ class Account(
var showSensitiveContent: MutableStateFlow<Boolean?> = MutableStateFlow(null), var showSensitiveContent: MutableStateFlow<Boolean?> = MutableStateFlow(null),
var warnAboutPostsWithReports: Boolean = true, var warnAboutPostsWithReports: Boolean = true,
var filterSpamFromStrangers: Boolean = true, var filterSpamFromStrangers: Boolean = true,
var lastReadPerRoute: Map<String, Long> = mapOf<String, Long>(), var lastReadPerRoute: MutableStateFlow<Map<String, MutableStateFlow<Long>>> = MutableStateFlow(mapOf()),
var hasDonatedInVersion: Set<String> = setOf<String>(), var hasDonatedInVersion: Set<String> = setOf<String>(),
var pendingAttestations: MutableStateFlow<Map<HexKey, String>> = MutableStateFlow<Map<HexKey, String>>(mapOf()), var pendingAttestations: MutableStateFlow<Map<HexKey, String>> = MutableStateFlow<Map<HexKey, String>>(mapOf()),
val scope: CoroutineScope = Amethyst.instance.applicationIOScope, val scope: CoroutineScope = Amethyst.instance.applicationIOScope,
@ -3213,9 +3213,15 @@ class Account(
route: String, route: String,
timestampInSecs: Long, timestampInSecs: Long,
): Boolean { ): Boolean {
val lastTime = lastReadPerRoute[route] val lastTime = lastReadPerRoute.value[route]
return if (lastTime == null || timestampInSecs > lastTime) { return if (lastTime == null) {
lastReadPerRoute = lastReadPerRoute + Pair(route, timestampInSecs) lastReadPerRoute.update {
it + Pair(route, MutableStateFlow(timestampInSecs))
}
saveable.invalidateData()
true
} else if (timestampInSecs > lastTime.value) {
lastTime.tryEmit(timestampInSecs)
saveable.invalidateData() saveable.invalidateData()
true true
} else { } else {
@ -3223,7 +3229,16 @@ class Account(
} }
} }
fun loadLastRead(route: String): Long = lastReadPerRoute[route] ?: 0 fun loadLastRead(route: String): Long = lastReadPerRoute.value[route]?.value ?: 0
fun loadLastReadFlow(route: String): StateFlow<Long> =
lastReadPerRoute.value[route] ?: run {
val newFlow = MutableStateFlow<Long>(0)
lastReadPerRoute.update {
it + Pair(route, newFlow)
}
newFlow
}
fun hasDonatedInThisVersion(): Boolean = hasDonatedInVersion.contains(BuildConfig.VERSION_NAME) fun hasDonatedInThisVersion(): Boolean = hasDonatedInVersion.contains(BuildConfig.VERSION_NAME)

View File

@ -50,7 +50,6 @@ import androidx.compose.runtime.LaunchedEffect
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.livedata.observeAsState
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
@ -387,8 +386,6 @@ private fun RenderChannel(
loadRobohash: Boolean, loadRobohash: Boolean,
onClick: () -> Unit, onClick: () -> Unit,
) { ) {
val hasNewMessages = remember { mutableStateOf(false) }
ChannelName( ChannelName(
channelIdHex = item.idHex, channelIdHex = item.idHex,
channelPicture = item.profilePicture(), channelPicture = item.profilePicture(),
@ -400,7 +397,7 @@ private fun RenderChannel(
}, },
channelLastTime = null, channelLastTime = null,
channelLastContent = item.summary(), channelLastContent = item.summary(),
hasNewMessages, hasNewMessages = false,
onClick = onClick, onClick = onClick,
loadProfilePicture = loadProfilePicture, loadProfilePicture = loadProfilePicture,
loadRobohash = loadRobohash, loadRobohash = loadRobohash,

View File

@ -33,7 +33,6 @@ import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.MutableState
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.livedata.observeAsState
@ -54,6 +53,7 @@ import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.text.withStyle import androidx.compose.ui.text.withStyle
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.lifecycle.compose.collectAsStateWithLifecycle
import androidx.lifecycle.distinctUntilChanged import androidx.lifecycle.distinctUntilChanged
import androidx.lifecycle.map import androidx.lifecycle.map
import com.patrykandpatrick.vico.core.extension.forEachIndexedExtended import com.patrykandpatrick.vico.core.extension.forEachIndexedExtended
@ -171,15 +171,14 @@ private fun ChannelRoomCompose(
.observeAsState() .observeAsState()
val authorName = remember(note, authorState) { authorState?.user?.toBestDisplayName() } val authorName = remember(note, authorState) { authorState?.user?.toBestDisplayName() }
val chanHex = remember { channel.idHex }
val channelState by channel.live.observeAsState() val channelState by channel.live.observeAsState()
val channelPicture by remember(note, channelState) { derivedStateOf { channel.profilePicture() } }
val channelName by remember(note, channelState) { derivedStateOf { channel.toBestDisplayName() } } val channelPicture = channelState?.channel?.profilePicture() ?: channel.profilePicture()
val channelName = channelState?.channel?.toBestDisplayName() ?: channel.toBestDisplayName()
val noteEvent = note.event val noteEvent = note.event
val route = remember(note) { "Channel/$chanHex" } val route = "Channel/${channel.idHex}"
val description = val description =
if (noteEvent is ChannelCreateEvent) { if (noteEvent is ChannelCreateEvent) {
@ -190,21 +189,15 @@ private fun ChannelRoomCompose(
noteEvent?.content()?.take(200) noteEvent?.content()?.take(200)
} }
val hasNewMessages = remember { mutableStateOf<Boolean>(false) } val lastReadTime by accountViewModel.account.loadLastReadFlow(route).collectAsStateWithLifecycle()
WatchNotificationChanges(note, route, accountViewModel) { newHasNewMessages ->
if (hasNewMessages.value != newHasNewMessages) {
hasNewMessages.value = newHasNewMessages
}
}
ChannelName( ChannelName(
channelIdHex = chanHex, channelIdHex = channel.idHex,
channelPicture = channelPicture, channelPicture = channelPicture,
channelTitle = { modifier -> ChannelTitleWithLabelInfo(channelName, modifier) }, channelTitle = { modifier -> ChannelTitleWithLabelInfo(channelName, modifier) },
channelLastTime = remember(note) { note.createdAt() }, channelLastTime = note.createdAt(),
channelLastContent = remember(note, authorState) { "$authorName: $description" }, channelLastContent = "$authorName: $description",
hasNewMessages = hasNewMessages, hasNewMessages = (noteEvent?.createdAt() ?: Long.MIN_VALUE) > lastReadTime,
loadProfilePicture = accountViewModel.settings.showProfilePictures.value, loadProfilePicture = accountViewModel.settings.showProfilePictures.value,
loadRobohash = accountViewModel.settings.featureSet != FeatureSetType.PERFORMANCE, loadRobohash = accountViewModel.settings.featureSet != FeatureSetType.PERFORMANCE,
onClick = { nav(route) }, onClick = { nav(route) },
@ -257,17 +250,9 @@ private fun UserRoomCompose(
accountViewModel: AccountViewModel, accountViewModel: AccountViewModel,
nav: (String) -> Unit, nav: (String) -> Unit,
) { ) {
val hasNewMessages = remember { mutableStateOf<Boolean>(false) } val route = "Room/${room.hashCode()}"
val route = remember(room) { "Room/${room.hashCode()}" } val lastReadTime by accountViewModel.account.loadLastReadFlow(route).collectAsStateWithLifecycle()
val createAt by remember(note) { derivedStateOf { note.createdAt() } }
WatchNotificationChanges(note, route, accountViewModel) { newHasNewMessages ->
if (hasNewMessages.value != newHasNewMessages) {
hasNewMessages.value = newHasNewMessages
}
}
LoadDecryptedContentOrNull(note, accountViewModel) { content -> LoadDecryptedContentOrNull(note, accountViewModel) { content ->
ChannelName( ChannelName(
@ -279,9 +264,9 @@ private fun UserRoomCompose(
) )
}, },
channelTitle = { RoomNameDisplay(room, it, accountViewModel) }, channelTitle = { RoomNameDisplay(room, it, accountViewModel) },
channelLastTime = createAt, channelLastTime = note.createdAt(),
channelLastContent = content, channelLastContent = content,
hasNewMessages = hasNewMessages, hasNewMessages = (note.createdAt() ?: Long.MIN_VALUE) > lastReadTime,
onClick = { nav(route) }, onClick = { nav(route) },
) )
} }
@ -412,20 +397,6 @@ fun ShortUsernameDisplay(
} }
} }
@Composable
private fun WatchNotificationChanges(
note: Note,
route: String,
accountViewModel: AccountViewModel,
onNewStatus: (Boolean) -> Unit,
) {
LaunchedEffect(key1 = note, accountViewModel.accountMarkAsReadUpdates.intValue) {
note.event?.createdAt()?.let {
onNewStatus(it > accountViewModel.account.loadLastRead(route))
}
}
}
@Composable @Composable
fun LoadUser( fun LoadUser(
baseUserHex: String, baseUserHex: String,
@ -455,7 +426,7 @@ fun ChannelName(
channelTitle: @Composable (Modifier) -> Unit, channelTitle: @Composable (Modifier) -> Unit,
channelLastTime: Long?, channelLastTime: Long?,
channelLastContent: String?, channelLastContent: String?,
hasNewMessages: MutableState<Boolean>, hasNewMessages: Boolean,
loadProfilePicture: Boolean, loadProfilePicture: Boolean,
loadRobohash: Boolean, loadRobohash: Boolean,
onClick: () -> Unit, onClick: () -> Unit,
@ -485,7 +456,7 @@ fun ChannelName(
channelTitle: @Composable (Modifier) -> Unit, channelTitle: @Composable (Modifier) -> Unit,
channelLastTime: Long?, channelLastTime: Long?,
channelLastContent: String?, channelLastContent: String?,
hasNewMessages: MutableState<Boolean>, hasNewMessages: Boolean,
onClick: () -> Unit, onClick: () -> Unit,
) { ) {
ChatHeaderLayout( ChatHeaderLayout(
@ -514,7 +485,7 @@ fun ChannelName(
) )
} }
if (hasNewMessages.value) { if (hasNewMessages) {
NewItemsBubble() NewItemsBubble()
} }
}, },

View File

@ -27,7 +27,6 @@ import android.util.LruCache
import androidx.compose.runtime.Immutable import androidx.compose.runtime.Immutable
import androidx.compose.runtime.MutableState import androidx.compose.runtime.MutableState
import androidx.compose.runtime.Stable import androidx.compose.runtime.Stable
import androidx.compose.runtime.mutableIntStateOf
import androidx.lifecycle.LiveData import androidx.lifecycle.LiveData
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.ViewModelProvider
@ -150,7 +149,6 @@ class AccountViewModel(
Dao { 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 accountMarkAsReadUpdates = mutableIntStateOf(0)
// TODO: contact lists are not notes yet // TODO: contact lists are not notes yet
// val kind3Relays: StateFlow<ContactListEvent?> = observeByAuthor(ContactListEvent.KIND, account.signer.pubKey) // val kind3Relays: StateFlow<ContactListEvent?> = observeByAuthor(ContactListEvent.KIND, account.signer.pubKey)
@ -1121,7 +1119,6 @@ class AccountViewModel(
suspend fun refreshMarkAsReadObservers() { suspend fun refreshMarkAsReadObservers() {
updateNotificationDots() updateNotificationDots()
accountMarkAsReadUpdates.value++
} }
fun loadAndMarkAsRead( fun loadAndMarkAsRead(

View File

@ -380,8 +380,6 @@ private fun DisplaySearchResults(
val channels by searchBarViewModel.searchResultsChannels.collectAsStateWithLifecycle() val channels by searchBarViewModel.searchResultsChannels.collectAsStateWithLifecycle()
val notes by searchBarViewModel.searchResultsNotes.collectAsStateWithLifecycle() val notes by searchBarViewModel.searchResultsNotes.collectAsStateWithLifecycle()
val hasNewMessages = remember { mutableStateOf(false) }
LazyColumn( LazyColumn(
modifier = Modifier.fillMaxHeight(), modifier = Modifier.fillMaxHeight(),
contentPadding = FeedPadding, contentPadding = FeedPadding,
@ -425,7 +423,7 @@ private fun DisplaySearchResults(
}, },
channelLastTime = null, channelLastTime = null,
channelLastContent = item.summary(), channelLastContent = item.summary(),
hasNewMessages = hasNewMessages, hasNewMessages = false,
loadProfilePicture = accountViewModel.settings.showProfilePictures.value, loadProfilePicture = accountViewModel.settings.showProfilePictures.value,
loadRobohash = accountViewModel.settings.featureSet != FeatureSetType.PERFORMANCE, loadRobohash = accountViewModel.settings.featureSet != FeatureSetType.PERFORMANCE,
onClick = { nav("Channel/${item.idHex}") }, onClick = { nav("Channel/${item.idHex}") },