fix: message list paged lifecycle resulting in notifications being cleared (#3815)

This commit is contained in:
Mac DeCourcy 2025-11-25 21:17:15 -08:00 committed by GitHub
parent 142412e771
commit 07ade75f72
No known key found for this signature in database
GPG key ID: B5690EEEBB952194

View file

@ -32,6 +32,7 @@ import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.MutableState
import androidx.compose.runtime.derivedStateOf
@ -48,6 +49,8 @@ import androidx.compose.ui.hapticfeedback.HapticFeedback
import androidx.compose.ui.hapticfeedback.HapticFeedbackType
import androidx.compose.ui.platform.LocalHapticFeedback
import androidx.compose.ui.unit.dp
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.compose.LocalLifecycleOwner
import androidx.paging.LoadState
import androidx.paging.compose.LazyPagingItems
import androidx.paging.compose.itemKey
@ -295,6 +298,21 @@ private fun AutoScrollToBottomPaged(
}
}
private fun findFirstVisibleUnreadMessage(messages: LazyPagingItems<Message>, visibleIndex: Int): Message? {
val firstVisibleUnreadIndex =
(visibleIndex until messages.itemCount).firstOrNull { i ->
val msg = messages[i]
msg != null && !msg.read && !msg.fromLocal
}
return firstVisibleUnreadIndex?.let { messages[it] }
}
private fun findLastUnreadMessageIndex(messages: LazyPagingItems<Message>): Int? =
(0 until messages.itemCount).lastOrNull { i ->
val msg = messages[i]
msg != null && !msg.read && !msg.fromLocal
}
@OptIn(FlowPreview::class)
@Composable
private fun UpdateUnreadCountPaged(
@ -303,6 +321,24 @@ private fun UpdateUnreadCountPaged(
onUnreadChange: (Long, Long) -> Unit,
) {
val currentOnUnreadChange by rememberUpdatedState(onUnreadChange)
val lifecycleOwner = LocalLifecycleOwner.current
var isResumed by remember {
mutableStateOf(lifecycleOwner.lifecycle.currentState.isAtLeast(Lifecycle.State.RESUMED))
}
// Track lifecycle state changes
DisposableEffect(lifecycleOwner) {
val observer =
androidx.lifecycle.LifecycleEventObserver { _, event ->
when (event) {
androidx.lifecycle.Lifecycle.Event.ON_RESUME -> isResumed = true
androidx.lifecycle.Lifecycle.Event.ON_PAUSE -> isResumed = false
else -> {}
}
}
lifecycleOwner.lifecycle.addObserver(observer)
onDispose { lifecycleOwner.lifecycle.removeObserver(observer) }
}
// Track remote message count to restart effect when remote messages change
// This fixes race condition when sending/receiving messages during debounce period
@ -318,42 +354,27 @@ private fun UpdateUnreadCountPaged(
// Mark messages as read after debounce period
// Handles both scrolling cases and when all unread messages are visible without scrolling
LaunchedEffect(remoteMessageCount, listState) {
// Effect restarts when isResumed changes, so returning from background will restart the debounce
LaunchedEffect(remoteMessageCount, listState, isResumed) {
snapshotFlow {
// Emit when scroll stops OR when at initial position (covers no-scroll case)
if (listState.isScrollInProgress) {
null // Scrolling in progress, don't emit
// Include isResumed in the snapshot so lifecycle changes trigger new emissions
if (listState.isScrollInProgress || !isResumed) {
null // Scrolling in progress or not resumed, don't emit
} else {
listState.firstVisibleItemIndex // Emit current position when not scrolling
listState.firstVisibleItemIndex // Emit current position when not scrolling and resumed
}
}
.debounce(timeoutMillis = UnreadUiDefaults.SCROLL_DEBOUNCE_MILLIS)
.collectLatest { index ->
if (index != null) {
// Find the last (oldest in timeline, highest index) unread message in loaded items
val lastUnreadIndex =
(0 until messages.itemCount).lastOrNull { i ->
val msg = messages[i]
msg != null && !msg.read && !msg.fromLocal
}
val lastUnreadIndex = findLastUnreadMessageIndex(messages)
// If we're at/past the oldest unread, mark the first visible unread message
// Since newer messages have HIGHER timestamps, marking a newer message's timestamp
// will batch-mark all older messages via SQL: WHERE received_time <= timestamp
if (lastUnreadIndex != null && index <= lastUnreadIndex) {
// Find the first (newest in timeline, lowest index) visible unread message
val firstVisibleUnreadIndex =
(index until messages.itemCount).firstOrNull { i ->
val msg = messages[i]
msg != null && !msg.read && !msg.fromLocal
}
if (firstVisibleUnreadIndex != null) {
val firstVisibleUnread = messages[firstVisibleUnreadIndex]
if (firstVisibleUnread != null) {
currentOnUnreadChange(firstVisibleUnread.uuid, firstVisibleUnread.receivedTime)
}
}
val firstVisibleUnread = findFirstVisibleUnreadMessage(messages, index)
firstVisibleUnread?.let { currentOnUnreadChange(it.uuid, it.receivedTime) }
}
}
}