mirror of
https://github.com/vitorpamplona/amethyst.git
synced 2025-03-26 17:52:29 +01:00
Moves has-new-item bottom navigation computations to use the new feed states and last read database in flows.
This commit is contained in:
parent
e17cfb4fdb
commit
4b3d986c5b
@ -194,7 +194,7 @@ fun AddNotifIconIfNeeded(
|
||||
accountViewModel: AccountViewModel,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
val flow = accountViewModel.notificationDots.hasNewItems[route] ?: return
|
||||
val flow = accountViewModel.hasNewItems[route] ?: return
|
||||
val hasNewItems by flow.collectAsStateWithLifecycle()
|
||||
if (hasNewItems) {
|
||||
NotificationDotIcon(modifier)
|
||||
|
@ -32,20 +32,10 @@ import androidx.navigation.NavHostController
|
||||
import androidx.navigation.NavType
|
||||
import androidx.navigation.navArgument
|
||||
import com.vitorpamplona.amethyst.R
|
||||
import com.vitorpamplona.amethyst.model.Account
|
||||
import com.vitorpamplona.amethyst.model.Note
|
||||
import com.vitorpamplona.amethyst.service.checkNotInMainThread
|
||||
import com.vitorpamplona.amethyst.ui.dal.AdditiveFeedFilter
|
||||
import com.vitorpamplona.amethyst.ui.dal.ChatroomListKnownFeedFilter
|
||||
import com.vitorpamplona.amethyst.ui.dal.HomeNewThreadFeedFilter
|
||||
import com.vitorpamplona.amethyst.ui.dal.NotificationFeedFilter
|
||||
import com.vitorpamplona.amethyst.ui.theme.Size20dp
|
||||
import com.vitorpamplona.amethyst.ui.theme.Size23dp
|
||||
import com.vitorpamplona.amethyst.ui.theme.Size24dp
|
||||
import com.vitorpamplona.amethyst.ui.theme.Size25dp
|
||||
import com.vitorpamplona.quartz.events.ChatroomKeyable
|
||||
import com.vitorpamplona.quartz.events.GenericRepostEvent
|
||||
import com.vitorpamplona.quartz.events.RepostEvent
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
@ -58,9 +48,6 @@ sealed class Route(
|
||||
val notifSize: Modifier = Modifier.size(Size23dp),
|
||||
val iconSize: Modifier = Modifier.size(Size20dp),
|
||||
val contentDescriptor: Int = R.string.route,
|
||||
val hasNewItems: (Account, Set<com.vitorpamplona.amethyst.model.Note>) -> Boolean = { _, _ ->
|
||||
false
|
||||
},
|
||||
val arguments: ImmutableList<NamedNavArgument> = persistentListOf(),
|
||||
) {
|
||||
object Home :
|
||||
@ -78,9 +65,6 @@ sealed class Route(
|
||||
},
|
||||
).toImmutableList(),
|
||||
contentDescriptor = R.string.route_home,
|
||||
hasNewItems = { accountViewModel, newNotes ->
|
||||
HomeLatestItem.hasNewItems(accountViewModel, newNotes)
|
||||
},
|
||||
)
|
||||
|
||||
object Global :
|
||||
@ -118,9 +102,6 @@ sealed class Route(
|
||||
Route(
|
||||
route = "Notification",
|
||||
icon = R.drawable.ic_notifications,
|
||||
hasNewItems = { accountViewModel, newNotes ->
|
||||
NotificationLatestItem.hasNewItems(accountViewModel, newNotes)
|
||||
},
|
||||
contentDescriptor = R.string.route_notifications,
|
||||
)
|
||||
|
||||
@ -128,9 +109,6 @@ sealed class Route(
|
||||
Route(
|
||||
route = "Message",
|
||||
icon = R.drawable.ic_dm,
|
||||
hasNewItems = { accountViewModel, newNotes ->
|
||||
MessagesLatestItem.hasNewItems(accountViewModel, newNotes)
|
||||
},
|
||||
contentDescriptor = R.string.route_messages,
|
||||
)
|
||||
|
||||
@ -241,138 +219,6 @@ sealed class Route(
|
||||
)
|
||||
}
|
||||
|
||||
open class LatestItem {
|
||||
var newestItemPerAccount: Map<String, Note?> = mapOf()
|
||||
|
||||
fun getNewestItem(account: Account): Note? = 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]
|
||||
|
||||
// Block list got updated
|
||||
val newNewest =
|
||||
if (newestItem == null || !account.isAcceptable(newestItem)) {
|
||||
filterMore(filter.feed(), account).firstOrNull { it.createdAt() != null && account.isAcceptable(it) }
|
||||
} else {
|
||||
filter
|
||||
.sort(
|
||||
filterMore(filter.applyFilter(newNotes), account) + newestItem,
|
||||
).firstOrNull { it.createdAt() != null && account.isAcceptable(it) }
|
||||
}
|
||||
|
||||
newestItemPerAccount = newestItemPerAccount + Pair(account.userProfile().pubkeyHex, newNewest)
|
||||
|
||||
return newestItemPerAccount[account.userProfile().pubkeyHex]
|
||||
}
|
||||
|
||||
open fun filterMore(
|
||||
newItems: Set<Note>,
|
||||
account: Account,
|
||||
): Set<Note> = newItems
|
||||
|
||||
open fun filterMore(
|
||||
newItems: List<Note>,
|
||||
account: Account,
|
||||
): List<Note> = newItems
|
||||
}
|
||||
|
||||
object HomeLatestItem : LatestItem() {
|
||||
fun hasNewItems(
|
||||
account: Account,
|
||||
newNotes: Set<Note>,
|
||||
): Boolean {
|
||||
checkNotInMainThread()
|
||||
|
||||
val lastTime = account.loadLastRead("HomeFollows")
|
||||
|
||||
val newestItem = updateNewestItem(newNotes, account, HomeNewThreadFeedFilter(account))
|
||||
|
||||
return (newestItem?.createdAt() ?: 0) > lastTime
|
||||
}
|
||||
|
||||
override fun filterMore(
|
||||
newItems: Set<Note>,
|
||||
account: Account,
|
||||
): Set<Note> {
|
||||
// removes reposts from the dot notifications.
|
||||
return newItems.filter { it.event !is GenericRepostEvent && it.event !is RepostEvent }.toSet()
|
||||
}
|
||||
}
|
||||
|
||||
object NotificationLatestItem : LatestItem() {
|
||||
fun hasNewItems(
|
||||
account: Account,
|
||||
newNotes: Set<Note>,
|
||||
): Boolean {
|
||||
checkNotInMainThread()
|
||||
|
||||
val lastTime = account.loadLastRead("Notification")
|
||||
|
||||
val newestItem = updateNewestItem(newNotes, account, NotificationFeedFilter(account))
|
||||
|
||||
return (newestItem?.createdAt() ?: 0) > lastTime
|
||||
}
|
||||
}
|
||||
|
||||
object MessagesLatestItem : LatestItem() {
|
||||
fun hasNewItems(
|
||||
account: Account,
|
||||
newNotes: Set<Note>,
|
||||
): Boolean {
|
||||
checkNotInMainThread()
|
||||
|
||||
// Checks if the current newest item is still unread.
|
||||
// If so, there is no need to check anything else
|
||||
if (isNew(getNewestItem(account), account)) {
|
||||
return true
|
||||
}
|
||||
|
||||
clearNewestItem(account)
|
||||
|
||||
// gets the newest of the unread items
|
||||
val newestItem = updateNewestItem(newNotes, account, ChatroomListKnownFeedFilter(account))
|
||||
|
||||
return isNew(newestItem, account)
|
||||
}
|
||||
|
||||
fun isNew(
|
||||
it: Note?,
|
||||
account: Account,
|
||||
): Boolean {
|
||||
if (it == null) return false
|
||||
|
||||
val currentUser = account.userProfile().pubkeyHex
|
||||
val room = (it.event as? ChatroomKeyable)?.chatroomKey(currentUser)
|
||||
return if (room != null) {
|
||||
val lastRead = account.loadLastRead("Room/${room.hashCode()}")
|
||||
(it.createdAt() ?: 0) > lastRead
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
override fun filterMore(
|
||||
newItems: Set<Note>,
|
||||
account: Account,
|
||||
): Set<Note> = newItems.filter { isNew(it, account) }.toSet()
|
||||
|
||||
override fun filterMore(
|
||||
newItems: List<Note>,
|
||||
account: Account,
|
||||
): List<Note> = newItems.filter { isNew(it, account) }
|
||||
}
|
||||
|
||||
fun getRouteWithArguments(navController: NavHostController): String? {
|
||||
val currentEntry = navController.currentBackStackEntry ?: return null
|
||||
return getRouteWithArguments(currentEntry.destination, currentEntry.arguments)
|
||||
|
@ -58,13 +58,14 @@ import com.vitorpamplona.amethyst.service.checkNotInMainThread
|
||||
import com.vitorpamplona.amethyst.service.lnurl.LightningAddressResolver
|
||||
import com.vitorpamplona.amethyst.ui.actions.Dao
|
||||
import com.vitorpamplona.amethyst.ui.components.UrlPreviewState
|
||||
import com.vitorpamplona.amethyst.ui.feeds.FeedState
|
||||
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
|
||||
import com.vitorpamplona.amethyst.ui.screen.SettingsState
|
||||
import com.vitorpamplona.amethyst.ui.screen.SharedPreferencesViewModel
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.notifications.CardFeedState
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.notifications.CombinedZap
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.notifications.showAmountAxis
|
||||
import com.vitorpamplona.amethyst.ui.stringRes
|
||||
@ -83,12 +84,14 @@ import com.vitorpamplona.quartz.events.ChatroomKeyable
|
||||
import com.vitorpamplona.quartz.events.DraftEvent
|
||||
import com.vitorpamplona.quartz.events.Event
|
||||
import com.vitorpamplona.quartz.events.EventInterface
|
||||
import com.vitorpamplona.quartz.events.GenericRepostEvent
|
||||
import com.vitorpamplona.quartz.events.GiftWrapEvent
|
||||
import com.vitorpamplona.quartz.events.LnZapEvent
|
||||
import com.vitorpamplona.quartz.events.LnZapRequestEvent
|
||||
import com.vitorpamplona.quartz.events.NIP90ContentDiscoveryResponseEvent
|
||||
import com.vitorpamplona.quartz.events.Participant
|
||||
import com.vitorpamplona.quartz.events.ReportEvent
|
||||
import com.vitorpamplona.quartz.events.RepostEvent
|
||||
import com.vitorpamplona.quartz.events.Response
|
||||
import com.vitorpamplona.quartz.events.SealedGossipEvent
|
||||
import com.vitorpamplona.quartz.events.SearchRelayListEvent
|
||||
@ -102,6 +105,7 @@ import kotlinx.collections.immutable.toImmutableList
|
||||
import kotlinx.collections.immutable.toImmutableSet
|
||||
import kotlinx.coroutines.CancellationException
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.async
|
||||
import kotlinx.coroutines.channels.BufferOverflow
|
||||
@ -110,7 +114,9 @@ import kotlinx.coroutines.flow.MutableSharedFlow
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.SharingStarted
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.combine
|
||||
import kotlinx.coroutines.flow.combineTransform
|
||||
import kotlinx.coroutines.flow.flatMapLatest
|
||||
import kotlinx.coroutines.flow.flowOn
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.flow.stateIn
|
||||
@ -120,7 +126,6 @@ import kotlinx.coroutines.suspendCancellableCoroutine
|
||||
import kotlinx.coroutines.withContext
|
||||
import kotlinx.coroutines.withTimeoutOrNull
|
||||
import kotlin.coroutines.resume
|
||||
import kotlin.time.measureTimedValue
|
||||
|
||||
@Immutable open class ToastMsg
|
||||
|
||||
@ -176,6 +181,82 @@ class AccountViewModel(
|
||||
|
||||
val feedStates = AccountFeedContentStates(this)
|
||||
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
val notificationHasNewItems =
|
||||
combineTransform(
|
||||
account.loadLastReadFlow("Notification"),
|
||||
feedStates.notifications.feedContent
|
||||
.flatMapLatest {
|
||||
if (it is CardFeedState.Loaded) {
|
||||
it.feed
|
||||
} else {
|
||||
MutableStateFlow(null)
|
||||
}
|
||||
}.map { it?.list?.firstOrNull()?.createdAt() },
|
||||
) { lastRead, newestItemCreatedAt ->
|
||||
emit(newestItemCreatedAt != null && newestItemCreatedAt > lastRead)
|
||||
}
|
||||
|
||||
val notificationHasNewItemsFlow = notificationHasNewItems.flowOn(Dispatchers.Default).stateIn(viewModelScope, SharingStarted.Eagerly, false)
|
||||
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
val messagesHasNewItems =
|
||||
feedStates.dmKnown.feedContent
|
||||
.flatMapLatest {
|
||||
if (it is FeedState.Loaded) {
|
||||
it.feed
|
||||
} else {
|
||||
MutableStateFlow(null)
|
||||
}
|
||||
}.flatMapLatest {
|
||||
val flows =
|
||||
it?.list?.mapNotNull { chat ->
|
||||
(chat.event as? ChatroomKeyable)?.let { event ->
|
||||
val room = event.chatroomKey(account.signer.pubKey)
|
||||
account.settings.getLastReadFlow("Room/${room.hashCode()}").map {
|
||||
(chat.event?.createdAt() ?: 0) > it
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (flows != null) {
|
||||
combine(flows) {
|
||||
it.any { it }
|
||||
}
|
||||
} else {
|
||||
MutableStateFlow(false)
|
||||
}
|
||||
}
|
||||
|
||||
val messagesHasNewItemsFlow = messagesHasNewItems.flowOn(Dispatchers.Default).stateIn(viewModelScope, SharingStarted.Eagerly, false)
|
||||
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
val homeHasNewItems =
|
||||
combineTransform(
|
||||
account.loadLastReadFlow("HomeFollows"),
|
||||
feedStates.homeNewThreads.feedContent
|
||||
.flatMapLatest {
|
||||
if (it is FeedState.Loaded) {
|
||||
it.feed
|
||||
} else {
|
||||
MutableStateFlow(null)
|
||||
}
|
||||
}.map {
|
||||
it?.list?.firstOrNull { it.event != null && it.event !is GenericRepostEvent && it.event !is RepostEvent }?.createdAt()
|
||||
},
|
||||
) { lastRead, newestItemCreatedAt ->
|
||||
emit(newestItemCreatedAt != null && newestItemCreatedAt > lastRead)
|
||||
}
|
||||
|
||||
val homeHasNewItemsFlow = homeHasNewItems.flowOn(Dispatchers.Default).stateIn(viewModelScope, SharingStarted.Eagerly, false)
|
||||
|
||||
val hasNewItems =
|
||||
mapOf(
|
||||
Route.Home to homeHasNewItemsFlow,
|
||||
Route.Message to messagesHasNewItemsFlow,
|
||||
Route.Notification to notificationHasNewItemsFlow,
|
||||
)
|
||||
|
||||
fun clearToasts() {
|
||||
viewModelScope.launch { toasts.emit(null) }
|
||||
}
|
||||
@ -1082,10 +1163,6 @@ class AccountViewModel(
|
||||
viewModelScope.launch(Dispatchers.IO) { onDone(OnlineChecker.isOnline(media)) }
|
||||
}
|
||||
|
||||
suspend fun refreshMarkAsReadObservers() {
|
||||
updateNotificationDots()
|
||||
}
|
||||
|
||||
fun loadAndMarkAsRead(
|
||||
routeForLastRead: String,
|
||||
createdAt: Long?,
|
||||
@ -1098,9 +1175,7 @@ class AccountViewModel(
|
||||
|
||||
if (onIsNew) {
|
||||
viewModelScope.launch(Dispatchers.Default) {
|
||||
if (account.markAsRead(routeForLastRead, createdAt)) {
|
||||
refreshMarkAsReadObservers()
|
||||
}
|
||||
account.markAsRead(routeForLastRead, createdAt)
|
||||
}
|
||||
}
|
||||
|
||||
@ -1112,8 +1187,6 @@ class AccountViewModel(
|
||||
onDone: () -> Unit,
|
||||
) {
|
||||
viewModelScope.launch(Dispatchers.IO) {
|
||||
var atLeastOne = false
|
||||
|
||||
for (note in notes) {
|
||||
note.event?.let { noteEvent ->
|
||||
val channelHex = note.channelHex()
|
||||
@ -1128,17 +1201,11 @@ class AccountViewModel(
|
||||
}
|
||||
|
||||
route?.let {
|
||||
if (account.markAsRead(route, noteEvent.createdAt())) {
|
||||
atLeastOne = true
|
||||
}
|
||||
account.markAsRead(route, noteEvent.createdAt())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (atLeastOne) {
|
||||
refreshMarkAsReadObservers()
|
||||
}
|
||||
|
||||
onDone()
|
||||
}
|
||||
}
|
||||
@ -1178,21 +1245,8 @@ class AccountViewModel(
|
||||
}
|
||||
|
||||
private var collectorJob: Job? = null
|
||||
val notificationDots = HasNotificationDot(bottomNavigationItems)
|
||||
private val bundlerInsert = BundledInsert<Set<Note>>(3000, Dispatchers.Default)
|
||||
|
||||
fun invalidateInsertData(newItems: Set<Note>) {
|
||||
bundlerInsert.invalidateList(newItems) { updateNotificationDots(it.flatten().toSet()) }
|
||||
}
|
||||
|
||||
fun updateNotificationDots(newNotes: Set<Note> = emptySet()) {
|
||||
val (value, elapsed) = measureTimedValue { notificationDots.update(newNotes, account) }
|
||||
Log.d(
|
||||
"Rendering Metrics",
|
||||
"Notification Dots Calculation in $elapsed for ${newNotes.size} new notes",
|
||||
)
|
||||
}
|
||||
|
||||
init {
|
||||
Log.d("Init", "AccountViewModel")
|
||||
collectorJob =
|
||||
@ -1202,10 +1256,9 @@ class AccountViewModel(
|
||||
LocalCache.live.newEventBundles.collect { newNotes ->
|
||||
Log.d(
|
||||
"Rendering Metrics",
|
||||
"Notification Dots Calculation refresh ${this@AccountViewModel} for ${account.userProfile().toBestDisplayName()}",
|
||||
"Update feeds ${this@AccountViewModel} for ${account.userProfile().toBestDisplayName()}",
|
||||
)
|
||||
feedStates.updateFeedsWith(newNotes)
|
||||
invalidateInsertData(newNotes)
|
||||
upgradeAttestations()
|
||||
}
|
||||
}
|
||||
@ -1573,33 +1626,6 @@ class AccountViewModel(
|
||||
}
|
||||
}
|
||||
|
||||
class HasNotificationDot(
|
||||
bottomNavigationItems: ImmutableList<Route>,
|
||||
) {
|
||||
val hasNewItems = bottomNavigationItems.associateWith { MutableStateFlow(false) }
|
||||
|
||||
fun update(
|
||||
newNotes: Set<Note>,
|
||||
account: Account,
|
||||
) {
|
||||
checkNotInMainThread()
|
||||
|
||||
hasNewItems.forEach {
|
||||
val (value, elapsed) =
|
||||
measureTimedValue {
|
||||
val newResult = it.key.hasNewItems(account, newNotes)
|
||||
if (newResult != it.value.value) {
|
||||
it.value.value = newResult
|
||||
}
|
||||
}
|
||||
Log.d(
|
||||
"Rendering Metrics",
|
||||
"Notification Dots Calculation for ${it.key.route} in $elapsed for ${newNotes.size} new notes",
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Immutable data class LoadedBechLink(
|
||||
val baseNote: Note?,
|
||||
val nip19: Nip19Bech32.ParseReturn,
|
||||
|
Loading…
x
Reference in New Issue
Block a user