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 c9d414105..f46e817bb 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 @@ -52,10 +52,8 @@ fun observeNote( EventFinderFilterAssemblerSubscription(note, accountViewModel) // Subscribe in the LocalCache for changes that arrive in the device - return note - .flow() - .metadata.stateFlow - .collectAsStateWithLifecycle() + val flow = remember(note) { note.flow().metadata.stateFlow } + return flow.collectAsStateWithLifecycle() } @Suppress("UNCHECKED_CAST") @@ -187,10 +185,8 @@ fun observeNoteReplies( EventFinderFilterAssemblerSubscription(note, accountViewModel) // Subscribe in the LocalCache for changes that arrive in the device - return note - .flow() - .replies.stateFlow - .collectAsStateWithLifecycle() + val flow = remember(note) { note.flow().replies.stateFlow } + return flow.collectAsStateWithLifecycle() } @OptIn(ExperimentalCoroutinesApi::class, FlowPreview::class) @@ -225,10 +221,8 @@ fun observeNoteReactions( EventFinderFilterAssemblerSubscription(note, accountViewModel) // Subscribe in the LocalCache for changes that arrive in the device - return note - .flow() - .reactions.stateFlow - .collectAsStateWithLifecycle() + val flow = remember(note) { note.flow().reactions.stateFlow } + return flow.collectAsStateWithLifecycle() } @OptIn(ExperimentalCoroutinesApi::class, FlowPreview::class) @@ -265,10 +259,8 @@ fun observeNoteZaps( EventFinderFilterAssemblerSubscription(note, accountViewModel) // Subscribe in the LocalCache for changes that arrive in the device - return note - .flow() - .zaps.stateFlow - .collectAsStateWithLifecycle() + val flow = remember(note) { note.flow().zaps.stateFlow } + return flow.collectAsStateWithLifecycle() } @Composable @@ -280,10 +272,8 @@ fun observeNoteReposts( EventFinderFilterAssemblerSubscription(note, accountViewModel) // Subscribe in the LocalCache for changes that arrive in the device - return note - .flow() - .boosts.stateFlow - .collectAsStateWithLifecycle() + val flow = remember(note) { note.flow().boosts.stateFlow } + return flow.collectAsStateWithLifecycle() } @OptIn(ExperimentalCoroutinesApi::class) @@ -365,11 +355,8 @@ fun observeNoteOts( EventFinderFilterAssemblerSubscription(note, accountViewModel) // Subscribe in the LocalCache for changes that arrive in the device - return note - .flow() - .ots - .stateFlow - .collectAsStateWithLifecycle() + val flow = remember(note) { note.flow().ots.stateFlow } + return flow.collectAsStateWithLifecycle() } @Composable @@ -381,11 +368,8 @@ fun observeNoteEdits( EventFinderFilterAssemblerSubscription(note, accountViewModel) // Subscribe in the LocalCache for changes that arrive in the device - return note - .flow() - .edits - .stateFlow - .collectAsStateWithLifecycle() + val flow = remember(note) { note.flow().edits.stateFlow } + return flow.collectAsStateWithLifecycle() } @Composable diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/BlockReportChecker.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/BlockReportChecker.kt index 847ec9007..f6e3fbe3a 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/BlockReportChecker.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/BlockReportChecker.kt @@ -59,7 +59,8 @@ fun WatchBlockAndReport( nav: INav, normalNote: @Composable (canPreview: Boolean) -> Unit, ) { - val isHidden by accountViewModel.createIsHiddenFlow(note).collectAsStateWithLifecycle() + val isHidden by remember(note) { accountViewModel.createIsHiddenFlow(note) } + .collectAsStateWithLifecycle() val showAnyway = remember { 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 7b2c010d7..70249d1dc 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 @@ -489,30 +489,31 @@ fun calculateBackgroundColor( ): MutableState { val defaultBackgroundColor = MaterialTheme.colorScheme.background val newItemColor = MaterialTheme.colorScheme.newItemBackgroundColor + + // Only fade in/out the "new item" highlight for items that track read state. + // Inner notes (reposts/quotes) pass routeForLastRead = null and reuse the parent color directly, + // so the LaunchedEffect would just park a coroutine for 5s per item during scroll. + val isNew = + remember(createdAt, routeForLastRead) { + routeForLastRead != null && accountViewModel.loadAndMarkAsRead(routeForLastRead, createdAt) + } + val bgColor = remember(createdAt) { mutableStateOf( - if (routeForLastRead != null) { - val isNew = accountViewModel.loadAndMarkAsRead(routeForLastRead, createdAt) - - if (isNew) { - if (parentBackgroundColor != null) { - newItemColor.compositeOver(parentBackgroundColor.value) - } else { - newItemColor.compositeOver(defaultBackgroundColor) - } - } else { - parentBackgroundColor?.value ?: defaultBackgroundColor.copy(alpha = 0f) - } + if (isNew) { + newItemColor.compositeOver(parentBackgroundColor?.value ?: defaultBackgroundColor) } else { - parentBackgroundColor?.value ?: defaultBackgroundColor.copy(alpha = 0f) + parentBackgroundColor?.value ?: Color.Transparent }, ) } - LaunchedEffect(createdAt) { - delay(5000) - bgColor.value = parentBackgroundColor?.value ?: defaultBackgroundColor.copy(alpha = 0f) + if (isNew) { + LaunchedEffect(createdAt) { + delay(5000) + bgColor.value = parentBackgroundColor?.value ?: Color.Transparent + } } return bgColor diff --git a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/compose/AsyncCachedState.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/compose/AsyncCachedState.kt index 0aaae33ac..752f899f5 100644 --- a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/compose/AsyncCachedState.kt +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/compose/AsyncCachedState.kt @@ -22,35 +22,47 @@ package com.vitorpamplona.amethyst.commons.compose import androidx.collection.LruCache import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.State -import androidx.compose.runtime.produceState +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +// On a cache hit, short-circuit with a remembered State and skip the produceState coroutine. +// Only on a miss do we launch the suspending update. @Composable fun produceCachedStateAsync( cache: AsyncCachedState, key: K, -): State = - @Suppress("ProduceStateDoesNotAssignValue") - produceState(initialValue = cache.cached(key), key1 = key) { - val newValue = cache.update(key) - if (newValue != value) { - value = newValue +): State { + val state = remember(key) { mutableStateOf(cache.cached(key)) } + if (state.value == null) { + LaunchedEffect(key) { + val newValue = cache.update(key) + if (state.value != newValue) { + state.value = newValue + } } } + return state +} @Composable fun produceCachedStateAsync( cache: AsyncCachedState, key: String, updateValue: K, -): State = - @Suppress("ProduceStateDoesNotAssignValue") - produceState(initialValue = cache.cached(updateValue), key1 = key) { - val newValue = cache.update(updateValue) - if (newValue != value) { - value = newValue +): State { + val state = remember(key) { mutableStateOf(cache.cached(updateValue)) } + if (state.value == null) { + LaunchedEffect(key) { + val newValue = cache.update(updateValue) + if (state.value != newValue) { + state.value = newValue + } } } + return state +} interface AsyncCachedState { fun cached(k: K): V? diff --git a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/compose/CachedState.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/compose/CachedState.kt index e00a83e84..b03110275 100644 --- a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/compose/CachedState.kt +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/compose/CachedState.kt @@ -22,35 +22,48 @@ package com.vitorpamplona.amethyst.commons.compose import androidx.collection.LruCache import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.State -import androidx.compose.runtime.produceState +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +// On a cache hit (the common case during scroll for things like Bech32 link previews), +// short-circuit with a remembered State and skip the produceState coroutine entirely. +// Only on a miss do we launch the suspending update. @Composable fun produceCachedState( cache: CachedState, key: K, -): State = - @Suppress("ProduceStateDoesNotAssignValue") - produceState(initialValue = cache.cached(key), key1 = key) { - val newValue = cache.update(key) - if (value != newValue) { - value = newValue +): State { + val state = remember(key) { mutableStateOf(cache.cached(key)) } + if (state.value == null) { + LaunchedEffect(key) { + val newValue = cache.update(key) + if (state.value != newValue) { + state.value = newValue + } } } + return state +} @Composable fun produceCachedState( cache: CachedState, key: String, updateValue: K, -): State = - @Suppress("ProduceStateDoesNotAssignValue") - produceState(initialValue = cache.cached(updateValue), key1 = key) { - val newValue = cache.update(updateValue) - if (value != newValue) { - value = newValue +): State { + val state = remember(key) { mutableStateOf(cache.cached(updateValue)) } + if (state.value == null) { + LaunchedEffect(key) { + val newValue = cache.update(updateValue) + if (state.value != newValue) { + state.value = newValue + } } } + return state +} interface CachedState { fun cached(k: K): V?