diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/event/EventObservers.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/event/EventObservers.kt index f46e817bbc..1f7a8e31e7 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/event/EventObservers.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/event/EventObservers.kt @@ -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 { +): State?> { // 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?>(initialValue = null, note) { + note + .flow() + .edits + .stateFlow + .mapLatest { LocalCache.findLatestModificationForNote(note) } + .distinctUntilChanged() + .flowOn(Dispatchers.IO) + .collect { value = it } + } } @Composable diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/NoteCompose.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/NoteCompose.kt index 70249d1dc3..e33451c4c4 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/NoteCompose.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/NoteCompose.kt @@ -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)?.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) } } } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/AccountViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/AccountViewModel.kt index a45ab31860..b5983ccc6e 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/AccountViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/AccountViewModel.kt @@ -1310,11 +1310,6 @@ class AccountViewModel( fun cachedModificationEventsForNote(note: Note) = LocalCache.cachedModificationEventsForNote(note) - suspend fun findModificationEventsForNote(note: Note): List = - withContext(Dispatchers.IO) { - LocalCache.findLatestModificationForNote(note) - } - fun checkGetOrCreatePublicChatChannel(key: HexKey): PublicChatChannel = LocalCache.getOrCreatePublicChatChannel(key) fun checkGetOrCreateLiveActivityChannel(key: Address): LiveActivitiesChannel = LocalCache.getOrCreateLiveChannel(key)