From e5b68e774b6e6a59bc90b212873df77a9cf1026f Mon Sep 17 00:00:00 2001 From: Marcel Hibbe Date: Tue, 2 Jun 2026 15:33:14 +0200 Subject: [PATCH] fix(chat): stabilize unread marker positioning and unread popup behavior Opening chats with unread messages had inconsistent behavior in the Compose chat list: - the unread marker was sometimes not scrolled into view (or not centered), - the unread popup could appear during initial entry when the marker should be the primary indicator, - popup counting could reset incorrectly and show "1 new message" repeatedly, - near-bottom auto-scroll to newest messages regressed in some cases. This change updates ChatView state/effect handling to: - scroll to and center the unread marker once it is present in the loaded item list, - suppress the unread popup during marker-based initial entry, - keep popup counting stable for real incoming messages after entry, - restore auto-scroll to newest messages when the user is already near the bottom. AI-assistant: GitHub Copilot 1.9.1-251 (GPT-5.3-Codex) Signed-off-by: Marcel Hibbe --- .../com/nextcloud/talk/ui/chat/ChatView.kt | 44 +++++++++++++++++-- 1 file changed, 41 insertions(+), 3 deletions(-) diff --git a/app/src/main/java/com/nextcloud/talk/ui/chat/ChatView.kt b/app/src/main/java/com/nextcloud/talk/ui/chat/ChatView.kt index 8e765b5626..0d95aea8fe 100644 --- a/app/src/main/java/com/nextcloud/talk/ui/chat/ChatView.kt +++ b/app/src/main/java/com/nextcloud/talk/ui/chat/ChatView.kt @@ -50,6 +50,7 @@ import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.rememberUpdatedState import androidx.compose.runtime.setValue import androidx.compose.runtime.snapshotFlow +import androidx.compose.runtime.withFrameNanos import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.alpha @@ -155,6 +156,8 @@ fun ChatView( var isNewerBoundaryLoadArmed by remember(state.chatMode, state.highlightedMessageId) { mutableStateOf(state.chatMode == ChatViewModel.ChatMode.DEFAULT_MODE) } + var didScrollToUnreadMarker by remember { mutableStateOf(false) } + var hadUnreadMarker by remember { mutableStateOf(false) } val handleQuotedMessageClick: (Int) -> Unit = remember(coroutineScope, listState) { { messageId -> @@ -180,17 +183,52 @@ fun ChatView( } } - // Track newest message and show unread popup + // Scroll once to unread marker when it becomes available (it can appear after initial load). + LaunchedEffect(state.chatItems, isDefaultMode) { + if (!isDefaultMode) return@LaunchedEffect + + val markerIndex = state.chatItems.indexOfFirst { + it is ChatViewModel.ChatItem.UnreadMessagesMarkerItem + } + + if (markerIndex < 0) { + didScrollToUnreadMarker = false + return@LaunchedEffect + } + + if (didScrollToUnreadMarker) return@LaunchedEffect + + // While marker exists, keep popup hidden and reset unread popup count once. + showUnreadPopup.value = false + unreadCount = 0 + listState.scrollToItem(markerIndex) + withFrameNanos { } + listState.centerItemInViewportIfVisible(markerIndex) + didScrollToUnreadMarker = true + } + + // Track newest message and show unread popup. LaunchedEffect(state.chatItems) { if (state.chatItems.isEmpty()) return@LaunchedEffect if (!isDefaultMode) return@LaunchedEffect + val hasUnreadMarker = state.chatItems.any { it is ChatViewModel.ChatItem.UnreadMessagesMarkerItem } + if (hasUnreadMarker && !hadUnreadMarker) { + showUnreadPopup.value = false + unreadCount = 0 + } + hadUnreadMarker = hasUnreadMarker + val newestId = state.chatItems.firstNotNullOfOrNull { it.messageOrNull()?.id } val previousNewestId = lastNewestIdRef.value + if (previousNewestId == null) { + lastNewestIdRef.value = newestId + return@LaunchedEffect + } + val isNearBottom = listState.firstVisibleItemIndex <= 2 - val hasNewMessage = previousNewestId != null && - newestId != previousNewestId && + val hasNewMessage = newestId != previousNewestId && state.chatMode == ChatViewModel.ChatMode.DEFAULT_MODE if (hasNewMessage) {