From 9a6f88b81baf82718ea1c008d25656681c7a752b Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Wed, 29 Mar 2023 15:14:52 -0400 Subject: [PATCH] Turning LocalCache Listeners into an Additive Feed type. --- .../amethyst/model/LocalCache.kt | 61 +++++++++---------- .../amethyst/ui/components/BundledUpdate.kt | 36 +++++++++++ .../amethyst/ui/dal/ChannelFeedFilter.kt | 12 +++- .../amethyst/ui/dal/ChatroomFeedFilter.kt | 21 ++++++- .../amethyst/ui/dal/FeedFilter.kt | 23 ++++++- .../amethyst/ui/dal/GlobalFeedFilter.kt | 42 ++++++------- .../amethyst/ui/dal/HashtagFeedFilter.kt | 24 +++++--- .../ui/dal/HomeConversationsFeedFilter.kt | 20 ++++-- .../ui/dal/HomeNewThreadFeedFilter.kt | 34 ++++++----- .../amethyst/ui/dal/NotificationFeedFilter.kt | 19 ++++-- .../amethyst/ui/screen/CardFeedViewModel.kt | 7 +-- .../amethyst/ui/screen/FeedViewModel.kt | 34 +++++++++-- .../amethyst/ui/screen/LnZapFeedViewModel.kt | 7 +-- .../amethyst/ui/screen/UserFeedViewModel.kt | 8 +-- 14 files changed, 242 insertions(+), 106 deletions(-) diff --git a/app/src/main/java/com/vitorpamplona/amethyst/model/LocalCache.kt b/app/src/main/java/com/vitorpamplona/amethyst/model/LocalCache.kt index da8686412..e0831547a 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/model/LocalCache.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/model/LocalCache.kt @@ -27,7 +27,7 @@ import com.vitorpamplona.amethyst.service.model.ReportEvent import com.vitorpamplona.amethyst.service.model.RepostEvent import com.vitorpamplona.amethyst.service.model.TextNoteEvent import com.vitorpamplona.amethyst.service.relays.Relay -import com.vitorpamplona.amethyst.ui.components.BundledUpdate +import com.vitorpamplona.amethyst.ui.components.BundledInsert import fr.acinq.secp256k1.Hex import kotlinx.coroutines.* import nostr.postr.toNpub @@ -208,7 +208,7 @@ object LocalCache { it.addReply(note) } - refreshObservers() + refreshObservers(note) } fun consume(event: LongTextNoteEvent, relay: Relay?) { @@ -237,7 +237,7 @@ object LocalCache { author.addNote(note) - refreshObservers() + refreshObservers(note) } } @@ -251,7 +251,7 @@ object LocalCache { if (event.createdAt > (note.createdAt() ?: 0)) { note.loadEvent(event, author, emptyList()) - refreshObservers() + refreshObservers(note) } } @@ -269,8 +269,6 @@ object LocalCache { note.loadEvent(event, author, replyTo) author.updateAcceptedBadges(note) - - refreshObservers() } } @@ -292,7 +290,7 @@ object LocalCache { it.addReply(note) } - refreshObservers() + refreshObservers(note) } @Suppress("UNUSED_PARAMETER") @@ -338,7 +336,7 @@ object LocalCache { recipient.addMessage(author, note) } - refreshObservers() + refreshObservers(note) } fun consume(event: DeletionEvent) { @@ -386,7 +384,7 @@ object LocalCache { } if (deletedAtLeastOne) { - live.invalidateData() + // refreshObservers() } } @@ -412,7 +410,7 @@ object LocalCache { it.addBoost(note) } - refreshObservers() + refreshObservers(note) } fun consume(event: ReactionEvent) { @@ -450,6 +448,8 @@ object LocalCache { it.addReport(note) } } + + refreshObservers(note) } fun consume(event: ReportEvent, relay: Relay?) { @@ -480,6 +480,8 @@ object LocalCache { repliesTo.forEach { it.addReport(note) } + + refreshObservers(note) } fun consume(event: ChannelCreateEvent) { @@ -496,7 +498,7 @@ object LocalCache { oldChannel.addNote(note) note.loadEvent(event, author, emptyList()) - refreshObservers() + refreshObservers(note) } } @@ -516,7 +518,7 @@ object LocalCache { oldChannel.addNote(note) note.loadEvent(event, author, emptyList()) - refreshObservers() + refreshObservers(note) } } else { // Log.d("MT","Relay sent a previous Metadata Event ${oldUser.toBestDisplayName()} ${formattedDateTime(event.createdAt)} > ${formattedDateTime(oldUser.updatedAt)}") @@ -563,7 +565,7 @@ object LocalCache { it.addReply(note) } - refreshObservers() + refreshObservers(note) } @Suppress("UNUSED_PARAMETER") @@ -606,6 +608,8 @@ object LocalCache { mentions.forEach { it.addZap(zapRequest, note) } + + refreshObservers(note) } fun consume(event: LnZapRequestEvent) { @@ -629,6 +633,8 @@ object LocalCache { mentions.forEach { it.addZap(note, null) } + + refreshObservers(note) } fun findUsersStartingWith(username: String): List { @@ -734,30 +740,23 @@ object LocalCache { } // Observers line up here. - val live: LocalCacheLiveData = LocalCacheLiveData(this) + val live: LocalCacheLiveData = LocalCacheLiveData() - private fun refreshObservers() { - live.invalidateData() + private fun refreshObservers(newNote: Note) { + live.invalidateData(newNote) } } -class LocalCacheLiveData(val cache: LocalCache) : - LiveData(LocalCacheState(cache)) { +class LocalCacheLiveData : LiveData>() { // Refreshes observers in batches. - private val bundler = BundledUpdate(300, Dispatchers.Main) { - if (hasActiveObservers()) { - refresh() + private val bundler = BundledInsert(300, Dispatchers.Main) + + fun invalidateData(newNote: Note) { + bundler.invalidateList(newNote) { bundledNewNotes -> + if (hasActiveObservers()) { + postValue(bundledNewNotes) + } } } - - fun invalidateData() { - bundler.invalidate() - } - - private fun refresh() { - postValue(LocalCacheState(cache)) - } } - -class LocalCacheState(val cache: LocalCache) diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/components/BundledUpdate.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/components/BundledUpdate.kt index d2172e560..3d118e08c 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/components/BundledUpdate.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/components/BundledUpdate.kt @@ -9,6 +9,7 @@ import kotlinx.coroutines.delay import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import java.util.concurrent.atomic.AtomicBoolean +import java.util.concurrent.atomic.AtomicReference /** * This class is designed to have a waiting time between two calls of invalidate @@ -44,3 +45,38 @@ class BundledUpdate( } } } + +/** + * This class is designed to have a waiting time between two calls of invalidate + */ +class BundledInsert( + val delay: Long, + val dispatcher: CoroutineDispatcher = Dispatchers.Default +) { + private var onlyOneInBlock = AtomicBoolean() + private var atomicSet = AtomicReference>(setOf()) + + fun invalidateList(newObject: T, onUpdate: (Set) -> Unit) { + // atomicSet.updateAndGet() { + // it + newObject + // } + + if (onlyOneInBlock.getAndSet(true)) { + return + } + + val scope = CoroutineScope(Job() + dispatcher) + scope.launch { + try { + // onUpdate(atomicSet.getAndSet(emptySet())) + onUpdate(emptySet()) + delay(delay) + // onUpdate(atomicSet.getAndSet(emptySet())) + } finally { + withContext(NonCancellable) { + onlyOneInBlock.set(false) + } + } + } + } +} diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/ChannelFeedFilter.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/ChannelFeedFilter.kt index 6e19217dd..14e4b0955 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/ChannelFeedFilter.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/ChannelFeedFilter.kt @@ -5,7 +5,7 @@ import com.vitorpamplona.amethyst.model.Channel import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.model.Note -object ChannelFeedFilter : FeedFilter() { +object ChannelFeedFilter : AdditiveFeedFilter() { lateinit var account: Account lateinit var channel: Channel @@ -22,4 +22,14 @@ object ChannelFeedFilter : FeedFilter() { .sortedBy { it.createdAt() } .reversed() } + + override fun applyFilter(collection: Set): List { + return collection + .filter { it.idHex in channel.notes.keys } + .filter { account.isAcceptable(it) } + } + + override fun sort(collection: List): List { + return collection.sortedBy { it.createdAt() }.reversed() + } } diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/ChatroomFeedFilter.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/ChatroomFeedFilter.kt index 9d77e1ac4..d885ce7e4 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/ChatroomFeedFilter.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/ChatroomFeedFilter.kt @@ -5,7 +5,7 @@ import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.model.User -object ChatroomFeedFilter : FeedFilter() { +object ChatroomFeedFilter : AdditiveFeedFilter() { var account: Account? = null var withUser: User? = null @@ -30,4 +30,23 @@ object ChatroomFeedFilter : FeedFilter() { .sortedBy { it.createdAt() } .reversed() } + + override fun applyFilter(collection: Set): List { + val myAccount = account + val myUser = withUser + + if (myAccount == null || myUser == null) return emptyList() + + val messages = myAccount + .userProfile() + .privateChatrooms[myUser] ?: return emptyList() + + return collection + .filter { it in messages.roomMessages } + .filter { account?.isAcceptable(it) == true } + } + + override fun sort(collection: List): List { + return collection.sortedBy { it.createdAt() }.reversed() + } } diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/FeedFilter.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/FeedFilter.kt index e5f78f4e2..d4395f8da 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/FeedFilter.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/FeedFilter.kt @@ -4,7 +4,7 @@ import android.util.Log import kotlin.time.ExperimentalTime import kotlin.time.measureTimedValue -abstract class FeedFilter() { +abstract class FeedFilter { @OptIn(ExperimentalTime::class) fun loadTop(): List { val (feed, elapsed) = measureTimedValue { @@ -17,3 +17,24 @@ abstract class FeedFilter() { abstract fun feed(): List } + +abstract class AdditiveFeedFilter : FeedFilter() { + abstract fun applyFilter(collection: Set): List + abstract fun sort(collection: List): List + + @OptIn(ExperimentalTime::class) + fun updateListWith(oldList: List, newItems: Set): List { + val (feed, elapsed) = measureTimedValue { + val newItemsToBeAdded = applyFilter(newItems) + if (newItemsToBeAdded.isNotEmpty()) { + val newList = oldList + newItemsToBeAdded + sort(newList).take(1000) + } else { + oldList + } + } + + Log.d("Time", "${this.javaClass.simpleName} Feed in $elapsed with ${feed.size} objects") + return feed + } +} diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/GlobalFeedFilter.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/GlobalFeedFilter.kt index ffed429cc..9122dd8f6 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/GlobalFeedFilter.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/GlobalFeedFilter.kt @@ -7,18 +7,28 @@ import com.vitorpamplona.amethyst.service.model.ChannelMessageEvent import com.vitorpamplona.amethyst.service.model.LongTextNoteEvent import com.vitorpamplona.amethyst.service.model.TextNoteEvent -object GlobalFeedFilter : FeedFilter() { +object GlobalFeedFilter : AdditiveFeedFilter() { lateinit var account: Account override fun feed(): List { + val notes = applyFilter(LocalCache.notes.values) + val longFormNotes = applyFilter(LocalCache.addressables.values) + + return sort(notes + longFormNotes) + } + + override fun applyFilter(collection: Set): List { + return applyFilter(collection) + } + + private fun applyFilter(collection: Collection): List { val followChannels = account.followingChannels() val followUsers = account.followingKeySet() - val notes = LocalCache.notes.values + return collection .asSequence() .filter { - (it.event is TextNoteEvent || it.event is LongTextNoteEvent || it.event is ChannelMessageEvent) && - it.replyTo.isNullOrEmpty() + (it.event is TextNoteEvent || it.event is LongTextNoteEvent || it.event is ChannelMessageEvent) && it.replyTo.isNullOrEmpty() } .filter { // does not show events already in the public chat list @@ -32,27 +42,9 @@ object GlobalFeedFilter : FeedFilter() { it.createdAt()!! <= System.currentTimeMillis() / 1000 } .toList() + } - val longFormNotes = LocalCache.addressables.values - .asSequence() - .filter { - (it.event is LongTextNoteEvent) && it.replyTo.isNullOrEmpty() - } - .filter { - // does not show events already in the public chat list - (it.channel() == null || it.channel() !in followChannels) && - // does not show people the user already follows - (it.author?.pubkeyHex !in followUsers) - } - .filter { account.isAcceptable(it) } - .filter { - // Do not show notes with the creation time exceeding the current time, as they will always stay at the top of the global feed, which is cheating. - it.createdAt()!! <= System.currentTimeMillis() / 1000 - } - .toList() - - return (notes + longFormNotes) - .sortedBy { it.createdAt() } - .reversed() + override fun sort(collection: List): List { + return collection.sortedBy { it.createdAt() }.reversed() } } diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/HashtagFeedFilter.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/HashtagFeedFilter.kt index 844505ff3..c33593ff4 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/HashtagFeedFilter.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/HashtagFeedFilter.kt @@ -8,14 +8,27 @@ import com.vitorpamplona.amethyst.service.model.LongTextNoteEvent import com.vitorpamplona.amethyst.service.model.PrivateDmEvent import com.vitorpamplona.amethyst.service.model.TextNoteEvent -object HashtagFeedFilter : FeedFilter() { +object HashtagFeedFilter : AdditiveFeedFilter() { lateinit var account: Account var tag: String? = null + fun loadHashtag(account: Account, tag: String?) { + this.account = account + this.tag = tag + } + override fun feed(): List { + return sort(applyFilter(LocalCache.notes.values)) + } + + override fun applyFilter(collection: Set): List { + return applyFilter(collection) + } + + private fun applyFilter(collection: Collection): List { val myTag = tag ?: return emptyList() - return LocalCache.notes.values + return collection .asSequence() .filter { ( @@ -27,13 +40,10 @@ object HashtagFeedFilter : FeedFilter() { it.event?.isTaggedHash(myTag) == true } .filter { account.isAcceptable(it) } - .sortedBy { it.createdAt() } .toList() - .reversed() } - fun loadHashtag(account: Account, tag: String?) { - this.account = account - this.tag = tag + override fun sort(collection: List): List { + return collection.sortedBy { it.createdAt() }.reversed() } } diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/HomeConversationsFeedFilter.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/HomeConversationsFeedFilter.kt index 50385acfd..3a2fda470 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/HomeConversationsFeedFilter.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/HomeConversationsFeedFilter.kt @@ -5,15 +5,24 @@ import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.service.model.TextNoteEvent -object HomeConversationsFeedFilter : FeedFilter() { +object HomeConversationsFeedFilter : AdditiveFeedFilter() { lateinit var account: Account override fun feed(): List { + return sort(applyFilter(LocalCache.notes.values)) + } + + override fun applyFilter(collection: Set): List { + return applyFilter(collection) + } + + private fun applyFilter(collection: Collection): List { val user = account.userProfile() val followingKeySet = user.cachedFollowingKeySet() val followingTagSet = user.cachedFollowingTagSet() - return LocalCache.notes.values + return collection + .asSequence() .filter { (it.event is TextNoteEvent) && (it.author?.pubkeyHex in followingKeySet || (it.event?.isTaggedHashes(followingTagSet) ?: false)) && @@ -21,7 +30,10 @@ object HomeConversationsFeedFilter : FeedFilter() { it.author?.let { !account.isHidden(it) } ?: true && !it.isNewThread() } - .sortedBy { it.createdAt() } - .reversed() + .toList() + } + + override fun sort(collection: List): List { + return collection.sortedBy { it.createdAt() }.reversed() } } diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/HomeNewThreadFeedFilter.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/HomeNewThreadFeedFilter.kt index ca88aaa96..5ae61e017 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/HomeNewThreadFeedFilter.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/HomeNewThreadFeedFilter.kt @@ -7,34 +7,38 @@ import com.vitorpamplona.amethyst.service.model.LongTextNoteEvent import com.vitorpamplona.amethyst.service.model.RepostEvent import com.vitorpamplona.amethyst.service.model.TextNoteEvent -object HomeNewThreadFeedFilter : FeedFilter() { +object HomeNewThreadFeedFilter : AdditiveFeedFilter() { lateinit var account: Account override fun feed(): List { + val notes = applyFilter(LocalCache.notes.values) + val longFormNotes = applyFilter(LocalCache.addressables.values) + + return sort(notes + longFormNotes) + } + + override fun applyFilter(collection: Set): List { + return applyFilter(collection) + } + + private fun applyFilter(collection: Collection): List { val user = account.userProfile() val followingKeySet = user.cachedFollowingKeySet() val followingTagSet = user.cachedFollowingTagSet() - val notes = LocalCache.notes.values + return collection + .asSequence() .filter { it -> - (it.event is TextNoteEvent || it.event is RepostEvent) && + (it.event is TextNoteEvent || it.event is RepostEvent || it.event is LongTextNoteEvent) && (it.author?.pubkeyHex in followingKeySet || (it.event?.isTaggedHashes(followingTagSet) ?: false)) && // && account.isAcceptable(it) // This filter follows only. No need to check if acceptable it.author?.let { !account.isHidden(it) } ?: true && it.isNewThread() } + .toList() + } - val longFormNotes = LocalCache.addressables.values - .filter { it -> - (it.event is LongTextNoteEvent) && - (it.author?.pubkeyHex in followingKeySet || (it.event?.isTaggedHashes(followingTagSet) ?: false)) && - // && account.isAcceptable(it) // This filter follows only. No need to check if acceptable - it.author?.let { !account.isHidden(it) } ?: true && - it.isNewThread() - } - - return (notes + longFormNotes) - .sortedBy { it.createdAt() } - .reversed() + override fun sort(collection: List): List { + return collection.sortedBy { it.createdAt() }.reversed() } } diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/NotificationFeedFilter.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/NotificationFeedFilter.kt index 39dcc920e..aa9327bf0 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/NotificationFeedFilter.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/NotificationFeedFilter.kt @@ -5,12 +5,21 @@ import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.service.model.* -object NotificationFeedFilter : FeedFilter() { +object NotificationFeedFilter : AdditiveFeedFilter() { lateinit var account: Account override fun feed(): List { + return sort(applyFilter(LocalCache.notes.values)) + } + + override fun applyFilter(collection: Set): List { + return applyFilter(collection) + } + + private fun applyFilter(collection: Collection): List { val loggedInUser = account.userProfile() - return LocalCache.notes.values + + return collection .asSequence() .filter { it.event !is ChannelCreateEvent && @@ -36,8 +45,10 @@ object NotificationFeedFilter : FeedFilter() { it.replyTo?.lastOrNull()?.author == loggedInUser || loggedInUser in it.directlyCiteUsers() } - .sortedBy { it.createdAt() } .toList() - .reversed() + } + + override fun sort(collection: List): List { + return collection.sortedBy { it.createdAt() }.reversed() } } diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/CardFeedViewModel.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/CardFeedViewModel.kt index 34337677e..5debefce8 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/CardFeedViewModel.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/CardFeedViewModel.kt @@ -4,7 +4,6 @@ import android.util.Log import androidx.compose.runtime.mutableStateOf import androidx.lifecycle.ViewModel import com.vitorpamplona.amethyst.model.LocalCache -import com.vitorpamplona.amethyst.model.LocalCacheState import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.service.model.BadgeAwardEvent import com.vitorpamplona.amethyst.service.model.ChannelCreateEvent @@ -47,7 +46,7 @@ open class CardFeedViewModel(val dataSource: FeedFilter) : ViewModel() { val lastNotesCopy = lastNotes - val oldNotesState = feedContent.value + val oldNotesState = _feedContent.value if (lastNotesCopy != null && oldNotesState is CardFeedState.Loaded) { val newCards = convertToCard(notes.minus(lastNotesCopy)) if (newCards.isNotEmpty()) { @@ -125,7 +124,7 @@ open class CardFeedViewModel(val dataSource: FeedFilter) : ViewModel() { private fun updateFeed(notes: List) { val scope = CoroutineScope(Job() + Dispatchers.Main) scope.launch { - val currentState = feedContent.value + val currentState = _feedContent.value if (notes.isEmpty()) { _feedContent.update { CardFeedState.Empty } @@ -152,7 +151,7 @@ open class CardFeedViewModel(val dataSource: FeedFilter) : ViewModel() { bundler.invalidate() } - private val cacheListener: (LocalCacheState) -> Unit = { + private val cacheListener: (Set) -> Unit = { newNotes -> invalidateData() } diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/FeedViewModel.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/FeedViewModel.kt index a6b2fcdb1..60daf1072 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/FeedViewModel.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/FeedViewModel.kt @@ -3,9 +3,10 @@ package com.vitorpamplona.amethyst.ui.screen import androidx.compose.runtime.mutableStateOf import androidx.lifecycle.ViewModel import com.vitorpamplona.amethyst.model.LocalCache -import com.vitorpamplona.amethyst.model.LocalCacheState import com.vitorpamplona.amethyst.model.Note +import com.vitorpamplona.amethyst.ui.components.BundledInsert import com.vitorpamplona.amethyst.ui.components.BundledUpdate +import com.vitorpamplona.amethyst.ui.dal.AdditiveFeedFilter import com.vitorpamplona.amethyst.ui.dal.BookmarkPrivateFeedFilter import com.vitorpamplona.amethyst.ui.dal.BookmarkPublicFeedFilter import com.vitorpamplona.amethyst.ui.dal.ChannelFeedFilter @@ -65,7 +66,7 @@ abstract class FeedViewModel(val localFilter: FeedFilter) : ViewModel() { fun refreshSuspended() { val notes = newListFromDataSource() - val oldNotesState = feedContent.value + val oldNotesState = _feedContent.value if (oldNotesState is FeedState.Loaded) { // Using size as a proxy for has changed. if (notes != oldNotesState.feed.value) { @@ -79,7 +80,7 @@ abstract class FeedViewModel(val localFilter: FeedFilter) : ViewModel() { private fun updateFeed(notes: List) { val scope = CoroutineScope(Job() + Dispatchers.Main) scope.launch { - val currentState = feedContent.value + val currentState = _feedContent.value if (notes.isEmpty()) { _feedContent.update { FeedState.Empty } } else if (currentState is FeedState.Loaded) { @@ -91,18 +92,41 @@ abstract class FeedViewModel(val localFilter: FeedFilter) : ViewModel() { } } + fun refreshFromOldState(newItems: Set) { + val oldNotesState = _feedContent.value + if (localFilter is AdditiveFeedFilter && oldNotesState is FeedState.Loaded) { + val newList = localFilter.updateListWith(oldNotesState.feed.value, newItems.toSet()) + updateFeed(newList) + } else { + // Refresh Everything + refreshSuspended() + } + } + private val bundler = BundledUpdate(250, Dispatchers.IO) { // adds the time to perform the refresh into this delay // holding off new updates in case of heavy refresh routines. refreshSuspended() } + private val bundlerInsert = BundledInsert>(250, Dispatchers.IO) fun invalidateData() { bundler.invalidate() } - private val cacheListener: (LocalCacheState) -> Unit = { - invalidateData() + fun invalidateInsertData(newItems: Set) { + bundlerInsert.invalidateList(newItems) { + refreshFromOldState(it.flatten().toSet()) + } + } + + private val cacheListener: (Set) -> Unit = { newNotes -> + if (localFilter is AdditiveFeedFilter && _feedContent.value is FeedState.Loaded) { + invalidateInsertData(newNotes) + } else { + // Refresh Everything + invalidateData() + } } init { diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/LnZapFeedViewModel.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/LnZapFeedViewModel.kt index 9973afab8..8c6806de8 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/LnZapFeedViewModel.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/LnZapFeedViewModel.kt @@ -3,7 +3,6 @@ package com.vitorpamplona.amethyst.ui.screen import androidx.compose.runtime.mutableStateOf import androidx.lifecycle.ViewModel import com.vitorpamplona.amethyst.model.LocalCache -import com.vitorpamplona.amethyst.model.LocalCacheState import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.ui.components.BundledUpdate import com.vitorpamplona.amethyst.ui.dal.FeedFilter @@ -32,7 +31,7 @@ open class LnZapFeedViewModel(val dataSource: FeedFilter>) : Vi private fun refreshSuspended() { val notes = dataSource.loadTop() - val oldNotesState = feedContent.value + val oldNotesState = _feedContent.value if (oldNotesState is LnZapFeedState.Loaded) { // Using size as a proxy for has changed. if (notes != oldNotesState.feed.value) { @@ -46,7 +45,7 @@ open class LnZapFeedViewModel(val dataSource: FeedFilter>) : Vi private fun updateFeed(notes: List>) { val scope = CoroutineScope(Job() + Dispatchers.Main) scope.launch { - val currentState = feedContent.value + val currentState = _feedContent.value if (notes.isEmpty()) { _feedContent.update { LnZapFeedState.Empty } } else if (currentState is LnZapFeedState.Loaded) { @@ -68,7 +67,7 @@ open class LnZapFeedViewModel(val dataSource: FeedFilter>) : Vi bundler.invalidate() } - private val cacheListener: (LocalCacheState) -> Unit = { + private val cacheListener: (Set) -> Unit = { newNotes -> invalidateData() } diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/UserFeedViewModel.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/UserFeedViewModel.kt index 500320035..aae7fd358 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/UserFeedViewModel.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/UserFeedViewModel.kt @@ -3,7 +3,7 @@ package com.vitorpamplona.amethyst.ui.screen import androidx.compose.runtime.mutableStateOf import androidx.lifecycle.ViewModel import com.vitorpamplona.amethyst.model.LocalCache -import com.vitorpamplona.amethyst.model.LocalCacheState +import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.ui.components.BundledUpdate import com.vitorpamplona.amethyst.ui.dal.FeedFilter @@ -36,7 +36,7 @@ open class UserFeedViewModel(val dataSource: FeedFilter) : ViewModel() { private fun refreshSuspended() { val notes = dataSource.loadTop() - val oldNotesState = feedContent.value + val oldNotesState = _feedContent.value if (oldNotesState is UserFeedState.Loaded) { // Using size as a proxy for has changed. if (notes != oldNotesState.feed.value) { @@ -50,7 +50,7 @@ open class UserFeedViewModel(val dataSource: FeedFilter) : ViewModel() { private fun updateFeed(notes: List) { val scope = CoroutineScope(Job() + Dispatchers.Main) scope.launch { - val currentState = feedContent.value + val currentState = _feedContent.value if (notes.isEmpty()) { _feedContent.update { UserFeedState.Empty } } else if (currentState is UserFeedState.Loaded) { @@ -72,7 +72,7 @@ open class UserFeedViewModel(val dataSource: FeedFilter) : ViewModel() { bundler.invalidate() } - private val cacheListener: (LocalCacheState) -> Unit = { + private val cacheListener: (Set) -> Unit = { newNotes -> invalidateData() }