Removing the Duplicated Observer infrastructure on User: It wasn't updating observers correctly due to the batch update we use.

This commit is contained in:
Vitor Pamplona
2023-02-14 11:01:08 -05:00
parent 8e897762dd
commit c3aa37534e
6 changed files with 62 additions and 119 deletions

View File

@ -20,6 +20,7 @@ import java.util.Locale
import java.util.concurrent.atomic.AtomicBoolean import java.util.concurrent.atomic.AtomicBoolean
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.Job import kotlinx.coroutines.Job
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
@ -391,13 +392,13 @@ class Account(
} }
init { init {
userProfile().subscribe(object: User.Listener() { userProfile().liveRelays.observeForever {
override fun onRelayChange() { GlobalScope.launch(Dispatchers.IO) {
Client.disconnect() Client.disconnect()
Client.connect(activeRelays() ?: convertLocalRelays()) Client.connect(activeRelays() ?: convertLocalRelays())
RelayPool.requestAndWatch() RelayPool.requestAndWatch()
} }
}) }
} }
// Observers line up here. // Observers line up here.

View File

@ -431,7 +431,7 @@ object LocalCache {
return return
} }
Log.d("ZP", "New ZapEvent ${event.content} (${notes.size},${users.size}) ${note.author?.toBestDisplayName()} ${formattedDateTime(event.createdAt)}") //Log.d("ZP", "New ZapEvent ${event.content} (${notes.size},${users.size}) ${note.author?.toBestDisplayName()} ${formattedDateTime(event.createdAt)}")
// Adds notifications to users. // Adds notifications to users.
mentions.forEach { mentions.forEach {
@ -461,7 +461,7 @@ object LocalCache {
note.loadEvent(event, author, mentions, repliesTo) note.loadEvent(event, author, mentions, repliesTo)
Log.d("ZP", "New Zap Request ${event.content} (${notes.size},${users.size}) ${note.author?.toBestDisplayName()} ${formattedDateTime(event.createdAt)}") //Log.d("ZP", "New Zap Request ${event.content} (${notes.size},${users.size}) ${note.author?.toBestDisplayName()} ${formattedDateTime(event.createdAt)}")
// Adds notifications to users. // Adds notifications to users.
mentions.forEach { mentions.forEach {

View File

@ -2,6 +2,7 @@ package com.vitorpamplona.amethyst.model
import androidx.lifecycle.LiveData import androidx.lifecycle.LiveData
import com.vitorpamplona.amethyst.lnurl.LnInvoiceUtil import com.vitorpamplona.amethyst.lnurl.LnInvoiceUtil
import com.vitorpamplona.amethyst.service.NostrHomeDataSource
import com.vitorpamplona.amethyst.service.NostrSingleUserDataSource import com.vitorpamplona.amethyst.service.NostrSingleUserDataSource
import com.vitorpamplona.amethyst.service.model.LnZapEvent import com.vitorpamplona.amethyst.service.model.LnZapEvent
import com.vitorpamplona.amethyst.service.model.ReportEvent import com.vitorpamplona.amethyst.service.model.ReportEvent
@ -89,29 +90,14 @@ class User(val pubkeyHex: String) {
liveFollows.invalidateData() liveFollows.invalidateData()
user.liveFollows.invalidateData() user.liveFollows.invalidateData()
updateSubscribers {
it.onFollowsChange()
}
user.updateSubscribers {
it.onFollowsChange()
}
} }
fun unfollow(user: User) { fun unfollow(user: User) {
follows = follows - user follows = follows - user
user.followers = user.followers - this user.followers = user.followers - this
liveFollows.invalidateData() liveFollows.invalidateData()
user.liveFollows.invalidateData() user.liveFollows.invalidateData()
updateSubscribers {
it.onFollowsChange()
}
user.updateSubscribers {
it.onFollowsChange()
}
} }
fun follow(users: Set<User>, followedAt: Long) { fun follow(users: Set<User>, followedAt: Long) {
@ -119,42 +105,31 @@ class User(val pubkeyHex: String) {
users.forEach { users.forEach {
it.followers = it.followers + this it.followers = it.followers + this
it.liveFollows.invalidateData() it.liveFollows.invalidateData()
it.updateSubscribers {
it.onFollowsChange()
}
} }
liveFollows.invalidateData() liveFollows.invalidateData()
updateSubscribers {
it.onFollowsChange()
}
} }
fun unfollow(users: Set<User>) { fun unfollow(users: Set<User>) {
follows = follows - users follows = follows - users
users.forEach { users.forEach {
it.followers = it.followers - this it.followers = it.followers - this
it.liveFollows.invalidateData() it.liveFollows.invalidateData()
it.updateSubscribers {
it.onFollowsChange()
}
} }
liveFollows.invalidateData() liveFollows.invalidateData()
updateSubscribers {
it.onFollowsChange()
}
} }
fun addTaggedPost(note: Note) { fun addTaggedPost(note: Note) {
if (note !in taggedPosts) { if (note !in taggedPosts) {
taggedPosts = taggedPosts + note taggedPosts = taggedPosts + note
updateSubscribers { it.onNewTaggedPosts() } // No need for Listener yet
} }
} }
fun addNote(note: Note) { fun addNote(note: Note) {
if (note !in notes) { if (note !in notes) {
notes = notes + note notes = notes + note
updateSubscribers { it.onNewNotes() } // No need for Listener yet
} }
} }
@ -216,7 +191,6 @@ class User(val pubkeyHex: String) {
if (msg !in channel) { if (msg !in channel) {
messages = messages + Pair(user, channel + msg) messages = messages + Pair(user, channel + msg)
liveMessages.invalidateData() liveMessages.invalidateData()
updateSubscribers { it.onNewMessage() }
} }
} }
@ -237,7 +211,6 @@ class User(val pubkeyHex: String) {
here.counter++ here.counter++
} }
updateSubscribers { it.onNewRelayInfo() }
liveRelayInfo.invalidateData() liveRelayInfo.invalidateData()
} }
@ -251,17 +224,11 @@ class User(val pubkeyHex: String) {
updatedFollowsAt = updateAt updatedFollowsAt = updateAt
} }
data class RelayMetadata(val read: Boolean, val write: Boolean, val activeTypes: Set<FeedType>)
fun updateRelays(relayUse: Map<String, ContactListEvent.ReadWrite>) { fun updateRelays(relayUse: Map<String, ContactListEvent.ReadWrite>) {
if (relays != relayUse) { if (relays != relayUse) {
relays = relayUse relays = relayUse
listeners.forEach { liveRelays.invalidateData()
it.onRelayChange()
}
} }
liveRelays.invalidateData()
} }
fun updateUserInfo(newUserInfo: UserMetadata, updateAt: Long) { fun updateUserInfo(newUserInfo: UserMetadata, updateAt: Long) {
@ -287,45 +254,6 @@ class User(val pubkeyHex: String) {
} != null } != null
} }
// Model Observers
private var listeners = setOf<Listener>()
fun subscribe(listener: Listener) {
listeners = listeners.plus(listener)
}
fun unsubscribe(listener: Listener) {
listeners = listeners.minus(listener)
}
abstract class Listener {
open fun onRelayChange() = Unit
open fun onFollowsChange() = Unit
open fun onNewTaggedPosts() = Unit
open fun onNewNotes() = Unit
open fun onNewMessage() = Unit
open fun onNewRelayInfo() = Unit
open fun onNewReports() = Unit
}
// Refreshes observers in batches.
var modelHandlerWaiting = AtomicBoolean()
@Synchronized
fun updateSubscribers(on: (Listener) -> Unit) {
if (modelHandlerWaiting.getAndSet(true)) return
modelHandlerWaiting.set(true)
val scope = CoroutineScope(Job() + Dispatchers.Default)
scope.launch {
delay(100)
listeners.forEach {
on(it)
}
modelHandlerWaiting.set(false)
}
}
// UI Observers line up here. // UI Observers line up here.
val liveFollows: UserLiveData = UserLiveData(this) val liveFollows: UserLiveData = UserLiveData(this)
val liveReports: UserLiveData = UserLiveData(this) val liveReports: UserLiveData = UserLiveData(this)

