Turning LocalCache Listeners into an Additive Feed type.

This commit is contained in:
Vitor Pamplona
2023-03-29 15:14:52 -04:00
parent e41da35a02
commit 9a6f88b81b
14 changed files with 242 additions and 106 deletions

View File

@@ -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<Note>())
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<User> {
@@ -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>(LocalCacheState(cache)) {
class LocalCacheLiveData : LiveData<Set<Note>>() {
// Refreshes observers in batches.
private val bundler = BundledUpdate(300, Dispatchers.Main) {
if (hasActiveObservers()) {
refresh()
private val bundler = BundledInsert<Note>(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)

View File

@@ -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<T>(
val delay: Long,
val dispatcher: CoroutineDispatcher = Dispatchers.Default
) {
private var onlyOneInBlock = AtomicBoolean()
private var atomicSet = AtomicReference<Set<T>>(setOf<T>())
fun invalidateList(newObject: T, onUpdate: (Set<T>) -> 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)
}
}
}
}
}

View File

@@ -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<Note>() {
object ChannelFeedFilter : AdditiveFeedFilter<Note>() {
lateinit var account: Account
lateinit var channel: Channel
@@ -22,4 +22,14 @@ object ChannelFeedFilter : FeedFilter<Note>() {
.sortedBy { it.createdAt() }
.reversed()
}
override fun applyFilter(collection: Set<Note>): List<Note> {
return collection
.filter { it.idHex in channel.notes.keys }
.filter { account.isAcceptable(it) }
}
override fun sort(collection: List<Note>): List<Note> {
return collection.sortedBy { it.createdAt() }.reversed()
}
}

View File

@@ -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<Note>() {
object ChatroomFeedFilter : AdditiveFeedFilter<Note>() {
var account: Account? = null
var withUser: User? = null
@@ -30,4 +30,23 @@ object ChatroomFeedFilter : FeedFilter<Note>() {
.sortedBy { it.createdAt() }
.reversed()
}
override fun applyFilter(collection: Set<Note>): List<Note> {
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<Note>): List<Note> {
return collection.sortedBy { it.createdAt() }.reversed()
}
}

View File

@@ -4,7 +4,7 @@ import android.util.Log
import kotlin.time.ExperimentalTime
import kotlin.time.measureTimedValue
abstract class FeedFilter<T>() {
abstract class FeedFilter<T> {
@OptIn(ExperimentalTime::class)
fun loadTop(): List<T> {
val (feed, elapsed) = measureTimedValue {
@@ -17,3 +17,24 @@ abstract class FeedFilter<T>() {
abstract fun feed(): List<T>
}
abstract class AdditiveFeedFilter<T> : FeedFilter<T>() {
abstract fun applyFilter(collection: Set<T>): List<T>
abstract fun sort(collection: List<T>): List<T>
@OptIn(ExperimentalTime::class)
fun updateListWith(oldList: List<T>, newItems: Set<T>): List<T> {
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
}
}

View File

@@ -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<Note>() {
object GlobalFeedFilter : AdditiveFeedFilter<Note>() {
lateinit var account: Account
override fun feed(): List<Note> {
val notes = applyFilter(LocalCache.notes.values)
val longFormNotes = applyFilter(LocalCache.addressables.values)
return sort(notes + longFormNotes)
}
override fun applyFilter(collection: Set<Note>): List<Note> {
return applyFilter(collection)
}
private fun applyFilter(collection: Collection<Note>): List<Note> {
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<Note>() {
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<Note>): List<Note> {
return collection.sortedBy { it.createdAt() }.reversed()
}
}

View File

@@ -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<Note>() {
object HashtagFeedFilter : AdditiveFeedFilter<Note>() {
lateinit var account: Account
var tag: String? = null
fun loadHashtag(account: Account, tag: String?) {
this.account = account
this.tag = tag
}
override fun feed(): List<Note> {
return sort(applyFilter(LocalCache.notes.values))
}
override fun applyFilter(collection: Set<Note>): List<Note> {
return applyFilter(collection)
}
private fun applyFilter(collection: Collection<Note>): List<Note> {
val myTag = tag ?: return emptyList()
return LocalCache.notes.values
return collection
.asSequence()
.filter {
(
@@ -27,13 +40,10 @@ object HashtagFeedFilter : FeedFilter<Note>() {
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<Note>): List<Note> {
return collection.sortedBy { it.createdAt() }.reversed()
}
}

View File

@@ -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<Note>() {
object HomeConversationsFeedFilter : AdditiveFeedFilter<Note>() {
lateinit var account: Account
override fun feed(): List<Note> {
return sort(applyFilter(LocalCache.notes.values))
}
override fun applyFilter(collection: Set<Note>): List<Note> {
return applyFilter(collection)
}
private fun applyFilter(collection: Collection<Note>): List<Note> {
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<Note>() {
it.author?.let { !account.isHidden(it) } ?: true &&
!it.isNewThread()
}
.sortedBy { it.createdAt() }
.reversed()
.toList()
}
override fun sort(collection: List<Note>): List<Note> {
return collection.sortedBy { it.createdAt() }.reversed()
}
}

View File

@@ -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<Note>() {
object HomeNewThreadFeedFilter : AdditiveFeedFilter<Note>() {
lateinit var account: Account
override fun feed(): List<Note> {
val notes = applyFilter(LocalCache.notes.values)
val longFormNotes = applyFilter(LocalCache.addressables.values)
return sort(notes + longFormNotes)
}
override fun applyFilter(collection: Set<Note>): List<Note> {
return applyFilter(collection)
}
private fun applyFilter(collection: Collection<Note>): List<Note> {
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<Note>): List<Note> {
return collection.sortedBy { it.createdAt() }.reversed()
}
}

View File

@@ -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<Note>() {
object NotificationFeedFilter : AdditiveFeedFilter<Note>() {
lateinit var account: Account
override fun feed(): List<Note> {
return sort(applyFilter(LocalCache.notes.values))
}
override fun applyFilter(collection: Set<Note>): List<Note> {
return applyFilter(collection)
}
private fun applyFilter(collection: Collection<Note>): List<Note> {
val loggedInUser = account.userProfile()
return LocalCache.notes.values
return collection
.asSequence()
.filter {
it.event !is ChannelCreateEvent &&
@@ -36,8 +45,10 @@ object NotificationFeedFilter : FeedFilter<Note>() {
it.replyTo?.lastOrNull()?.author == loggedInUser ||
loggedInUser in it.directlyCiteUsers()
}
.sortedBy { it.createdAt() }
.toList()
.reversed()
}
override fun sort(collection: List<Note>): List<Note> {
return collection.sortedBy { it.createdAt() }.reversed()
}
}

View File

@@ -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<Note>) : 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<Note>) : ViewModel() {
private fun updateFeed(notes: List<Card>) {
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<Note>) : ViewModel() {
bundler.invalidate()
}
private val cacheListener: (LocalCacheState) -> Unit = {
private val cacheListener: (Set<Note>) -> Unit = { newNotes ->
invalidateData()
}

View File

@@ -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<Note>) : 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<Note>) : ViewModel() {
private fun updateFeed(notes: List<Note>) {
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<Note>) : ViewModel() {
}
}
fun refreshFromOldState(newItems: Set<Note>) {
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<Set<Note>>(250, Dispatchers.IO)
fun invalidateData() {
bundler.invalidate()
}
private val cacheListener: (LocalCacheState) -> Unit = {
invalidateData()
fun invalidateInsertData(newItems: Set<Note>) {
bundlerInsert.invalidateList(newItems) {
refreshFromOldState(it.flatten().toSet())
}
}
private val cacheListener: (Set<Note>) -> Unit = { newNotes ->
if (localFilter is AdditiveFeedFilter && _feedContent.value is FeedState.Loaded) {
invalidateInsertData(newNotes)
} else {
// Refresh Everything
invalidateData()
}
}
init {

View File

@@ -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<Pair<Note, Note>>) : 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<Pair<Note, Note>>) : Vi
private fun updateFeed(notes: List<Pair<Note, Note>>) {
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<Pair<Note, Note>>) : Vi
bundler.invalidate()
}
private val cacheListener: (LocalCacheState) -> Unit = {
private val cacheListener: (Set<Note>) -> Unit = { newNotes ->
invalidateData()
}

View File

@@ -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<User>) : 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<User>) : ViewModel() {
private fun updateFeed(notes: List<User>) {
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<User>) : ViewModel() {
bundler.invalidate()
}
private val cacheListener: (LocalCacheState) -> Unit = {
private val cacheListener: (Set<Note>) -> Unit = { newNotes ->
invalidateData()
}