mirror of
https://github.com/vitorpamplona/amethyst.git
synced 2025-09-18 19:20:45 +02:00
Merge pull request #356 from vitorpamplona/additive_feed_test
Additive feeds (No more list reloadings in the main screens)
This commit is contained in:
@@ -6,7 +6,7 @@ import com.fasterxml.jackson.databind.DeserializationFeature
|
||||
import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
|
||||
import com.vitorpamplona.amethyst.service.model.*
|
||||
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
|
||||
@@ -189,7 +189,7 @@ object LocalCache {
|
||||
it.addReply(note)
|
||||
}
|
||||
|
||||
refreshObservers()
|
||||
refreshObservers(note)
|
||||
}
|
||||
|
||||
fun consume(event: LongTextNoteEvent, relay: Relay?) {
|
||||
@@ -218,7 +218,7 @@ object LocalCache {
|
||||
|
||||
author.addNote(note)
|
||||
|
||||
refreshObservers()
|
||||
refreshObservers(note)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -255,7 +255,7 @@ object LocalCache {
|
||||
it.addReply(note)
|
||||
}
|
||||
|
||||
refreshObservers()
|
||||
refreshObservers(note)
|
||||
}
|
||||
|
||||
fun consume(event: BadgeDefinitionEvent) {
|
||||
@@ -268,7 +268,7 @@ object LocalCache {
|
||||
if (event.createdAt > (note.createdAt() ?: 0)) {
|
||||
note.loadEvent(event, author, emptyList<Note>())
|
||||
|
||||
refreshObservers()
|
||||
refreshObservers(note)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -286,8 +286,6 @@ object LocalCache {
|
||||
note.loadEvent(event, author, replyTo)
|
||||
|
||||
author.updateAcceptedBadges(note)
|
||||
|
||||
refreshObservers()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -309,7 +307,7 @@ object LocalCache {
|
||||
it.addReply(note)
|
||||
}
|
||||
|
||||
refreshObservers()
|
||||
refreshObservers(note)
|
||||
}
|
||||
|
||||
@Suppress("UNUSED_PARAMETER")
|
||||
@@ -353,7 +351,7 @@ object LocalCache {
|
||||
recipient.addMessage(author, note)
|
||||
}
|
||||
|
||||
refreshObservers()
|
||||
refreshObservers(note)
|
||||
}
|
||||
|
||||
fun consume(event: DeletionEvent) {
|
||||
@@ -401,7 +399,7 @@ object LocalCache {
|
||||
}
|
||||
|
||||
if (deletedAtLeastOne) {
|
||||
live.invalidateData()
|
||||
// refreshObservers()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -427,7 +425,7 @@ object LocalCache {
|
||||
it.addBoost(note)
|
||||
}
|
||||
|
||||
refreshObservers()
|
||||
refreshObservers(note)
|
||||
}
|
||||
|
||||
fun consume(event: ReactionEvent) {
|
||||
@@ -465,6 +463,8 @@ object LocalCache {
|
||||
it.addReport(note)
|
||||
}
|
||||
}
|
||||
|
||||
refreshObservers(note)
|
||||
}
|
||||
|
||||
fun consume(event: ReportEvent, relay: Relay?) {
|
||||
@@ -495,6 +495,8 @@ object LocalCache {
|
||||
repliesTo.forEach {
|
||||
it.addReport(note)
|
||||
}
|
||||
|
||||
refreshObservers(note)
|
||||
}
|
||||
|
||||
fun consume(event: ChannelCreateEvent) {
|
||||
@@ -511,7 +513,7 @@ object LocalCache {
|
||||
oldChannel.addNote(note)
|
||||
note.loadEvent(event, author, emptyList())
|
||||
|
||||
refreshObservers()
|
||||
refreshObservers(note)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -531,7 +533,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)}")
|
||||
@@ -578,7 +580,7 @@ object LocalCache {
|
||||
it.addReply(note)
|
||||
}
|
||||
|
||||
refreshObservers()
|
||||
refreshObservers(note)
|
||||
}
|
||||
|
||||
@Suppress("UNUSED_PARAMETER")
|
||||
@@ -620,6 +622,8 @@ object LocalCache {
|
||||
mentions.forEach {
|
||||
it.addZap(zapRequest, note)
|
||||
}
|
||||
|
||||
refreshObservers(note)
|
||||
}
|
||||
|
||||
fun consume(event: LnZapRequestEvent) {
|
||||
@@ -643,6 +647,8 @@ object LocalCache {
|
||||
mentions.forEach {
|
||||
it.addZap(note, null)
|
||||
}
|
||||
|
||||
refreshObservers(note)
|
||||
}
|
||||
|
||||
fun findUsersStartingWith(username: String): List<User> {
|
||||
@@ -749,30 +755,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>>(setOf<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)
|
||||
|
@@ -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,37 @@ 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()))
|
||||
delay(delay)
|
||||
onUpdate(atomicSet.getAndSet(emptySet()))
|
||||
} finally {
|
||||
withContext(NonCancellable) {
|
||||
onlyOneInBlock.set(false)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -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>): Set<Note> {
|
||||
return collection
|
||||
.filter { it.idHex in channel.notes.keys && account.isAcceptable(it) }
|
||||
.toSet()
|
||||
}
|
||||
|
||||
override fun sort(collection: Set<Note>): List<Note> {
|
||||
return collection.sortedBy { it.createdAt() }.reversed()
|
||||
}
|
||||
}
|
||||
|
@@ -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>): Set<Note> {
|
||||
val myAccount = account
|
||||
val myUser = withUser
|
||||
|
||||
if (myAccount == null || myUser == null) return emptySet()
|
||||
|
||||
val messages = myAccount
|
||||
.userProfile()
|
||||
.privateChatrooms[myUser] ?: return emptySet()
|
||||
|
||||
return collection
|
||||
.filter { it in messages.roomMessages && account?.isAcceptable(it) == true }
|
||||
.toSet()
|
||||
}
|
||||
|
||||
override fun sort(collection: Set<Note>): List<Note> {
|
||||
return collection.sortedBy { it.createdAt() }.reversed()
|
||||
}
|
||||
}
|
||||
|
@@ -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>): Set<T>
|
||||
abstract fun sort(collection: Set<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.toSet() + newItemsToBeAdded
|
||||
sort(newList).take(1000)
|
||||
} else {
|
||||
oldList
|
||||
}
|
||||
}
|
||||
|
||||
Log.d("Time", "${this.javaClass.simpleName} Feed in $elapsed with ${feed.size} objects")
|
||||
return feed
|
||||
}
|
||||
}
|
||||
|
@@ -5,15 +5,26 @@ import com.vitorpamplona.amethyst.model.LocalCache
|
||||
import com.vitorpamplona.amethyst.model.Note
|
||||
import com.vitorpamplona.amethyst.service.model.*
|
||||
|
||||
object GlobalFeedFilter : FeedFilter<Note>() {
|
||||
object GlobalFeedFilter : AdditiveFeedFilter<Note>() {
|
||||
lateinit var account: Account
|
||||
|
||||
override fun feed(): List<Note> {
|
||||
val notes = innerApplyFilter(LocalCache.notes.values)
|
||||
val longFormNotes = innerApplyFilter(LocalCache.addressables.values)
|
||||
|
||||
return sort(notes + longFormNotes)
|
||||
}
|
||||
|
||||
override fun applyFilter(collection: Set<Note>): Set<Note> {
|
||||
return innerApplyFilter(collection)
|
||||
}
|
||||
|
||||
private fun innerApplyFilter(collection: Collection<Note>): Set<Note> {
|
||||
val followChannels = account.followingChannels
|
||||
val followUsers = account.followingKeySet()
|
||||
val now = System.currentTimeMillis() / 1000
|
||||
|
||||
val notes = LocalCache.notes.values
|
||||
return collection
|
||||
.asSequence()
|
||||
.filter {
|
||||
it.event is BaseTextNoteEvent && it.replyTo.isNullOrEmpty()
|
||||
@@ -30,29 +41,10 @@ object GlobalFeedFilter : FeedFilter<Note>() {
|
||||
// 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()
|
||||
}
|
||||
|
||||
val longFormNotes = LocalCache.addressables.values
|
||||
.asSequence()
|
||||
.filter {
|
||||
it.event is LongTextNoteEvent && it.replyTo.isNullOrEmpty()
|
||||
}
|
||||
.filter {
|
||||
val channel = it.channelHex()
|
||||
// does not show events already in the public chat list
|
||||
(channel == null || 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()!! <= now
|
||||
}
|
||||
.toList()
|
||||
|
||||
return (notes + longFormNotes)
|
||||
.sortedBy { it.createdAt() }
|
||||
.reversed()
|
||||
override fun sort(collection: Set<Note>): List<Note> {
|
||||
return collection.sortedBy { it.createdAt() }.reversed()
|
||||
}
|
||||
}
|
||||
|
@@ -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
|
||||
|
||||
override fun feed(): List<Note> {
|
||||
val myTag = tag ?: return emptyList()
|
||||
fun loadHashtag(account: Account, tag: String?) {
|
||||
this.account = account
|
||||
this.tag = tag
|
||||
}
|
||||
|
||||
return LocalCache.notes.values
|
||||
override fun feed(): List<Note> {
|
||||
return sort(innerApplyFilter(LocalCache.notes.values))
|
||||
}
|
||||
|
||||
override fun applyFilter(collection: Set<Note>): Set<Note> {
|
||||
return applyFilter(collection)
|
||||
}
|
||||
|
||||
private fun innerApplyFilter(collection: Collection<Note>): Set<Note> {
|
||||
val myTag = tag ?: return emptySet()
|
||||
|
||||
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()
|
||||
.toSet()
|
||||
}
|
||||
|
||||
fun loadHashtag(account: Account, tag: String?) {
|
||||
this.account = account
|
||||
this.tag = tag
|
||||
override fun sort(collection: Set<Note>): List<Note> {
|
||||
return collection.sortedBy { it.createdAt() }.reversed()
|
||||
}
|
||||
}
|
||||
|
@@ -6,15 +6,24 @@ import com.vitorpamplona.amethyst.model.Note
|
||||
import com.vitorpamplona.amethyst.service.model.PollNoteEvent
|
||||
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(innerApplyFilter(LocalCache.notes.values))
|
||||
}
|
||||
|
||||
override fun applyFilter(collection: Set<Note>): Set<Note> {
|
||||
return innerApplyFilter(collection)
|
||||
}
|
||||
|
||||
private fun innerApplyFilter(collection: Collection<Note>): Set<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.event is PollNoteEvent) &&
|
||||
(it.author?.pubkeyHex in followingKeySet || (it.event?.isTaggedHashes(followingTagSet) ?: false)) &&
|
||||
@@ -22,7 +31,10 @@ object HomeConversationsFeedFilter : FeedFilter<Note>() {
|
||||
it.author?.let { !account.isHidden(it) } ?: true &&
|
||||
!it.isNewThread()
|
||||
}
|
||||
.sortedBy { it.createdAt() }
|
||||
.reversed()
|
||||
.toSet()
|
||||
}
|
||||
|
||||
override fun sort(collection: Set<Note>): List<Note> {
|
||||
return collection.sortedBy { it.createdAt() }.reversed()
|
||||
}
|
||||
}
|
||||
|
@@ -8,34 +8,38 @@ import com.vitorpamplona.amethyst.service.model.PollNoteEvent
|
||||
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 = innerApplyFilter(LocalCache.notes.values)
|
||||
val longFormNotes = innerApplyFilter(LocalCache.addressables.values)
|
||||
|
||||
return sort(notes + longFormNotes)
|
||||
}
|
||||
|
||||
override fun applyFilter(collection: Set<Note>): Set<Note> {
|
||||
return innerApplyFilter(collection)
|
||||
}
|
||||
|
||||
private fun innerApplyFilter(collection: Collection<Note>): Set<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 PollNoteEvent) &&
|
||||
(it.event is TextNoteEvent || it.event is RepostEvent || it.event is LongTextNoteEvent || it.event is PollNoteEvent) &&
|
||||
(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.pubkeyHex) } ?: true &&
|
||||
it.isNewThread()
|
||||
}
|
||||
.toSet()
|
||||
}
|
||||
|
||||
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.pubkeyHex) } ?: true &&
|
||||
it.isNewThread()
|
||||
}
|
||||
|
||||
return (notes + longFormNotes)
|
||||
.sortedBy { it.createdAt() }
|
||||
.reversed()
|
||||
override fun sort(collection: Set<Note>): List<Note> {
|
||||
return collection.sortedBy { it.createdAt() }.reversed()
|
||||
}
|
||||
}
|
||||
|
@@ -6,14 +6,22 @@ import com.vitorpamplona.amethyst.model.Note
|
||||
import com.vitorpamplona.amethyst.model.User
|
||||
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(innerApplyFilter(LocalCache.notes.values))
|
||||
}
|
||||
|
||||
override fun applyFilter(collection: Set<Note>): Set<Note> {
|
||||
return innerApplyFilter(collection)
|
||||
}
|
||||
|
||||
private fun innerApplyFilter(collection: Collection<Note>): Set<Note> {
|
||||
val loggedInUser = account.userProfile()
|
||||
val loggedInUserHex = loggedInUser.pubkeyHex
|
||||
|
||||
return LocalCache.notes.values.filter {
|
||||
return collection.filter {
|
||||
it.event !is ChannelCreateEvent &&
|
||||
it.event !is ChannelMetadataEvent &&
|
||||
it.event !is LnZapRequestEvent &&
|
||||
@@ -23,9 +31,11 @@ object NotificationFeedFilter : FeedFilter<Note>() {
|
||||
it.event?.isTaggedUser(loggedInUserHex) ?: false &&
|
||||
(it.author == null || !account.isHidden(it.author!!.pubkeyHex)) &&
|
||||
tagsAnEventByUser(it, loggedInUser)
|
||||
}
|
||||
.sortedBy { it.createdAt() }
|
||||
.reversed()
|
||||
}.toSet()
|
||||
}
|
||||
|
||||
override fun sort(collection: Set<Note>): List<Note> {
|
||||
return collection.sortedBy { it.createdAt() }.reversed()
|
||||
}
|
||||
|
||||
fun tagsAnEventByUser(note: Note, author: User): Boolean {
|
||||
|
@@ -5,7 +5,6 @@ import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.lifecycle.ViewModel
|
||||
import com.vitorpamplona.amethyst.model.Account
|
||||
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.service.model.BadgeAwardEvent
|
||||
@@ -15,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
|
||||
@@ -30,7 +31,7 @@ import kotlin.time.measureTimedValue
|
||||
|
||||
class NotificationViewModel : CardFeedViewModel(NotificationFeedFilter)
|
||||
|
||||
open class CardFeedViewModel(val dataSource: FeedFilter<Note>) : ViewModel() {
|
||||
open class CardFeedViewModel(val localFilter: FeedFilter<Note>) : ViewModel() {
|
||||
private val _feedContent = MutableStateFlow<CardFeedState>(CardFeedState.Loading)
|
||||
val feedContent = _feedContent.asStateFlow()
|
||||
|
||||
@@ -46,28 +47,28 @@ open class CardFeedViewModel(val dataSource: FeedFilter<Note>) : 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
|
||||
val oldNotesState = _feedContent.value
|
||||
if (lastNotesCopy != null && oldNotesState is CardFeedState.Loaded) {
|
||||
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<Note>): List<Card> {
|
||||
private fun convertToCard(notes: Collection<Note>): List<Card> {
|
||||
val reactionsPerEvent = mutableMapOf<Note, MutableList<Note>>()
|
||||
notes
|
||||
.filter { it.event is ReactionEvent }
|
||||
@@ -159,7 +160,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 }
|
||||
@@ -172,6 +173,28 @@ open class CardFeedViewModel(val dataSource: FeedFilter<Note>) : ViewModel() {
|
||||
}
|
||||
}
|
||||
|
||||
fun refreshFromOldState(newItems: Set<Note>) {
|
||||
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
|
||||
@@ -181,13 +204,29 @@ open class CardFeedViewModel(val dataSource: FeedFilter<Note>) : ViewModel() {
|
||||
}
|
||||
Log.d("Time", "${this.javaClass.simpleName} Card update $elapsed")
|
||||
}
|
||||
private val bundlerInsert = BundledInsert<Set<Note>>(250, Dispatchers.IO)
|
||||
|
||||
fun invalidateData() {
|
||||
bundler.invalidate()
|
||||
}
|
||||
|
||||
private val cacheListener: (LocalCacheState) -> Unit = {
|
||||
invalidateData()
|
||||
@OptIn(ExperimentalTime::class)
|
||||
fun invalidateInsertData(newItems: Set<Note>) {
|
||||
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<Note>) -> Unit = { newNotes ->
|
||||
if (localFilter is AdditiveFeedFilter && _feedContent.value is CardFeedState.Loaded) {
|
||||
invalidateInsertData(newNotes)
|
||||
} else {
|
||||
// Refresh Everything
|
||||
invalidateData()
|
||||
}
|
||||
}
|
||||
|
||||
init {
|
||||
|
@@ -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 {
|
||||
|
@@ -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()
|
||||
}
|
||||
|
||||
|
@@ -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()
|
||||
}
|
||||
|
||||
|
Reference in New Issue
Block a user