View File

@ -2,11 +2,16 @@ package com.vitorpamplona.amethyst.service
import com.vitorpamplona.amethyst.model.Account import com.vitorpamplona.amethyst.model.Account
import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.model.LocalCache
import com.vitorpamplona.amethyst.model.LocalCacheState
import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.model.Note
import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.model.User
import com.vitorpamplona.amethyst.model.UserState
import com.vitorpamplona.amethyst.service.model.RepostEvent import com.vitorpamplona.amethyst.service.model.RepostEvent
import com.vitorpamplona.amethyst.service.relays.FeedType import com.vitorpamplona.amethyst.service.relays.FeedType
import com.vitorpamplona.amethyst.service.relays.TypedFilter import com.vitorpamplona.amethyst.service.relays.TypedFilter
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch
import nostr.postr.JsonFilter import nostr.postr.JsonFilter
import nostr.postr.events.TextNoteEvent import nostr.postr.events.TextNoteEvent
import nostr.postr.toHex import nostr.postr.toHex
@ -14,22 +19,26 @@ import nostr.postr.toHex
object NostrHomeDataSource: NostrDataSource<Note>("HomeFeed") { object NostrHomeDataSource: NostrDataSource<Note>("HomeFeed") {
lateinit var account: Account lateinit var account: Account
object cacheListener: User.Listener() { private val cacheListener: (UserState) -> Unit = {
override fun onFollowsChange() { resetFilters()
resetFilters()
}
} }
override fun start() { override fun start() {
if (this::account.isInitialized) if (this::account.isInitialized) {
account.userProfile().subscribe(cacheListener) GlobalScope.launch(Dispatchers.Main) {
account.userProfile().liveFollows.observeForever(cacheListener)
}
}
super.start() super.start()
} }
override fun stop() { override fun stop() {
super.stop() super.stop()
if (this::account.isInitialized) if (this::account.isInitialized) {
account.userProfile().unsubscribe(cacheListener) GlobalScope.launch(Dispatchers.Main) {
account.userProfile().liveFollows.removeObserver(cacheListener)
}
}
} }
fun createFollowAccountsFilter(): TypedFilter { fun createFollowAccountsFilter(): TypedFilter {

View File

@ -78,34 +78,38 @@ abstract class FeedViewModel(val dataSource: NostrDataSource<Note>): ViewModel()
} }
fun refresh() { fun refresh() {
viewModelScope.launch(Dispatchers.Default) { val scope = CoroutineScope(Job() + Dispatchers.Default)
val notes = newListFromDataSource() scope.launch {
refreshSuspended()
}
}
val oldNotesState = feedContent.value fun refreshSuspended() {
if (oldNotesState is FeedState.Loaded) { val notes = newListFromDataSource()
if (notes != oldNotesState.feed) {
withContext(Dispatchers.Main) { val oldNotesState = feedContent.value
updateFeed(notes) if (oldNotesState is FeedState.Loaded) {
} if (notes != oldNotesState.feed) {
} updateFeed(notes)
} else {
withContext(Dispatchers.Main) {
updateFeed(notes)
}
} }
} else {
updateFeed(notes)
} }
} }
private fun updateFeed(notes: List<Note>) { private fun updateFeed(notes: List<Note>) {
val currentState = feedContent.value val scope = CoroutineScope(Job() + Dispatchers.Main)
scope.launch {
val currentState = feedContent.value
if (notes.isEmpty()) { if (notes.isEmpty()) {
_feedContent.update { FeedState.Empty } _feedContent.update { FeedState.Empty }
} else if (currentState is FeedState.Loaded) { } else if (currentState is FeedState.Loaded) {
// updates the current list // updates the current list
currentState.feed.value = notes currentState.feed.value = notes
} else { } else {
_feedContent.update { FeedState.Loaded(mutableStateOf(notes)) } _feedContent.update { FeedState.Loaded(mutableStateOf(notes)) }
}
} }
} }

View File

@ -20,6 +20,8 @@ import androidx.navigation.NavController
import com.google.accompanist.swiperefresh.SwipeRefresh import com.google.accompanist.swiperefresh.SwipeRefresh
import com.google.accompanist.swiperefresh.rememberSwipeRefreshState import com.google.accompanist.swiperefresh.rememberSwipeRefreshState
import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.model.User
import com.vitorpamplona.amethyst.model.UserState
import com.vitorpamplona.amethyst.service.NostrHomeDataSource
import com.vitorpamplona.amethyst.ui.actions.NewRelayListView import com.vitorpamplona.amethyst.ui.actions.NewRelayListView
import com.vitorpamplona.amethyst.ui.note.RelayCompose import com.vitorpamplona.amethyst.ui.note.RelayCompose
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
@ -59,21 +61,20 @@ class RelayFeedViewModel: ViewModel() {
} }
} }
inner class CacheListener: User.Listener() { val listener: (UserState) -> Unit = {
override fun onNewRelayInfo() { invalidateData() } invalidateData()
override fun onRelayChange() { invalidateData() }
} }
val listener = CacheListener()
fun subscribeTo(user: User) { fun subscribeTo(user: User) {
currentUser = user currentUser = user
user.subscribe(listener) user.liveRelays.observeForever(listener)
user.liveRelayInfo.observeForever(listener)
invalidateData() invalidateData()
} }
fun unsubscribeTo(user: User) { fun unsubscribeTo(user: User) {
user.unsubscribe(listener) user.liveRelays.removeObserver(listener)
user.liveRelayInfo.removeObserver(listener)
currentUser = null currentUser = null
} }