From 533d0e40accb2b9fc50b6243e70cfeac02f21a99 Mon Sep 17 00:00:00 2001 From: Diogo Date: Mon, 30 Sep 2024 15:45:32 +0100 Subject: [PATCH] android, desktop: add floating date separator to chatview (#4951) * android, desktop: add floating date separator to chatview * closer near bottom * uncessary code * same pill bg as other btns * space * varname * safe get for lastVisibleItem * move floating date outside of floating buttons * fast cleanup on chat change * reduced recomposes * change delay position * base near bottom offset on viewport size * refactor * Revert "change delay position" This reverts commit 27b19580edd211f216f6b7e7cc2504bbc45027d5. * simplified * exact match on header position * reduce recomposes --------- Co-authored-by: Avently <7953703+avently@users.noreply.github.com> --- .../simplex/common/views/chat/ChatView.kt | 111 +++++++++++++++++- 1 file changed, 110 insertions(+), 1 deletion(-) diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ChatView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ChatView.kt index 6ffd02dc60..2ba4316b0f 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ChatView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ChatView.kt @@ -12,6 +12,7 @@ import androidx.compose.material.* import androidx.compose.runtime.* import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.ui.* +import androidx.compose.ui.draw.clip import androidx.compose.ui.draw.drawWithCache import androidx.compose.ui.graphics.* import androidx.compose.ui.layout.layoutId @@ -1250,6 +1251,12 @@ fun BoxWithConstraintsScope.ChatItemsList( } } FloatingButtons(chatModel.chatItems, unreadCount, remoteHostId, chatInfo, searchValue, markRead, setFloatingButton, listState) + + FloatingDate( + Modifier.padding(top = 10.dp).align(Alignment.TopCenter), + listState, + ) + LaunchedEffect(Unit) { snapshotFlow { listState.isScrollInProgress } .collect { @@ -1476,6 +1483,108 @@ private fun TopEndFloatingButton( } } +@Composable +private fun FloatingDate( + modifier: Modifier, + listState: LazyListState, +) { + var nearBottomIndex by remember { mutableStateOf(-1) } + var isNearBottom by remember { mutableStateOf(true) } + val lastVisibleItemDate = remember { + derivedStateOf { + if (listState.layoutInfo.visibleItemsInfo.lastIndex >= 0 && listState.firstVisibleItemIndex >= 0) { + val lastVisibleChatItemIndex = chatModel.chatItems.value.lastIndex - listState.firstVisibleItemIndex - listState.layoutInfo.visibleItemsInfo.lastIndex + val item = chatModel.chatItems.value.getOrNull(lastVisibleChatItemIndex) + val timeZone = TimeZone.currentSystemDefault() + item?.meta?.itemTs?.toLocalDateTime(timeZone)?.date?.atStartOfDayIn(timeZone) + } else { + null + } + } + } + val showDate = remember { mutableStateOf(false) } + LaunchedEffect(Unit) { + launch { + snapshotFlow { chatModel.chatId.value } + .distinctUntilChanged() + .collect { + showDate.value = false + isNearBottom = true + nearBottomIndex = -1 + } + } + } + + LaunchedEffect(Unit) { + snapshotFlow { listState.layoutInfo.visibleItemsInfo } + .collect { visibleItemsInfo -> + if (visibleItemsInfo.find { it.index == 0 } != null) { + var elapsedOffset = 0 + + for (it in visibleItemsInfo) { + if (elapsedOffset >= listState.layoutInfo.viewportSize.height / 2.5) { + nearBottomIndex = it.index + break; + } + elapsedOffset += it.size + } + } + + isNearBottom = if (nearBottomIndex == -1) true else (visibleItemsInfo.firstOrNull()?.index ?: 0) <= nearBottomIndex + } + } + + fun setDateVisibility(isVisible: Boolean) { + if (isVisible) { + val now = Clock.System.now() + val date = lastVisibleItemDate.value + if (!isNearBottom && !showDate.value && date != null && getTimestampDateText(date) != getTimestampDateText(now)) { + showDate.value = true + } + } else if (showDate.value) { + showDate.value = false + } + } + + LaunchedEffect(Unit) { + var hideDateWhenNotScrolling: Job = Job() + snapshotFlow { listState.firstVisibleItemScrollOffset } + .collect { + setDateVisibility(true) + hideDateWhenNotScrolling.cancel() + hideDateWhenNotScrolling = launch { + delay(1000) + setDateVisibility(false) + } + } + } + + AnimatedVisibility( + modifier = modifier, + visible = showDate.value, + enter = fadeIn(tween(durationMillis = 350)), + exit = fadeOut(tween(durationMillis = 350)) + ) { + val date = lastVisibleItemDate.value + Column { + Text( + text = if (date != null) getTimestampDateText(date) else "", + Modifier + .background( + color = MaterialTheme.colors.secondaryVariant, + RoundedCornerShape(25.dp) + ) + .padding(vertical = 4.dp, horizontal = 8.dp) + .clip(RoundedCornerShape(25.dp)), + fontSize = 14.sp, + fontWeight = FontWeight.Medium, + textAlign = TextAlign.Center, + color = MaterialTheme.colors.secondary + ) + } + } +} + @Composable private fun DownloadFilesButton( forwardConfirmation: ForwardConfirmation.FilesNotAccepted, @@ -1539,7 +1648,7 @@ private fun ButtonRow(horizontalArrangement: Arrangement.Horizontal, content: @C private fun DateSeparator(date: Instant) { Text( text = getTimestampDateText(date), - Modifier.padding(DEFAULT_PADDING).fillMaxWidth(), + Modifier.padding(vertical = DEFAULT_PADDING_HALF + 4.dp, horizontal = DEFAULT_PADDING_HALF).fillMaxWidth(), fontSize = 14.sp, fontWeight = FontWeight.Medium, textAlign = TextAlign.Center,