From 6a64ebcc5b5ba05f1d8a500d9228ad2cda25cd1f Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Sun, 27 Aug 2023 12:18:11 -0400 Subject: [PATCH] migrates to a double layer observer structure for the memory cache --- .../amethyst/model/LocalCache.kt | 4 +- .../com/vitorpamplona/amethyst/model/Note.kt | 129 +++++++++++------- .../com/vitorpamplona/amethyst/model/User.kt | 120 +++++++++------- .../amethyst/ui/note/ReactionsRow.kt | 29 ++-- 4 files changed, 165 insertions(+), 117 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 4e88c8726..602fb8a89 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/model/LocalCache.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/model/LocalCache.kt @@ -360,7 +360,7 @@ object LocalCache { if (event.createdAt > (note.createdAt() ?: 0)) { note.loadEvent(event, author, emptyList()) - author.liveSet?.statuses?.invalidateData() + author.liveSet?.innerStatuses?.invalidateData() refreshObservers(note) } @@ -650,7 +650,7 @@ object LocalCache { mentions.forEach { // doesn't add to reports, but triggers recounts - it.liveSet?.reports?.invalidateData() + it.liveSet?.innerReports?.invalidateData() } } diff --git a/app/src/main/java/com/vitorpamplona/amethyst/model/Note.kt b/app/src/main/java/com/vitorpamplona/amethyst/model/Note.kt index 78b690678..542a8d8c9 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/model/Note.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/model/Note.kt @@ -3,8 +3,8 @@ package com.vitorpamplona.amethyst.model import androidx.compose.runtime.Immutable import androidx.compose.runtime.Stable import androidx.lifecycle.LiveData +import androidx.lifecycle.MediatorLiveData import androidx.lifecycle.distinctUntilChanged -import androidx.lifecycle.map import com.vitorpamplona.amethyst.service.NostrSingleEventDataSource import com.vitorpamplona.amethyst.service.checkNotInMainThread import com.vitorpamplona.amethyst.service.firstFullCharOrEmoji @@ -22,6 +22,7 @@ import com.vitorpamplona.quartz.encoders.Nip19 import com.vitorpamplona.quartz.encoders.toNote import com.vitorpamplona.quartz.events.* import com.vitorpamplona.quartz.utils.TimeUtils +import kotlinx.collections.immutable.toImmutableList import kotlinx.coroutines.Dispatchers import java.math.BigDecimal import java.time.Instant @@ -126,7 +127,7 @@ open class Note(val idHex: String) { this.author = author this.replyTo = replyTo - liveSet?.metadata?.invalidateData() + liveSet?.innerMetadata?.invalidateData() } } @@ -174,21 +175,21 @@ open class Note(val idHex: String) { fun addReply(note: Note) { if (note !in replies) { replies = replies + note - liveSet?.replies?.invalidateData() + liveSet?.innerReplies?.invalidateData() } } fun removeReply(note: Note) { if (note in replies) { replies = replies - note - liveSet?.replies?.invalidateData() + liveSet?.innerReplies?.invalidateData() } } fun removeBoost(note: Note) { if (note in boosts) { boosts = boosts - note - liveSet?.boosts?.invalidateData() + liveSet?.innerBoosts?.invalidateData() } } @@ -211,11 +212,11 @@ open class Note(val idHex: String) { relays = listOf() lastReactionsDownloadTime = emptyMap() - liveSet?.replies?.invalidateData() - liveSet?.reactions?.invalidateData() - liveSet?.boosts?.invalidateData() - liveSet?.reports?.invalidateData() - liveSet?.zaps?.invalidateData() + liveSet?.innerReplies?.invalidateData() + liveSet?.innerReactions?.invalidateData() + liveSet?.innerBoosts?.invalidateData() + liveSet?.innerReports?.invalidateData() + liveSet?.innerZaps?.invalidateData() return toBeRemoved } @@ -234,7 +235,7 @@ open class Note(val idHex: String) { reactions = reactions + Pair(reaction, newList) } - liveSet?.reactions?.invalidateData() + liveSet?.innerReactions?.invalidateData() } } } @@ -246,7 +247,7 @@ open class Note(val idHex: String) { if (author in reports.keys && reports[author]?.contains(deleteNote) == true) { reports[author]?.let { reports = reports + Pair(author, it.minus(deleteNote)) - liveSet?.reports?.invalidateData() + liveSet?.innerReports?.invalidateData() } } } @@ -254,28 +255,28 @@ open class Note(val idHex: String) { fun removeZap(note: Note) { if (zaps[note] != null) { zaps = zaps.minus(note) - liveSet?.zaps?.invalidateData() + liveSet?.innerZaps?.invalidateData() } else if (zaps.containsValue(note)) { zaps = zaps.filterValues { it != note } - liveSet?.zaps?.invalidateData() + liveSet?.innerZaps?.invalidateData() } } fun removeZapPayment(note: Note) { if (zapPayments[note] != null) { zapPayments = zapPayments.minus(note) - liveSet?.zaps?.invalidateData() + liveSet?.innerZaps?.invalidateData() } else if (zapPayments.containsValue(note)) { val toRemove = zapPayments.filterValues { it == note } zapPayments = zapPayments.minus(toRemove.keys) - liveSet?.zaps?.invalidateData() + liveSet?.innerZaps?.invalidateData() } } fun addBoost(note: Note) { if (note !in boosts) { boosts = boosts + note - liveSet?.boosts?.invalidateData() + liveSet?.innerBoosts?.invalidateData() } } @@ -297,12 +298,12 @@ open class Note(val idHex: String) { if (zapRequest !in zaps.keys) { val inserted = innerAddZap(zapRequest, zap) if (inserted) { - liveSet?.zaps?.invalidateData() + liveSet?.innerZaps?.invalidateData() } } else if (zaps[zapRequest] == null) { val inserted = innerAddZap(zapRequest, zap) if (inserted) { - liveSet?.zaps?.invalidateData() + liveSet?.innerZaps?.invalidateData() } } } @@ -325,12 +326,12 @@ open class Note(val idHex: String) { if (zapPaymentRequest !in zapPayments.keys) { val inserted = innerAddZapPayment(zapPaymentRequest, zapPayment) if (inserted) { - liveSet?.zaps?.invalidateData() + liveSet?.innerZaps?.invalidateData() } } else if (zapPayments[zapPaymentRequest] == null) { val inserted = innerAddZapPayment(zapPaymentRequest, zapPayment) if (inserted) { - liveSet?.zaps?.invalidateData() + liveSet?.innerZaps?.invalidateData() } } } @@ -341,10 +342,10 @@ open class Note(val idHex: String) { if (reaction !in reactions.keys) { reactions = reactions + Pair(reaction, listOf(note)) - liveSet?.reactions?.invalidateData() + liveSet?.innerReactions?.invalidateData() } else if (reactions[reaction]?.contains(note) == false) { reactions = reactions + Pair(reaction, (reactions[reaction] ?: emptySet()) + note) - liveSet?.reactions?.invalidateData() + liveSet?.innerReactions?.invalidateData() } } @@ -353,17 +354,17 @@ open class Note(val idHex: String) { if (author !in reports.keys) { reports = reports + Pair(author, listOf(note)) - liveSet?.reports?.invalidateData() + liveSet?.innerReports?.invalidateData() } else if (reports[author]?.contains(note) == false) { reports = reports + Pair(author, (reports[author] ?: emptySet()) + note) - liveSet?.reports?.invalidateData() + liveSet?.innerReports?.invalidateData() } } fun addRelay(relay: Relay) { if (relay.url !in relays) { relays = relays + relay.url - liveSet?.relays?.invalidateData() + liveSet?.innerRelays?.invalidateData() } } @@ -653,40 +654,52 @@ open class Note(val idHex: String) { @Stable class NoteLiveSet(u: Note) { // Observers line up here. - val metadata: NoteLiveData = NoteLiveData(u) + val innerMetadata = NoteBundledRefresherLiveData(u) + val innerReactions = NoteBundledRefresherLiveData(u) + val innerBoosts = NoteBundledRefresherLiveData(u) + val innerReplies = NoteBundledRefresherLiveData(u) + val innerReports = NoteBundledRefresherLiveData(u) + val innerRelays = NoteBundledRefresherLiveData(u) + val innerZaps = NoteBundledRefresherLiveData(u) - val authorChanges = metadata.map { + val metadata = innerMetadata.map { it } + val reactions = innerReactions.map { it } + val boosts = innerBoosts.map { it } + val replies = innerReplies.map { it } + val reports = innerReports.map { it } + val relays = innerRelays.map { it } + val zaps = innerZaps.map { it } + + val authorChanges = innerMetadata.map { it.note.author } - val hasEvent = metadata.map { + + val hasEvent = innerMetadata.map { it.note.event != null }.distinctUntilChanged() - val reactions: NoteLiveData = NoteLiveData(u) - val boosts: NoteLiveData = NoteLiveData(u) - val replies: NoteLiveData = NoteLiveData(u) - val reports: NoteLiveData = NoteLiveData(u) - val relays: NoteLiveData = NoteLiveData(u) - val zaps: NoteLiveData = NoteLiveData(u) - - val hasReactions = zaps.combineWith(boosts, reactions) { zapState, boostState, reactionState -> + val hasReactions = innerZaps.combineWith(innerBoosts, innerReactions) { zapState, boostState, reactionState -> zapState?.note?.zaps?.isNotEmpty() ?: false || boostState?.note?.boosts?.isNotEmpty() ?: false || reactionState?.note?.reactions?.isNotEmpty() ?: false }.distinctUntilChanged() - val replyCount = replies.map { + val replyCount = innerReplies.map { it.note.replies.size }.distinctUntilChanged() - val reactionCount = reactions.map { + val reactionCount = innerReactions.map { it.note.reactions.values.sumOf { it.size } }.distinctUntilChanged() - val boostCount = boosts.map { + val boostCount = innerBoosts.map { it.note.boosts.size }.distinctUntilChanged() + val boostList = innerBoosts.map { + it.note.boosts.toImmutableList() + }.distinctUntilChanged() + fun isInUse(): Boolean { return metadata.hasObservers() || reactions.hasObservers() || @@ -694,21 +707,28 @@ class NoteLiveSet(u: Note) { replies.hasObservers() || reports.hasObservers() || relays.hasObservers() || - zaps.hasObservers() + zaps.hasObservers() || + authorChanges.hasObservers() || + hasEvent.hasObservers() || + hasReactions.hasObservers() || + replyCount.hasObservers() || + reactionCount.hasObservers() || + boostCount.hasObservers() || + boostList.hasObservers() } fun destroy() { - metadata.destroy() - reactions.destroy() - boosts.destroy() - replies.destroy() - reports.destroy() - relays.destroy() - zaps.destroy() + innerMetadata.destroy() + innerReactions.destroy() + innerBoosts.destroy() + innerReplies.destroy() + innerReports.destroy() + innerRelays.destroy() + innerZaps.destroy() } } -class NoteLiveData(val note: Note) : LiveData(NoteState(note)) { +class NoteBundledRefresherLiveData(val note: Note) : LiveData(NoteState(note)) { // Refreshes observers in batches. private val bundler = BundledUpdate(500, Dispatchers.IO) @@ -730,6 +750,17 @@ class NoteLiveData(val note: Note) : LiveData(NoteState(note)) { } } + fun map( + transform: (NoteState) -> Y + ): NoteLoadingLiveData { + val initialValue = this.value?.let { transform(it) } + val result = NoteLoadingLiveData(note, initialValue) + result.addSource(this) { x -> result.value = transform(x) } + return result + } +} + +class NoteLoadingLiveData(val note: Note, initialValue: Y?) : MediatorLiveData(initialValue) { override fun onActive() { super.onActive() if (note is AddressableNote) { diff --git a/app/src/main/java/com/vitorpamplona/amethyst/model/User.kt b/app/src/main/java/com/vitorpamplona/amethyst/model/User.kt index 1941864ad..85d608bae 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/model/User.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/model/User.kt @@ -3,6 +3,7 @@ package com.vitorpamplona.amethyst.model import androidx.compose.runtime.Immutable import androidx.compose.runtime.Stable import androidx.lifecycle.LiveData +import androidx.lifecycle.MediatorLiveData import androidx.lifecycle.distinctUntilChanged import androidx.lifecycle.map import com.vitorpamplona.amethyst.service.NostrSingleUserDataSource @@ -23,7 +24,6 @@ import com.vitorpamplona.quartz.events.MetadataEvent import com.vitorpamplona.quartz.events.ReportEvent import com.vitorpamplona.quartz.events.UserMetadata import com.vitorpamplona.quartz.events.toImmutableListOfLists -import com.vitorpamplona.quartz.utils.TimeUtils import kotlinx.collections.immutable.persistentSetOf import kotlinx.coroutines.Dispatchers import java.math.BigDecimal @@ -101,7 +101,7 @@ class User(val pubkeyHex: String) { if (event.id == latestBookmarkList?.id) return latestBookmarkList = event - liveSet?.bookmarks?.invalidateData() + liveSet?.innerBookmarks?.invalidateData() } fun updateContactList(event: ContactListEvent) { @@ -111,18 +111,18 @@ class User(val pubkeyHex: String) { latestContactList = event // Update following of the current user - liveSet?.follows?.invalidateData() + liveSet?.innerFollows?.invalidateData() // Update Followers of the past user list // Update Followers of the new contact list (oldContactListEvent)?.unverifiedFollowKeySet()?.forEach { - LocalCache.users[it]?.liveSet?.followers?.invalidateData() + LocalCache.users[it]?.liveSet?.innerFollowers?.invalidateData() } (latestContactList)?.unverifiedFollowKeySet()?.forEach { - LocalCache.users[it]?.liveSet?.followers?.invalidateData() + LocalCache.users[it]?.liveSet?.innerFollowers?.invalidateData() } - liveSet?.relays?.invalidateData() + liveSet?.innerRelays?.invalidateData() } fun addReport(note: Note) { @@ -130,10 +130,10 @@ class User(val pubkeyHex: String) { if (author !in reports.keys) { reports = reports + Pair(author, setOf(note)) - liveSet?.reports?.invalidateData() + liveSet?.innerReports?.invalidateData() } else if (reports[author]?.contains(note) == false) { reports = reports + Pair(author, (reports[author] ?: emptySet()) + note) - liveSet?.reports?.invalidateData() + liveSet?.innerReports?.invalidateData() } } @@ -143,7 +143,7 @@ class User(val pubkeyHex: String) { if (author in reports.keys && reports[author]?.contains(deleteNote) == true) { reports[author]?.let { reports = reports + Pair(author, it.minus(deleteNote)) - liveSet?.reports?.invalidateData() + liveSet?.innerReports?.invalidateData() } } } @@ -151,20 +151,20 @@ class User(val pubkeyHex: String) { fun addZap(zapRequest: Note, zap: Note?) { if (zapRequest !in zaps.keys) { zaps = zaps + Pair(zapRequest, zap) - liveSet?.zaps?.invalidateData() + liveSet?.innerZaps?.invalidateData() } else if (zapRequest in zaps.keys && zaps[zapRequest] == null) { zaps = zaps + Pair(zapRequest, zap) - liveSet?.zaps?.invalidateData() + liveSet?.innerZaps?.invalidateData() } } fun removeZap(zapRequestOrZapEvent: Note) { if (zapRequestOrZapEvent in zaps.keys) { zaps = zaps.minus(zapRequestOrZapEvent) - liveSet?.zaps?.invalidateData() + liveSet?.innerZaps?.invalidateData() } else if (zapRequestOrZapEvent in zaps.values) { zaps = zaps.filter { it.value != zapRequestOrZapEvent } - liveSet?.zaps?.invalidateData() + liveSet?.innerZaps?.invalidateData() } } @@ -218,7 +218,7 @@ class User(val pubkeyHex: String) { val privateChatroom = getOrCreatePrivateChatroom(room) if (msg !in privateChatroom.roomMessages) { privateChatroom.addMessageSync(msg) - liveSet?.messages?.invalidateData() + liveSet?.innerMessages?.invalidateData() } } @@ -226,7 +226,7 @@ class User(val pubkeyHex: String) { val privateChatroom = getOrCreatePrivateChatroom(user) if (msg !in privateChatroom.roomMessages) { privateChatroom.addMessageSync(msg) - liveSet?.messages?.invalidateData() + liveSet?.innerMessages?.invalidateData() } } @@ -240,7 +240,7 @@ class User(val pubkeyHex: String) { val privateChatroom = getOrCreatePrivateChatroom(user) if (msg in privateChatroom.roomMessages) { privateChatroom.removeMessageSync(msg) - liveSet?.messages?.invalidateData() + liveSet?.innerMessages?.invalidateData() } } @@ -255,7 +255,7 @@ class User(val pubkeyHex: String) { here.counter++ } - liveSet?.relayInfo?.invalidateData() + liveSet?.innerRelayInfo?.invalidateData() } fun updateUserInfo(newUserInfo: UserMetadata, latestMetadata: MetadataEvent) { @@ -280,7 +280,7 @@ class User(val pubkeyHex: String) { } } - liveSet?.metadata?.invalidateData() + liveSet?.innerMetadata?.invalidateData() } fun isFollowing(user: User): Boolean { @@ -382,54 +382,70 @@ class User(val pubkeyHex: String) { @Stable class UserLiveSet(u: User) { - // UI Observers line up here. - val follows: UserLiveData = UserLiveData(u) - val followers: UserLiveData = UserLiveData(u) - val reports: UserLiveData = UserLiveData(u) - val messages: UserLiveData = UserLiveData(u) - val relays: UserLiveData = UserLiveData(u) - val relayInfo: UserLiveData = UserLiveData(u) - val metadata: UserLiveData = UserLiveData(u) - val zaps: UserLiveData = UserLiveData(u) - val bookmarks: UserLiveData = UserLiveData(u) - val statuses: UserLiveData = UserLiveData(u) + val innerMetadata = UserBundledRefresherLiveData(u) - val profilePictureChanges = metadata.map { + // UI Observers line up here. + val innerFollows = UserBundledRefresherLiveData(u) + val innerFollowers = UserBundledRefresherLiveData(u) + val innerReports = UserBundledRefresherLiveData(u) + val innerMessages = UserBundledRefresherLiveData(u) + val innerRelays = UserBundledRefresherLiveData(u) + val innerRelayInfo = UserBundledRefresherLiveData(u) + val innerZaps = UserBundledRefresherLiveData(u) + val innerBookmarks = UserBundledRefresherLiveData(u) + val innerStatuses = UserBundledRefresherLiveData(u) + + // UI Observers line up here. + val metadata = innerMetadata.map { it } + val follows = innerFollows.map { it } + val followers = innerFollowers.map { it } + val reports = innerReports.map { it } + val messages = innerMessages.map { it } + val relays = innerRelays.map { it } + val relayInfo = innerRelayInfo.map { it } + val zaps = innerZaps.map { it } + val bookmarks = innerBookmarks.map { it } + val statuses = innerStatuses.map { it } + + val profilePictureChanges = innerMetadata.map { it.user.profilePicture() }.distinctUntilChanged() - val nip05Changes = metadata.map { + val nip05Changes = innerMetadata.map { it.user.nip05() }.distinctUntilChanged() - val userMetadataInfo = metadata.map { + val userMetadataInfo = innerMetadata.map { it.user.info }.distinctUntilChanged() fun isInUse(): Boolean { - return follows.hasObservers() || + return metadata.hasObservers() || + follows.hasObservers() || followers.hasObservers() || reports.hasObservers() || messages.hasObservers() || relays.hasObservers() || relayInfo.hasObservers() || - metadata.hasObservers() || zaps.hasObservers() || + bookmarks.hasObservers() || statuses.hasObservers() || - bookmarks.hasObservers() + profilePictureChanges.hasObservers() || + nip05Changes.hasObservers() || + userMetadataInfo.hasObservers() } fun destroy() { - follows.destroy() - followers.destroy() - reports.destroy() - messages.destroy() - relays.destroy() - relayInfo.destroy() - metadata.destroy() - zaps.destroy() - bookmarks.destroy() - statuses.destroy() + innerMetadata.destroy() + innerFollows.destroy() + innerFollowers.destroy() + innerReports.destroy() + innerMessages.destroy() + innerRelays.destroy() + innerRelayInfo.destroy() + innerZaps.destroy() + innerBookmarks.destroy() + innerStatuses.destroy() } } @@ -440,8 +456,7 @@ data class RelayInfo( var counter: Long ) - -class UserLiveData(val user: User) : LiveData(UserState(user)) { +class UserBundledRefresherLiveData(val user: User) : LiveData(UserState(user)) { // Refreshes observers in batches. private val bundler = BundledUpdate(500, Dispatchers.IO) @@ -462,6 +477,17 @@ class UserLiveData(val user: User) : LiveData(UserState(user)) { } } + fun map( + transform: (UserState) -> Y + ): UserLoadingLiveData { + val initialValue = this.value?.let { transform(it) } + val result = UserLoadingLiveData(user, initialValue) + result.addSource(this) { x -> result.value = transform(x) } + return result + } +} + +class UserLoadingLiveData(val user: User, initialValue: Y?) : MediatorLiveData(initialValue) { override fun onActive() { super.onActive() NostrSingleUserDataSource.add(user) diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/ReactionsRow.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/ReactionsRow.kt index a7fbccffc..8bbf1d157 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/ReactionsRow.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/ReactionsRow.kt @@ -319,15 +319,7 @@ fun RenderZapRaiser(baseNote: Note, zapraiserAmount: Long, details: Boolean, acc @Composable private fun WatchReactionsZapsBoostsAndDisplayIfExists(baseNote: Note, content: @Composable () -> Unit) { - val hasReactions by baseNote.live().zaps.combineWith( - liveData1 = baseNote.live().boosts, - liveData2 = baseNote.live().reactions, - block = { zapsState, boostsState, reactionsState -> - zapsState?.note?.zaps?.isNotEmpty() == true || - boostsState?.note?.boosts?.isNotEmpty() == true || - reactionsState?.note?.reactions?.isNotEmpty() == true - } - ).observeAsState( + val hasReactions by baseNote.live().hasReactions.observeAsState( baseNote.zaps.isNotEmpty() || baseNote.boosts.isNotEmpty() || baseNote.reactions.isNotEmpty() @@ -420,17 +412,16 @@ private fun WatchBoostsAndRenderGallery( nav: (String) -> Unit, accountViewModel: AccountViewModel ) { - val boostsState by baseNote.live().boosts.observeAsState() - val boostsEvents by remember(boostsState) { - derivedStateOf { baseNote.boosts.toImmutableList() } - } + val boostsEvents by baseNote.live().boostList.observeAsState() - if (boostsEvents.isNotEmpty()) { - RenderBoostGallery( - boostsEvents, - nav, - accountViewModel - ) + boostsEvents?.let { + if (it.isNotEmpty()) { + RenderBoostGallery( + it, + nav, + accountViewModel + ) + } } }