diff --git a/app/src/main/java/com/nextcloud/talk/chat/ChatActivity.kt b/app/src/main/java/com/nextcloud/talk/chat/ChatActivity.kt index ba39423bc7..abe223d0f2 100644 --- a/app/src/main/java/com/nextcloud/talk/chat/ChatActivity.kt +++ b/app/src/main/java/com/nextcloud/talk/chat/ChatActivity.kt @@ -62,11 +62,19 @@ import androidx.compose.runtime.SideEffect import androidx.compose.runtime.collectAsState import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableIntStateOf import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.produceState import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.setValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.platform.ComposeView +import androidx.compose.ui.platform.LocalDensity +import androidx.compose.ui.unit.dp +import androidx.coordinatorlayout.widget.CoordinatorLayout +import androidx.core.content.ContextCompat import androidx.core.content.FileProvider import androidx.core.content.PermissionChecker import androidx.core.content.PermissionChecker.PERMISSION_GRANTED @@ -287,6 +295,7 @@ class ChatActivity : private val chatEmptyStateType = mutableStateOf(null) private val upcomingEventUiState = mutableStateOf(ChatViewModel.UpcomingEventUIState.None) + private val overflowContainerHeightPx = mutableIntStateOf(0) private val startSelectContactForResult = registerForActivityResult( ActivityResultContracts @@ -510,6 +519,10 @@ class ChatActivity : setUpcomingEventContent() + binding.chatOverflowContainer.viewTreeObserver.addOnGlobalLayoutListener { + overflowContainerHeightPx.intValue = binding.chatOverflowContainer.height + } + lifecycleScope.launch { currentUserProvider.getCurrentUser() .onSuccess { user -> @@ -787,6 +800,9 @@ class ChatActivity : openWhenDownloadState.value = (downloadingFileState.value.intersect(visibleIds).isNotEmpty()) } + val overflowHeightDp = with(LocalDensity.current) { + overflowContainerHeightPx.intValue.toDp() + } ChatView( state = ChatViewState( chatItems = uiState.items, @@ -798,7 +814,8 @@ class ChatActivity : highlightedSearchTerm = uiState.highlightedSearchTerm, hasChatPermission = this::participantPermissions.isInitialized && participantPermissions.hasChatPermission(), - downloadingFileState = downloadingFileState.value + downloadingFileState = downloadingFileState.value, + stickyHeaderTopOffset = overflowHeightDp ), callbacks = ChatViewCallbacks( onLoadMore = { messageId, direction -> loadMoreMessages(messageId, direction) }, 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..d8670585bb 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 @@ -54,9 +54,11 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.alpha import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.res.pluralStringResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import com.nextcloud.talk.R @@ -95,7 +97,8 @@ data class ChatViewState( val chatMode: ChatViewModel.ChatMode = ChatViewModel.ChatMode.DEFAULT_MODE, val highlightedMessageId: Int? = null, val highlightedSearchTerm: String? = null, - val downloadingFileState: List = listOf() + val downloadingFileState: List = listOf(), + val stickyHeaderTopOffset: Dp = 0.dp ) data class ChatViewCallbacks( @@ -263,23 +266,36 @@ fun ChatView( } // Sticky date header - val stickyDateHeaderText by remember(listState, state.chatItems) { + val density = LocalDensity.current + val overflowPx = with(density) { state.stickyHeaderTopOffset.roundToPx() } + + val stickyDateHeaderText by remember(listState, state.chatItems, overflowPx) { derivedStateOf { - state.chatItems.getOrNull( - listState.layoutInfo.visibleItemsInfo.lastOrNull()?.index ?: 0 - )?.let { item -> - when (item) { - is ChatViewModel.ChatItem.MessageItem -> - formatTime(item.uiMessage.timestamp * LONG_1000) + val visibleItems = listState.layoutInfo.visibleItemsInfo + val viewportEnd = listState.layoutInfo.viewportEndOffset + // In reverseLayout=true, offsets increase from bottom (newest) to top (oldest). + // An item's bottom edge is at screen Y = viewportEnd - offset; it is visible when that + // is >= overflowPx, i.e. offset <= viewportEnd - overflowPx. + val targetItem = if (overflowPx > 0) { + visibleItems.filter { it.offset <= viewportEnd - overflowPx }.lastOrNull() + } else { + visibleItems.lastOrNull() + } + targetItem?.let { itemInfo -> + state.chatItems.getOrNull(itemInfo.index)?.let { item -> + when (item) { + is ChatViewModel.ChatItem.MessageItem -> + formatTime(item.uiMessage.timestamp * LONG_1000) - is ChatViewModel.ChatItem.DateHeaderItem -> - formatTime(item.date) + is ChatViewModel.ChatItem.DateHeaderItem -> + formatTime(item.date) - is ChatViewModel.ChatItem.UnreadMessagesMarkerItem -> - formatTime(item.date) + is ChatViewModel.ChatItem.UnreadMessagesMarkerItem -> + formatTime(item.date) - else -> "" - } + else -> "" + } + } ?: "" } ?: "" } } @@ -397,7 +413,7 @@ fun ChatView( text = stickyDateHeaderText, modifier = Modifier .align(Alignment.TopCenter) - .padding(top = 2.dp) + .padding(top = state.stickyHeaderTopOffset + 2.dp) .alpha(stickyDateHeaderAlpha) ) diff --git a/app/src/main/res/layout/activity_chat.xml b/app/src/main/res/layout/activity_chat.xml index 5b42861611..16a10d70a5 100644 --- a/app/src/main/res/layout/activity_chat.xml +++ b/app/src/main/res/layout/activity_chat.xml @@ -57,6 +57,7 @@