mirror of
https://github.com/vitorpamplona/amethyst.git
synced 2026-08-09 16:14:40 +00:00
perf(observeEdits): filter modification updates at the flow level
`observeEdits` in NoteCompose previously listened to the raw
`note.flow().edits.stateFlow` and re-ran `findModificationEventsForNote`
(an IO scan) inside a `LaunchedEffect` on every emission — even when
the emission didn't change the actual list of modifications. That
mutated `editState` repeatedly with the same value during scroll on
any active TextNote.
Add `observeNoteModifications` in EventObservers.kt: it does the IO
resolution via `mapLatest { LocalCache.findLatestModificationForNote(note) }`
on Dispatchers.IO, then `distinctUntilChanged()` — so the State only
updates (and the consumer's LaunchedEffect only re-keys) when the
modification list truly changes. The State is `null` until the first
IO resolution completes; consumers treat that as "still loading" and
don't flip the UI to "no edits" prematurely.
Drop the now-unused `observeNoteEdits` and the
`AccountViewModel.findModificationEventsForNote` wrapper.
This commit is contained in:
+19
-5
@@ -22,8 +22,10 @@ package com.vitorpamplona.amethyst.service.relayClient.reqCommand.event
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.State
|
||||
import androidx.compose.runtime.produceState
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import com.vitorpamplona.amethyst.model.LocalCache
|
||||
import com.vitorpamplona.amethyst.model.Note
|
||||
import com.vitorpamplona.amethyst.model.NoteState
|
||||
import com.vitorpamplona.amethyst.model.User
|
||||
@@ -359,17 +361,29 @@ fun observeNoteOts(
|
||||
return flow.collectAsStateWithLifecycle()
|
||||
}
|
||||
|
||||
// Resolves the actual modification list off the main thread and filters identical results,
|
||||
// so the caller's LaunchedEffect only fires when the list of edits truly changes.
|
||||
// Returns `null` until the first IO resolution completes — callers should treat that as
|
||||
// "still loading" and not flip their UI to "no edits".
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
@Composable
|
||||
fun observeNoteEdits(
|
||||
fun observeNoteModifications(
|
||||
note: Note,
|
||||
accountViewModel: AccountViewModel,
|
||||
): State<NoteState?> {
|
||||
): State<List<Note>?> {
|
||||
// Subscribe in the relay for changes in this note.
|
||||
EventFinderFilterAssemblerSubscription(note, accountViewModel)
|
||||
|
||||
// Subscribe in the LocalCache for changes that arrive in the device
|
||||
val flow = remember(note) { note.flow().edits.stateFlow }
|
||||
return flow.collectAsStateWithLifecycle()
|
||||
return produceState<List<Note>?>(initialValue = null, note) {
|
||||
note
|
||||
.flow()
|
||||
.edits
|
||||
.stateFlow
|
||||
.mapLatest { LocalCache.findLatestModificationForNote(note) }
|
||||
.distinctUntilChanged()
|
||||
.flowOn(Dispatchers.IO)
|
||||
.collect { value = it }
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
|
||||
@@ -66,8 +66,8 @@ import com.vitorpamplona.amethyst.model.LocalCache
|
||||
import com.vitorpamplona.amethyst.model.Note
|
||||
import com.vitorpamplona.amethyst.service.relayClient.reqCommand.channel.observeChannelPicture
|
||||
import com.vitorpamplona.amethyst.service.relayClient.reqCommand.event.observeCommunityApprovalNeedStatus
|
||||
import com.vitorpamplona.amethyst.service.relayClient.reqCommand.event.observeNoteEdits
|
||||
import com.vitorpamplona.amethyst.service.relayClient.reqCommand.event.observeNoteEvent
|
||||
import com.vitorpamplona.amethyst.service.relayClient.reqCommand.event.observeNoteModifications
|
||||
import com.vitorpamplona.amethyst.ui.components.ClickableBox
|
||||
import com.vitorpamplona.amethyst.ui.components.GenericLoadable
|
||||
import com.vitorpamplona.amethyst.ui.components.RobohashFallbackAsyncImage
|
||||
@@ -1783,23 +1783,25 @@ fun observeEdits(
|
||||
)
|
||||
}
|
||||
|
||||
val updatedNote by observeNoteEdits(baseNote, accountViewModel)
|
||||
// Upstream resolves on IO and `distinctUntilChanged`s, so this LaunchedEffect only
|
||||
// fires when the actual modification list changes — no more recomputing + reassigning
|
||||
// editState on every unrelated emission of the edits flow.
|
||||
val modifications by observeNoteModifications(baseNote, accountViewModel)
|
||||
|
||||
LaunchedEffect(key1 = updatedNote) {
|
||||
updatedNote?.note?.let {
|
||||
val newModifications = accountViewModel.findModificationEventsForNote(it)
|
||||
if (newModifications.isEmpty()) {
|
||||
if (editState.value !is GenericLoadable.Empty) {
|
||||
editState.value = GenericLoadable.Empty()
|
||||
}
|
||||
LaunchedEffect(modifications) {
|
||||
val mods = modifications ?: return@LaunchedEffect
|
||||
if (mods.isEmpty()) {
|
||||
if (editState.value !is GenericLoadable.Empty) {
|
||||
editState.value = GenericLoadable.Empty()
|
||||
}
|
||||
} else {
|
||||
val current = editState.value
|
||||
if (current is GenericLoadable.Loaded) {
|
||||
current.loaded.updateModifications(mods)
|
||||
} else {
|
||||
if (editState.value is GenericLoadable.Loaded) {
|
||||
(editState.value as? GenericLoadable.Loaded<EditState>)?.loaded?.updateModifications(newModifications)
|
||||
} else {
|
||||
val state = EditState()
|
||||
state.updateModifications(newModifications)
|
||||
editState.value = GenericLoadable.Loaded(state)
|
||||
}
|
||||
val state = EditState()
|
||||
state.updateModifications(mods)
|
||||
editState.value = GenericLoadable.Loaded(state)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
-5
@@ -1310,11 +1310,6 @@ class AccountViewModel(
|
||||
|
||||
fun cachedModificationEventsForNote(note: Note) = LocalCache.cachedModificationEventsForNote(note)
|
||||
|
||||
suspend fun findModificationEventsForNote(note: Note): List<Note> =
|
||||
withContext(Dispatchers.IO) {
|
||||
LocalCache.findLatestModificationForNote(note)
|
||||
}
|
||||
|
||||
fun checkGetOrCreatePublicChatChannel(key: HexKey): PublicChatChannel = LocalCache.getOrCreatePublicChatChannel(key)
|
||||
|
||||
fun checkGetOrCreateLiveActivityChannel(key: Address): LiveActivitiesChannel = LocalCache.getOrCreateLiveChannel(key)
|
||||
|
||||
Reference in New Issue
Block a user