From 9a6f88b81baf82718ea1c008d25656681c7a752b Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Wed, 29 Mar 2023 15:14:52 -0400 Subject: [PATCH 1/7] 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() } From 6a9a321e2bfdcb7445f4acf96050a7d5e2a39b27 Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Wed, 19 Apr 2023 13:00:52 -0400 Subject: [PATCH 2/7] Fixing format --- .../amethyst/ui/dal/NotificationFeedFilter.kt | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) 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 46e483c41..39334ca15 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 @@ -23,14 +23,14 @@ object NotificationFeedFilter : AdditiveFeedFilter() { return collection.filter { it.event !is ChannelCreateEvent && - it.event !is ChannelMetadataEvent && - it.event !is LnZapRequestEvent && - it.event !is BadgeDefinitionEvent && - it.event !is BadgeProfilesEvent && - it.author !== loggedInUser && - it.event?.isTaggedUser(loggedInUserHex) ?: false && - (it.author == null || !account.isHidden(it.author!!.pubkeyHex)) && - tagsAnEventByUser(it, loggedInUser) + it.event !is ChannelMetadataEvent && + it.event !is LnZapRequestEvent && + it.event !is BadgeDefinitionEvent && + it.event !is BadgeProfilesEvent && + it.author !== loggedInUser && + it.event?.isTaggedUser(loggedInUserHex) ?: false && + (it.author == null || !account.isHidden(it.author!!.pubkeyHex)) && + tagsAnEventByUser(it, loggedInUser) } } From e775ae9ada6f191b8e4daf365f3ad4c2a21d8697 Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Wed, 19 Apr 2023 13:54:00 -0400 Subject: [PATCH 3/7] Adding the new parameter. --- .../main/java/com/vitorpamplona/amethyst/model/LocalCache.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 3dc33582b..1beb05b36 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/model/LocalCache.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/model/LocalCache.kt @@ -255,7 +255,7 @@ object LocalCache { it.addReply(note) } - refreshObservers() + refreshObservers(note) } fun consume(event: BadgeDefinitionEvent) { From 47398e6e867c74e2e86611f060148e8361fdf220 Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Wed, 19 Apr 2023 15:56:58 -0400 Subject: [PATCH 4/7] Starts with an empty set as database --- .../main/java/com/vitorpamplona/amethyst/model/LocalCache.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 1beb05b36..ee4636288 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/model/LocalCache.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/model/LocalCache.kt @@ -762,7 +762,7 @@ object LocalCache { } } -class LocalCacheLiveData : LiveData>() { +class LocalCacheLiveData : LiveData>(setOf()) { // Refreshes observers in batches. private val bundler = BundledInsert(300, Dispatchers.Main) From 57b35398bebe2e8a6c960d441e7db50d3b5ca9bd Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Wed, 19 Apr 2023 15:57:17 -0400 Subject: [PATCH 5/7] Activates updates --- .../amethyst/ui/components/BundledUpdate.kt | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) 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 3d118e08c..ea6dff88f 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 @@ -57,9 +57,9 @@ class BundledInsert( private var atomicSet = AtomicReference>(setOf()) fun invalidateList(newObject: T, onUpdate: (Set) -> Unit) { - // atomicSet.updateAndGet() { - // it + newObject - // } + atomicSet.updateAndGet() { + it + newObject + } if (onlyOneInBlock.getAndSet(true)) { return @@ -68,10 +68,9 @@ class BundledInsert( val scope = CoroutineScope(Job() + dispatcher) scope.launch { try { - // onUpdate(atomicSet.getAndSet(emptySet())) - onUpdate(emptySet()) + onUpdate(atomicSet.getAndSet(emptySet())) delay(delay) - // onUpdate(atomicSet.getAndSet(emptySet())) + onUpdate(atomicSet.getAndSet(emptySet())) } finally { withContext(NonCancellable) { onlyOneInBlock.set(false) From abdad7fbea85b88c8ca6a543f988d86bf1f3b7d3 Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Wed, 19 Apr 2023 15:57:27 -0400 Subject: [PATCH 6/7] solve recursive method call --- .../com/vitorpamplona/amethyst/ui/dal/GlobalFeedFilter.kt | 8 ++++---- .../vitorpamplona/amethyst/ui/dal/HashtagFeedFilter.kt | 4 ++-- .../amethyst/ui/dal/HomeConversationsFeedFilter.kt | 6 +++--- .../amethyst/ui/dal/HomeNewThreadFeedFilter.kt | 8 ++++---- .../amethyst/ui/dal/NotificationFeedFilter.kt | 6 +++--- 5 files changed, 16 insertions(+), 16 deletions(-) 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 b55a9adf2..ebd4d2cdf 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 @@ -9,17 +9,17 @@ object GlobalFeedFilter : AdditiveFeedFilter() { lateinit var account: Account override fun feed(): List { - val notes = applyFilter(LocalCache.notes.values) - val longFormNotes = applyFilter(LocalCache.addressables.values) + val notes = innerApplyFilter(LocalCache.notes.values) + val longFormNotes = innerApplyFilter(LocalCache.addressables.values) return sort(notes + longFormNotes) } override fun applyFilter(collection: Set): List { - return applyFilter(collection) + return innerApplyFilter(collection) } - private fun applyFilter(collection: Collection): List { + private fun innerApplyFilter(collection: Collection): List { val followChannels = account.followingChannels val followUsers = account.followingKeySet() val now = System.currentTimeMillis() / 1000 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 c33593ff4..32354c408 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 @@ -18,14 +18,14 @@ object HashtagFeedFilter : AdditiveFeedFilter() { } override fun feed(): List { - return sort(applyFilter(LocalCache.notes.values)) + return sort(innerApplyFilter(LocalCache.notes.values)) } override fun applyFilter(collection: Set): List { return applyFilter(collection) } - private fun applyFilter(collection: Collection): List { + private fun innerApplyFilter(collection: Collection): List { val myTag = tag ?: return emptyList() return collection 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 5f2f3e6e5..f18885612 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 @@ -10,14 +10,14 @@ object HomeConversationsFeedFilter : AdditiveFeedFilter() { lateinit var account: Account override fun feed(): List { - return sort(applyFilter(LocalCache.notes.values)) + return sort(innerApplyFilter(LocalCache.notes.values)) } override fun applyFilter(collection: Set): List { - return applyFilter(collection) + return innerApplyFilter(collection) } - private fun applyFilter(collection: Collection): List { + private fun innerApplyFilter(collection: Collection): List { val user = account.userProfile() val followingKeySet = user.cachedFollowingKeySet() val followingTagSet = user.cachedFollowingTagSet() 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 443c11526..3ddf1471a 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 @@ -12,17 +12,17 @@ object HomeNewThreadFeedFilter : AdditiveFeedFilter() { lateinit var account: Account override fun feed(): List { - val notes = applyFilter(LocalCache.notes.values) - val longFormNotes = applyFilter(LocalCache.addressables.values) + val notes = innerApplyFilter(LocalCache.notes.values) + val longFormNotes = innerApplyFilter(LocalCache.addressables.values) return sort(notes + longFormNotes) } override fun applyFilter(collection: Set): List { - return applyFilter(collection) + return innerApplyFilter(collection) } - private fun applyFilter(collection: Collection): List { + private fun innerApplyFilter(collection: Collection): List { val user = account.userProfile() val followingKeySet = user.cachedFollowingKeySet() val followingTagSet = user.cachedFollowingTagSet() 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 39334ca15..8cb1f6280 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 @@ -10,14 +10,14 @@ object NotificationFeedFilter : AdditiveFeedFilter() { lateinit var account: Account override fun feed(): List { - return sort(applyFilter(LocalCache.notes.values)) + return sort(innerApplyFilter(LocalCache.notes.values)) } override fun applyFilter(collection: Set): List { - return applyFilter(collection) + return innerApplyFilter(collection) } - private fun applyFilter(collection: Collection): List { + private fun innerApplyFilter(collection: Collection): List { val loggedInUser = account.userProfile() val loggedInUserHex = loggedInUser.pubkeyHex From 6981fe8f8ac8ed8ec48fef059c34db0e80dada30 Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Wed, 19 Apr 2023 16:35:26 -0400 Subject: [PATCH 7/7] Adapting interfaces for the additive filter. --- .../amethyst/ui/dal/ChannelFeedFilter.kt | 8 +-- .../amethyst/ui/dal/ChatroomFeedFilter.kt | 12 ++--- .../amethyst/ui/dal/FeedFilter.kt | 6 +-- .../amethyst/ui/dal/GlobalFeedFilter.kt | 8 +-- .../amethyst/ui/dal/HashtagFeedFilter.kt | 10 ++-- .../ui/dal/HomeConversationsFeedFilter.kt | 8 +-- .../ui/dal/HomeNewThreadFeedFilter.kt | 8 +-- .../amethyst/ui/dal/NotificationFeedFilter.kt | 8 +-- .../amethyst/ui/screen/CardFeedViewModel.kt | 54 ++++++++++++++++--- 9 files changed, 81 insertions(+), 41 deletions(-) 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 14e4b0955..aa2921dce 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 @@ -23,13 +23,13 @@ object ChannelFeedFilter : AdditiveFeedFilter() { .reversed() } - override fun applyFilter(collection: Set): List { + override fun applyFilter(collection: Set): Set { return collection - .filter { it.idHex in channel.notes.keys } - .filter { account.isAcceptable(it) } + .filter { it.idHex in channel.notes.keys && account.isAcceptable(it) } + .toSet() } - override fun sort(collection: List): List { + override fun sort(collection: Set): 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 d885ce7e4..f96c68681 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 @@ -31,22 +31,22 @@ object ChatroomFeedFilter : AdditiveFeedFilter() { .reversed() } - override fun applyFilter(collection: Set): List { + override fun applyFilter(collection: Set): Set { val myAccount = account val myUser = withUser - if (myAccount == null || myUser == null) return emptyList() + if (myAccount == null || myUser == null) return emptySet() val messages = myAccount .userProfile() - .privateChatrooms[myUser] ?: return emptyList() + .privateChatrooms[myUser] ?: return emptySet() return collection - .filter { it in messages.roomMessages } - .filter { account?.isAcceptable(it) == true } + .filter { it in messages.roomMessages && account?.isAcceptable(it) == true } + .toSet() } - override fun sort(collection: List): List { + override fun sort(collection: Set): 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 9254d766b..505a959ef 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 @@ -19,15 +19,15 @@ abstract class FeedFilter { } abstract class AdditiveFeedFilter : FeedFilter() { - abstract fun applyFilter(collection: Set): List - abstract fun sort(collection: List): List + abstract fun applyFilter(collection: Set): Set + abstract fun sort(collection: Set): 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 + val newList = oldList.toSet() + newItemsToBeAdded sort(newList).take(1000) } else { oldList 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 ebd4d2cdf..f17d5ade5 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 @@ -15,11 +15,11 @@ object GlobalFeedFilter : AdditiveFeedFilter() { return sort(notes + longFormNotes) } - override fun applyFilter(collection: Set): List { + override fun applyFilter(collection: Set): Set { return innerApplyFilter(collection) } - private fun innerApplyFilter(collection: Collection): List { + private fun innerApplyFilter(collection: Collection): Set { val followChannels = account.followingChannels val followUsers = account.followingKeySet() val now = System.currentTimeMillis() / 1000 @@ -41,10 +41,10 @@ object GlobalFeedFilter : AdditiveFeedFilter() { // 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()!! <= now } - .toList() + .toSet() } - override fun sort(collection: List): List { + override fun sort(collection: Set): 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 32354c408..d4fd4765e 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 @@ -21,12 +21,12 @@ object HashtagFeedFilter : AdditiveFeedFilter() { return sort(innerApplyFilter(LocalCache.notes.values)) } - override fun applyFilter(collection: Set): List { + override fun applyFilter(collection: Set): Set { return applyFilter(collection) } - private fun innerApplyFilter(collection: Collection): List { - val myTag = tag ?: return emptyList() + private fun innerApplyFilter(collection: Collection): Set { + val myTag = tag ?: return emptySet() return collection .asSequence() @@ -40,10 +40,10 @@ object HashtagFeedFilter : AdditiveFeedFilter() { it.event?.isTaggedHash(myTag) == true } .filter { account.isAcceptable(it) } - .toList() + .toSet() } - override fun sort(collection: List): List { + override fun sort(collection: Set): 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 f18885612..6026e2669 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 @@ -13,11 +13,11 @@ object HomeConversationsFeedFilter : AdditiveFeedFilter() { return sort(innerApplyFilter(LocalCache.notes.values)) } - override fun applyFilter(collection: Set): List { + override fun applyFilter(collection: Set): Set { return innerApplyFilter(collection) } - private fun innerApplyFilter(collection: Collection): List { + private fun innerApplyFilter(collection: Collection): Set { val user = account.userProfile() val followingKeySet = user.cachedFollowingKeySet() val followingTagSet = user.cachedFollowingTagSet() @@ -31,10 +31,10 @@ object HomeConversationsFeedFilter : AdditiveFeedFilter() { it.author?.let { !account.isHidden(it) } ?: true && !it.isNewThread() } - .toList() + .toSet() } - override fun sort(collection: List): List { + override fun sort(collection: Set): 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 3ddf1471a..d31a45c68 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 @@ -18,11 +18,11 @@ object HomeNewThreadFeedFilter : AdditiveFeedFilter() { return sort(notes + longFormNotes) } - override fun applyFilter(collection: Set): List { + override fun applyFilter(collection: Set): Set { return innerApplyFilter(collection) } - private fun innerApplyFilter(collection: Collection): List { + private fun innerApplyFilter(collection: Collection): Set { val user = account.userProfile() val followingKeySet = user.cachedFollowingKeySet() val followingTagSet = user.cachedFollowingTagSet() @@ -36,10 +36,10 @@ object HomeNewThreadFeedFilter : AdditiveFeedFilter() { it.author?.let { !account.isHidden(it.pubkeyHex) } ?: true && it.isNewThread() } - .toList() + .toSet() } - override fun sort(collection: List): List { + override fun sort(collection: Set): 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 8cb1f6280..ba746ebba 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 @@ -13,11 +13,11 @@ object NotificationFeedFilter : AdditiveFeedFilter() { return sort(innerApplyFilter(LocalCache.notes.values)) } - override fun applyFilter(collection: Set): List { + override fun applyFilter(collection: Set): Set { return innerApplyFilter(collection) } - private fun innerApplyFilter(collection: Collection): List { + private fun innerApplyFilter(collection: Collection): Set { val loggedInUser = account.userProfile() val loggedInUserHex = loggedInUser.pubkeyHex @@ -31,10 +31,10 @@ object NotificationFeedFilter : AdditiveFeedFilter() { it.event?.isTaggedUser(loggedInUserHex) ?: false && (it.author == null || !account.isHidden(it.author!!.pubkeyHex)) && tagsAnEventByUser(it, loggedInUser) - } + }.toSet() } - override fun sort(collection: List): List { + override fun sort(collection: Set): 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 f2a2eb9cc..f7e1a24c0 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 @@ -14,7 +14,9 @@ import com.vitorpamplona.amethyst.service.model.LnZapEvent import com.vitorpamplona.amethyst.service.model.PrivateDmEvent import com.vitorpamplona.amethyst.service.model.ReactionEvent import com.vitorpamplona.amethyst.service.model.RepostEvent +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.FeedFilter import com.vitorpamplona.amethyst.ui.dal.NotificationFeedFilter import kotlinx.coroutines.CoroutineScope @@ -29,7 +31,7 @@ import kotlin.time.measureTimedValue class NotificationViewModel : CardFeedViewModel(NotificationFeedFilter) -open class CardFeedViewModel(val dataSource: FeedFilter) : ViewModel() { +open class CardFeedViewModel(val localFilter: FeedFilter) : ViewModel() { private val _feedContent = MutableStateFlow(CardFeedState.Loading) val feedContent = _feedContent.asStateFlow() @@ -45,9 +47,9 @@ open class CardFeedViewModel(val dataSource: FeedFilter) : ViewModel() { @Synchronized private fun refreshSuspended() { - val notes = dataSource.loadTop() + val notes = localFilter.loadTop() - val thisAccount = (dataSource as? NotificationFeedFilter)?.account + val thisAccount = (localFilter as? NotificationFeedFilter)?.account val lastNotesCopy = if (thisAccount == lastAccount) lastNotes else null val oldNotesState = _feedContent.value @@ -55,18 +57,18 @@ open class CardFeedViewModel(val dataSource: FeedFilter) : ViewModel() { val newCards = convertToCard(notes.minus(lastNotesCopy)) if (newCards.isNotEmpty()) { lastNotes = notes - lastAccount = (dataSource as? NotificationFeedFilter)?.account + lastAccount = (localFilter as? NotificationFeedFilter)?.account updateFeed((oldNotesState.feed.value + newCards).distinctBy { it.id() }.sortedBy { it.createdAt() }.reversed()) } } else { val cards = convertToCard(notes) lastNotes = notes - lastAccount = (dataSource as? NotificationFeedFilter)?.account + lastAccount = (localFilter as? NotificationFeedFilter)?.account updateFeed(cards) } } - private fun convertToCard(notes: List): List { + private fun convertToCard(notes: Collection): List { val reactionsPerEvent = mutableMapOf>() notes .filter { it.event is ReactionEvent } @@ -171,6 +173,28 @@ open class CardFeedViewModel(val dataSource: FeedFilter) : ViewModel() { } } + fun refreshFromOldState(newItems: Set) { + val oldNotesState = _feedContent.value + + val thisAccount = (localFilter as? NotificationFeedFilter)?.account + val lastNotesCopy = if (thisAccount == lastAccount) lastNotes else null + + if (lastNotesCopy != null && localFilter is AdditiveFeedFilter && oldNotesState is CardFeedState.Loaded) { + val filteredNewList = localFilter.applyFilter(newItems) + val actuallyNew = filteredNewList.minus(lastNotesCopy) + + val newCards = convertToCard(actuallyNew) + if (newCards.isNotEmpty()) { + lastNotes = lastNotesCopy + newItems + lastAccount = (localFilter as? NotificationFeedFilter)?.account + updateFeed((oldNotesState.feed.value + newCards).distinctBy { it.id() }.sortedBy { it.createdAt() }.reversed()) + } + } else { + // Refresh Everything + refreshSuspended() + } + } + @OptIn(ExperimentalTime::class) private val bundler = BundledUpdate(250, Dispatchers.IO) { // adds the time to perform the refresh into this delay @@ -180,13 +204,29 @@ open class CardFeedViewModel(val dataSource: FeedFilter) : ViewModel() { } Log.d("Time", "${this.javaClass.simpleName} Card update $elapsed") } + private val bundlerInsert = BundledInsert>(250, Dispatchers.IO) fun invalidateData() { bundler.invalidate() } + @OptIn(ExperimentalTime::class) + fun invalidateInsertData(newItems: Set) { + bundlerInsert.invalidateList(newItems) { + val (value, elapsed) = measureTimedValue { + refreshFromOldState(it.flatten().toSet()) + } + Log.d("Time", "${this.javaClass.simpleName} Card additive update $elapsed") + } + } + private val cacheListener: (Set) -> Unit = { newNotes -> - invalidateData() + if (localFilter is AdditiveFeedFilter && _feedContent.value is CardFeedState.Loaded) { + invalidateInsertData(newNotes) + } else { + // Refresh Everything + invalidateData() + } } init {