Moves the last seen time saved per route to an account property in order to speed up loading and facilitate multithreading in the app.

pull/459/head
Vitor Pamplona 2023-06-17 17:28:14 -04:00
rodzic f0fe2dfc17
commit ce5106684f
14 zmienionych plików z 115 dodań i 120 usunięć

Wyświetl plik

@ -64,6 +64,7 @@ private object PrefKeys {
const val SHOW_SENSITIVE_CONTENT = "show_sensitive_content"
const val WARN_ABOUT_REPORTS = "warn_about_reports"
const val FILTER_SPAM_FROM_STRANGERS = "filter_spam_from_strangers"
const val LAST_READ_PER_ROUTE = "last_read_route_per_route"
val LAST_READ: (String) -> String = { route -> "last_read_route_$route" }
}
@ -223,6 +224,7 @@ object LocalPreferences {
putInt(PrefKeys.PROXY_PORT, account.proxyPort)
putBoolean(PrefKeys.WARN_ABOUT_REPORTS, account.warnAboutPostsWithReports)
putBoolean(PrefKeys.FILTER_SPAM_FROM_STRANGERS, account.filterSpamFromStrangers)
putString(PrefKeys.LAST_READ_PER_ROUTE, gson.toJson(account.lastReadPerRoute))
if (account.showSensitiveContent == null) {
remove(PrefKeys.SHOW_SENSITIVE_CONTENT)
@ -320,6 +322,18 @@ object LocalPreferences {
val filterSpam = getBoolean(PrefKeys.FILTER_SPAM_FROM_STRANGERS, true)
val warnAboutReports = getBoolean(PrefKeys.WARN_ABOUT_REPORTS, true)
val lastReadPerRoute = try {
getString(PrefKeys.LAST_READ_PER_ROUTE, null)?.let {
gson.fromJson(
it,
object : TypeToken<Map<String, Long>>() {}.type
) as Map<String, Long>
} ?: mapOf()
} catch (e: Throwable) {
e.printStackTrace()
mapOf()
}
val a = Account(
loggedIn = Persona(privKey = privKey?.hexToByteArray(), pubKey = pubKey.hexToByteArray()),
followingChannels = followingChannels,
@ -343,25 +357,14 @@ object LocalPreferences {
proxyPort = proxyPort,
showSensitiveContent = showSensitiveContent,
warnAboutPostsWithReports = warnAboutReports,
filterSpamFromStrangers = filterSpam
filterSpamFromStrangers = filterSpam,
lastReadPerRoute = lastReadPerRoute
)
return a
}
}
fun saveLastRead(route: String, timestampInSecs: Long) {
encryptedPreferences(currentAccount()).edit().apply {
putLong(PrefKeys.LAST_READ(route), timestampInSecs)
}.apply()
}
fun loadLastRead(route: String): Long {
encryptedPreferences(currentAccount()).run {
return getLong(PrefKeys.LAST_READ(route), 0)
}
}
fun migrateSingleUserPrefs() {
if (currentAccount() != null) return

Wyświetl plik

@ -1,53 +0,0 @@
package com.vitorpamplona.amethyst
import androidx.lifecycle.LiveData
import com.vitorpamplona.amethyst.ui.components.BundledUpdate
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.launch
object NotificationCache {
// TODO: This must be account-based
val lastReadByRoute = mutableMapOf<String, Long>()
fun markAsRead(route: String, timestampInSecs: Long) {
val lastTime = lastReadByRoute[route]
if (lastTime == null || timestampInSecs > lastTime) {
lastReadByRoute.put(route, timestampInSecs)
val scope = CoroutineScope(Job() + Dispatchers.IO)
scope.launch {
LocalPreferences.saveLastRead(route, timestampInSecs)
live.invalidateData()
}
}
}
fun load(route: String): Long {
var lastTime = lastReadByRoute[route]
if (lastTime == null) {
lastTime = LocalPreferences.loadLastRead(route)
lastReadByRoute[route] = lastTime
}
return lastTime
}
// Observers line up here.
val live: NotificationLiveData = NotificationLiveData(this)
}
class NotificationLiveData(val cache: NotificationCache) : LiveData<NotificationState>(NotificationState(cache)) {
// Refreshes observers in batches.
private val bundler = BundledUpdate(300, Dispatchers.IO)
fun invalidateData() {
bundler.invalidate() {
if (hasActiveObservers()) {
postValue(NotificationState(cache))
}
}
}
}
class NotificationState(val cache: NotificationCache)

Wyświetl plik

@ -70,13 +70,15 @@ class Account(
var proxyPort: Int,
var showSensitiveContent: Boolean? = null,
var warnAboutPostsWithReports: Boolean = true,
var filterSpamFromStrangers: Boolean = true
var filterSpamFromStrangers: Boolean = true,
var lastReadPerRoute: Map<String, Long> = mapOf<String, Long>()
) {
var transientHiddenUsers: Set<String> = setOf()
// Observers line up here.
val live: AccountLiveData = AccountLiveData(this)
val liveLanguages: AccountLiveData = AccountLiveData(this)
val liveLastRead: AccountLiveData = AccountLiveData(this)
val saveable: AccountLiveData = AccountLiveData(this)
var userProfileCache: User? = null
@ -1154,6 +1156,19 @@ class Account(
live.invalidateData()
}
fun markAsRead(route: String, timestampInSecs: Long) {
val lastTime = lastReadPerRoute[route]
if (lastTime == null || timestampInSecs > lastTime) {
lastReadPerRoute = lastReadPerRoute + Pair(route, timestampInSecs)
saveable.invalidateData()
liveLastRead.invalidateData()
}
}
fun loadLastRead(route: String): Long {
return lastReadPerRoute[route] ?: 0
}
fun registerObservers() {
// Observes relays to restart connections
userProfile().live().relays.observeForever {

Wyświetl plik

@ -39,7 +39,6 @@ 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.NotificationCache
import com.vitorpamplona.amethyst.model.LocalCache
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
import kotlinx.coroutines.Dispatchers
@ -165,12 +164,11 @@ fun WatchPossibleNotificationChanges(
val accountState by accountViewModel.accountLiveData.observeAsState()
val account = remember(accountState) { accountState?.account } ?: return
val notifState by NotificationCache.live.observeAsState()
val notif = remember(notifState) { notifState?.cache } ?: return
val notifState by accountViewModel.accountLastReadLiveData.observeAsState()
LaunchedEffect(key1 = notifState, key2 = accountState) {
launch(Dispatchers.IO) {
onChange(route.hasNewItems(account, notif, emptySet()))
onChange(route.hasNewItems(account, emptySet()))
}
}
@ -178,7 +176,7 @@ fun WatchPossibleNotificationChanges(
launch(Dispatchers.IO) {
LocalCache.live.newEventBundles.collect {
launch(Dispatchers.IO) {
onChange(route.hasNewItems(account, notif, it))
onChange(route.hasNewItems(account, it))
}
}
}

Wyświetl plik

@ -10,7 +10,6 @@ import androidx.navigation.NavHostController
import androidx.navigation.NavType
import androidx.navigation.compose.currentBackStackEntryAsState
import androidx.navigation.navArgument
import com.vitorpamplona.amethyst.NotificationCache
import com.vitorpamplona.amethyst.R
import com.vitorpamplona.amethyst.model.Account
import com.vitorpamplona.amethyst.model.Note
@ -27,7 +26,7 @@ import kotlinx.collections.immutable.toImmutableList
sealed class Route(
val route: String,
val icon: Int,
val hasNewItems: (Account, NotificationCache, Set<com.vitorpamplona.amethyst.model.Note>) -> Boolean = { _, _, _ -> false },
val hasNewItems: (Account, Set<com.vitorpamplona.amethyst.model.Note>) -> Boolean = { _, _ -> false },
val arguments: ImmutableList<NamedNavArgument> = persistentListOf()
) {
val base: String
@ -40,7 +39,7 @@ sealed class Route(
navArgument("scrollToTop") { type = NavType.BoolType; defaultValue = false },
navArgument("nip47") { type = NavType.StringType; nullable = true; defaultValue = null }
).toImmutableList(),
hasNewItems = { accountViewModel, cache, newNotes -> HomeLatestItem.hasNewItems(accountViewModel, cache, newNotes) }
hasNewItems = { accountViewModel, newNotes -> HomeLatestItem.hasNewItems(accountViewModel, newNotes) }
)
object Search : Route(
@ -59,13 +58,13 @@ sealed class Route(
route = "Notification?scrollToTop={scrollToTop}",
icon = R.drawable.ic_notifications,
arguments = listOf(navArgument("scrollToTop") { type = NavType.BoolType; defaultValue = false }).toImmutableList(),
hasNewItems = { accountViewModel, cache, newNotes -> NotificationLatestItem.hasNewItems(accountViewModel, cache, newNotes) }
hasNewItems = { accountViewModel, newNotes -> NotificationLatestItem.hasNewItems(accountViewModel, newNotes) }
)
object Message : Route(
route = "Message",
icon = R.drawable.ic_dm,
hasNewItems = { accountViewModel, cache, newNotes -> MessagesLatestItem.hasNewItems(accountViewModel, cache, newNotes) }
hasNewItems = { accountViewModel, newNotes -> MessagesLatestItem.hasNewItems(accountViewModel, newNotes) }
)
object BlockedUsers : Route(
@ -127,29 +126,40 @@ fun currentRoute(navController: NavHostController): String? {
open class LatestItem {
var newestItemPerAccount: Map<String, Note?> = mapOf()
fun getNewestItem(account: Account): Note? {
return newestItemPerAccount[account.userProfile().pubkeyHex]
}
fun clearNewestItem(account: Account) {
val userHex = account.userProfile().pubkeyHex
if (newestItemPerAccount.contains(userHex)) {
newestItemPerAccount = newestItemPerAccount - userHex
}
}
fun updateNewestItem(newNotes: Set<Note>, account: Account, filter: AdditiveFeedFilter<Note>): Note? {
val newestItem = newestItemPerAccount[account.userProfile().pubkeyHex]
if (newestItem == null) {
newestItemPerAccount = newestItemPerAccount + Pair(
account.userProfile().pubkeyHex,
filterMore(filter.feed()).firstOrNull { it.createdAt() != null }
filterMore(filter.feed(), account).firstOrNull { it.createdAt() != null }
)
} else {
newestItemPerAccount = newestItemPerAccount + Pair(
account.userProfile().pubkeyHex,
filter.sort(filterMore(filter.applyFilter(newNotes)) + newestItem).first()
filter.sort(filterMore(filter.applyFilter(newNotes), account) + newestItem).first()
)
}
return newestItemPerAccount[account.userProfile().pubkeyHex]
}
open fun filterMore(newItems: Set<Note>): Set<Note> {
open fun filterMore(newItems: Set<Note>, account: Account): Set<Note> {
return newItems
}
open fun filterMore(newItems: List<Note>): List<Note> {
open fun filterMore(newItems: List<Note>, account: Account): List<Note> {
return newItems
}
}
@ -157,10 +167,9 @@ open class LatestItem {
object HomeLatestItem : LatestItem() {
fun hasNewItems(
account: Account,
cache: NotificationCache,
newNotes: Set<Note>
): Boolean {
val lastTime = cache.load("HomeFollows")
val lastTime = account.loadLastRead("HomeFollows")
val newestItem = updateNewestItem(newNotes, account, HomeNewThreadFeedFilter(account))
@ -171,10 +180,9 @@ object HomeLatestItem : LatestItem() {
object NotificationLatestItem : LatestItem() {
fun hasNewItems(
account: Account,
cache: NotificationCache,
newNotes: Set<Note>
): Boolean {
val lastTime = cache.load("Notification")
val lastTime = account.loadLastRead("Notification")
val newestItem = updateNewestItem(newNotes, account, NotificationFeedFilter(account))
@ -185,24 +193,53 @@ object NotificationLatestItem : LatestItem() {
object MessagesLatestItem : LatestItem() {
fun hasNewItems(
account: Account,
cache: NotificationCache,
newNotes: Set<Note>
): Boolean {
println("AAA Hey")
// Checks if the current newest item is still unread.
// If so, there is no need to check anything else
if (isNew(getNewestItem(account), account)) {
println("AAA Enter ${getNewestItem(account)?.author?.toBestDisplayName()}")
return true
}
clearNewestItem(account)
println("AAA Hey 2")
// gets the newest of the unread items
val newestItem = updateNewestItem(newNotes, account, ChatroomListKnownFeedFilter(account))
val roomUserHex = (newestItem?.event as? PrivateDmEvent)?.talkingWith(account.userProfile().pubkeyHex)
println("AAA ${newestItem?.author?.toBestDisplayName()} ${isNew(newestItem, account)}")
val lastTime = cache.load("Room/$roomUserHex")
return (newestItem?.createdAt() ?: 0) > lastTime
return isNew(newestItem, account)
}
override fun filterMore(newItems: Set<Note>): Set<Note> {
return newItems.filter { it.event is PrivateDmEvent }.toSet()
fun isNew(it: Note?, account: Account): Boolean {
if (it == null) return false
val currentUser = account.userProfile().pubkeyHex
val room = (it.event as? PrivateDmEvent)?.talkingWith(currentUser)
return if (room != null) {
val lastRead = account.loadLastRead("Room/$room")
(it.createdAt() ?: 0) > lastRead
} else {
false
}
}
override fun filterMore(newItems: List<Note>): List<Note> {
return newItems.filter { it.event is PrivateDmEvent }
override fun filterMore(newItems: Set<Note>, account: Account): Set<Note> {
return newItems.filter {
isNew(it, account)
}.toSet()
}
override fun filterMore(newItems: List<Note>, account: Account): List<Note> {
return newItems.filter {
isNew(it, account)
}
}
}

Wyświetl plik

@ -33,7 +33,6 @@ import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import com.vitorpamplona.amethyst.NotificationCache
import com.vitorpamplona.amethyst.R
import com.vitorpamplona.amethyst.ui.screen.BadgeCard
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
@ -62,9 +61,9 @@ fun BadgeCompose(likeSetCard: BadgeCard, isInnerNote: Boolean = false, routeForL
LaunchedEffect(key1 = likeSetCard) {
scope.launch(Dispatchers.IO) {
val isNew = likeSetCard.createdAt() > NotificationCache.load(routeForLastRead)
val isNew = likeSetCard.createdAt() > accountViewModel.account.loadLastRead(routeForLastRead)
NotificationCache.markAsRead(routeForLastRead, likeSetCard.createdAt())
accountViewModel.account.markAsRead(routeForLastRead, likeSetCard.createdAt())
val newBackgroundColor = if (isNew) {
newItemColor.compositeOver(defaultBackgroundColor)

Wyświetl plik

@ -39,7 +39,6 @@ import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.text.withStyle
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import com.vitorpamplona.amethyst.NotificationCache
import com.vitorpamplona.amethyst.R
import com.vitorpamplona.amethyst.model.Channel
import com.vitorpamplona.amethyst.model.LocalCache
@ -74,7 +73,7 @@ fun ChatroomCompose(
BlankNote(Modifier)
} else if (channelHex != null) {
LoadChannel(baseChannelHex = channelHex!!) { channel ->
ChannelRoomCompose(note, channel, nav)
ChannelRoomCompose(note, channel, accountViewModel, nav)
}
} else {
val userRoomHex = remember(noteState, accountViewModel) {
@ -91,6 +90,7 @@ fun ChatroomCompose(
private fun ChannelRoomCompose(
note: Note,
channel: Channel,
accountViewModel: AccountViewModel,
nav: (String) -> Unit
) {
val authorState by note.author!!.live().metadata.observeAsState()
@ -128,7 +128,7 @@ private fun ChannelRoomCompose(
var hasNewMessages by remember { mutableStateOf<Boolean>(false) }
WatchNotificationChanges(note, route) { newHasNewMessages ->
WatchNotificationChanges(note, route, accountViewModel) { newHasNewMessages ->
if (hasNewMessages != newHasNewMessages) {
hasNewMessages = newHasNewMessages
}
@ -182,7 +182,7 @@ private fun UserRoomCompose(
"Room/${user.pubkeyHex}"
}
WatchNotificationChanges(note, route) { newHasNewMessages ->
WatchNotificationChanges(note, route, accountViewModel) { newHasNewMessages ->
if (hasNewMessages != newHasNewMessages) {
hasNewMessages = newHasNewMessages
}
@ -208,14 +208,15 @@ private fun UserRoomCompose(
private fun WatchNotificationChanges(
note: Note,
route: String,
accountViewModel: AccountViewModel,
onNewStatus: (Boolean) -> Unit
) {
val cacheState by NotificationCache.live.observeAsState()
val cacheState by accountViewModel.accountLastReadLiveData.observeAsState()
LaunchedEffect(key1 = note, cacheState) {
launch(Dispatchers.IO) {
note.event?.createdAt()?.let {
val lastTime = NotificationCache.load(route)
val lastTime = accountViewModel.account.loadLastRead(route)
onNewStatus(it > lastTime)
}
}

Wyświetl plik

@ -52,7 +52,6 @@ import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.IntSize
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import com.vitorpamplona.amethyst.NotificationCache
import com.vitorpamplona.amethyst.R
import com.vitorpamplona.amethyst.model.Note
import com.vitorpamplona.amethyst.service.model.ChannelCreateEvent
@ -184,11 +183,11 @@ fun ChatroomMessageCompose(
LaunchedEffect(key1 = routeForLastRead) {
routeForLastRead?.let {
scope.launch(Dispatchers.IO) {
val lastTime = NotificationCache.load(it)
val lastTime = accountViewModel.account.loadLastRead(it)
val createdAt = note.createdAt()
if (createdAt != null) {
NotificationCache.markAsRead(it, createdAt)
accountViewModel.account.markAsRead(it, createdAt)
}
}
}

Wyświetl plik

@ -27,7 +27,6 @@ import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.compositeOver
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.unit.dp
import com.vitorpamplona.amethyst.NotificationCache
import com.vitorpamplona.amethyst.R
import com.vitorpamplona.amethyst.ui.screen.MessageSetCard
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
@ -53,9 +52,9 @@ fun MessageSetCompose(messageSetCard: MessageSetCard, routeForLastRead: String,
LaunchedEffect(key1 = messageSetCard) {
scope.launch(Dispatchers.IO) {
val isNew = messageSetCard.createdAt() > NotificationCache.load(routeForLastRead)
val isNew = messageSetCard.createdAt() > accountViewModel.account.loadLastRead(routeForLastRead)
NotificationCache.markAsRead(routeForLastRead, messageSetCard.createdAt())
accountViewModel.account.markAsRead(routeForLastRead, messageSetCard.createdAt())
val newBackgroundColor = if (isNew) {
newItemColor.compositeOver(defaultBackgroundColor)

Wyświetl plik

@ -43,7 +43,6 @@ import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import com.vitorpamplona.amethyst.NotificationCache
import com.vitorpamplona.amethyst.R
import com.vitorpamplona.amethyst.model.LocalCache
import com.vitorpamplona.amethyst.model.Note
@ -79,9 +78,9 @@ fun MultiSetCompose(multiSetCard: MultiSetCard, routeForLastRead: String, accoun
LaunchedEffect(key1 = multiSetCard) {
launch(Dispatchers.IO) {
val isNew = multiSetCard.maxCreatedAt > NotificationCache.load(routeForLastRead)
val isNew = multiSetCard.maxCreatedAt > accountViewModel.account.loadLastRead(routeForLastRead)
NotificationCache.markAsRead(routeForLastRead, multiSetCard.maxCreatedAt)
accountViewModel.account.markAsRead(routeForLastRead, multiSetCard.maxCreatedAt)
val newBackgroundColor = if (isNew) {
newItemColor.compositeOver(defaultBackgroundColor)

Wyświetl plik

@ -82,7 +82,6 @@ import androidx.core.graphics.get
import coil.compose.AsyncImage
import coil.compose.AsyncImagePainter
import coil.request.SuccessResult
import com.vitorpamplona.amethyst.NotificationCache
import com.vitorpamplona.amethyst.R
import com.vitorpamplona.amethyst.model.Channel
import com.vitorpamplona.amethyst.model.LocalCache
@ -424,11 +423,11 @@ private fun CheckNewAndRenderNote(
LaunchedEffect(key1 = routeForLastRead, key2 = parentBackgroundColor?.value) {
launch(Dispatchers.IO) {
routeForLastRead?.let {
val lastTime = NotificationCache.load(it)
val lastTime = accountViewModel.account.loadLastRead(it)
val createdAt = baseNote.createdAt()
if (createdAt != null) {
NotificationCache.markAsRead(it, createdAt)
accountViewModel.account.markAsRead(it, createdAt)
val isNew = createdAt > lastTime

Wyświetl plik

@ -26,7 +26,6 @@ import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.compositeOver
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import com.vitorpamplona.amethyst.NotificationCache
import com.vitorpamplona.amethyst.R
import com.vitorpamplona.amethyst.ui.screen.ZapUserSetCard
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
@ -43,9 +42,9 @@ fun ZapUserSetCompose(zapSetCard: ZapUserSetCard, isInnerNote: Boolean = false,
LaunchedEffect(key1 = zapSetCard.createdAt()) {
launch(Dispatchers.IO) {
val isNew = zapSetCard.createdAt > NotificationCache.load(routeForLastRead)
val isNew = zapSetCard.createdAt > accountViewModel.account.loadLastRead(routeForLastRead)
NotificationCache.markAsRead(routeForLastRead, zapSetCard.createdAt)
accountViewModel.account.markAsRead(routeForLastRead, zapSetCard.createdAt)
val newBackgroundColor = if (isNew) {
newItemColor.compositeOver(defaultBackgroundColor)

Wyświetl plik

@ -26,7 +26,6 @@ import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.vitorpamplona.amethyst.NotificationCache
import com.vitorpamplona.amethyst.service.model.PrivateDmEvent
import com.vitorpamplona.amethyst.ui.note.ChatroomCompose
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
@ -103,7 +102,7 @@ private fun FeedLoaded(
"Room/$roomUser"
}
NotificationCache.markAsRead(route, it.createdAt())
accountViewModel.account.markAsRead(route, it.createdAt())
}
}
markAsRead.value = false

Wyświetl plik

@ -30,6 +30,7 @@ import java.util.Locale
class AccountViewModel(val account: Account) : ViewModel() {
val accountLiveData: LiveData<AccountState> = account.live.map { it }
val accountLanguagesLiveData: LiveData<AccountState> = account.liveLanguages.map { it }
val accountLastReadLiveData: LiveData<AccountState> = account.liveLastRead.map { it }
fun isWriteable(): Boolean {
return account.isWriteable()