mirror of
https://github.com/simplex-chat/simplex-chat.git
synced 2026-09-06 10:15:27 +00:00
android, desktop: add floating date separator to chatview
This commit is contained in:
+118
-1
@@ -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
|
||||
@@ -1336,6 +1337,8 @@ fun BoxWithConstraintsScope.FloatingButtons(
|
||||
var firstVisibleIndex by remember { mutableStateOf(listState.firstVisibleItemIndex) }
|
||||
var lastIndexOfVisibleItems by remember { mutableStateOf(listState.layoutInfo.visibleItemsInfo.lastIndex) }
|
||||
var firstItemIsVisible by remember { mutableStateOf(firstVisibleIndex == 0) }
|
||||
var nearBottomIndex by remember { mutableStateOf<Int>(-1) }
|
||||
var isNearBottom by remember { mutableStateOf<Boolean>(true) }
|
||||
|
||||
LaunchedEffect(listState) {
|
||||
snapshotFlow { listState.firstVisibleItemIndex }
|
||||
@@ -1345,6 +1348,24 @@ fun BoxWithConstraintsScope.FloatingButtons(
|
||||
firstItemIsVisible = firstVisibleIndex == 0
|
||||
}
|
||||
}
|
||||
LaunchedEffect(listState) {
|
||||
snapshotFlow { listState.layoutInfo.visibleItemsInfo }
|
||||
.collect { visibleItemsInfo ->
|
||||
if (visibleItemsInfo.find { it.index == 0 } != null) {
|
||||
var elapsedOffset = 0
|
||||
|
||||
for (it in visibleItemsInfo) {
|
||||
if (elapsedOffset >= 800) {
|
||||
nearBottomIndex = it.index
|
||||
break;
|
||||
}
|
||||
elapsedOffset += it.size
|
||||
}
|
||||
}
|
||||
|
||||
isNearBottom = (visibleItemsInfo.firstOrNull()?.index ?: 0) < nearBottomIndex
|
||||
}
|
||||
}
|
||||
|
||||
LaunchedEffect(listState) {
|
||||
// When both snapshotFlows located in one LaunchedEffect second block will never be called because coroutine is paused on first block
|
||||
@@ -1401,6 +1422,29 @@ fun BoxWithConstraintsScope.FloatingButtons(
|
||||
onLongClick = { showDropDown.value = true }
|
||||
)
|
||||
|
||||
|
||||
val lastVisibleItem by remember {
|
||||
derivedStateOf {
|
||||
if (lastIndexOfVisibleItems >= 0) {
|
||||
val topVisibleItemIndex = chatItems.value.lastIndex - firstVisibleIndex - lastIndexOfVisibleItems
|
||||
chatItems.value[topVisibleItemIndex]
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val firstVisibleItemScrollOffset by remember {
|
||||
derivedStateOf { listState.firstVisibleItemScrollOffset }
|
||||
}
|
||||
|
||||
TopCenterFloatingButton(
|
||||
Modifier.padding(top = 10.dp).align(Alignment.TopCenter),
|
||||
lastVisibleItem,
|
||||
isNearBottom,
|
||||
firstVisibleItemScrollOffset
|
||||
)
|
||||
|
||||
Box {
|
||||
DefaultDropdownMenu(showDropDown, offset = DpOffset(this@FloatingButtons.maxWidth - DEFAULT_PADDING, 24.dp + fabSize)) {
|
||||
ItemAction(
|
||||
@@ -1497,6 +1541,79 @@ private fun TopEndFloatingButton(
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun TopCenterFloatingButton(
|
||||
modifier: Modifier,
|
||||
lastVisibleItem: ChatItem?,
|
||||
isNearBottom: Boolean,
|
||||
firstVisibleItemScrollOffset: Int
|
||||
) {
|
||||
val coroutineScope = rememberCoroutineScope()
|
||||
var hideDateWhenNotScrolling: Job? by remember { mutableStateOf(null) }
|
||||
val firstVisibleItemDate = remember { mutableStateOf<Instant?>(null) }
|
||||
|
||||
|
||||
val showDate = remember { mutableStateOf(false) }
|
||||
fun setDateVisibility(isVisible: Boolean) {
|
||||
if (isVisible) {
|
||||
val date = firstVisibleItemDate.value
|
||||
val now = Clock.System.now()
|
||||
if (!isNearBottom && !showDate.value && date != null && getTimestampDateText(date) != getTimestampDateText(now)) {
|
||||
showDate.value = true
|
||||
}
|
||||
} else if (showDate.value) {
|
||||
showDate.value = false
|
||||
}
|
||||
}
|
||||
|
||||
KeyChangeEffect(firstVisibleItemScrollOffset) {
|
||||
hideDateWhenNotScrolling?.cancel()
|
||||
|
||||
hideDateWhenNotScrolling = coroutineScope.launch {
|
||||
delay(1000)
|
||||
setDateVisibility(false)
|
||||
hideDateWhenNotScrolling = null
|
||||
}
|
||||
}
|
||||
|
||||
LaunchedEffect(lastVisibleItem) {
|
||||
snapshotFlow { lastVisibleItem?.meta?.itemTs }
|
||||
.distinctUntilChanged()
|
||||
.filterNotNull()
|
||||
.collect {
|
||||
val timeZone = TimeZone.currentSystemDefault()
|
||||
firstVisibleItemDate.value = it.toLocalDateTime(timeZone).date.atStartOfDayIn(timeZone)
|
||||
setDateVisibility(true)
|
||||
}
|
||||
}
|
||||
|
||||
AnimatedVisibility(
|
||||
modifier = modifier,
|
||||
visible = showDate.value,
|
||||
enter = fadeIn(tween(durationMillis = 350)),
|
||||
exit = fadeOut(tween(durationMillis = 350))
|
||||
) {
|
||||
val date = firstVisibleItemDate.value
|
||||
Column {
|
||||
Text(
|
||||
text = if (date != null) getTimestampDateText(date) else "",
|
||||
Modifier
|
||||
.background(
|
||||
color = MaterialTheme.appColors.receivedMessage,
|
||||
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,
|
||||
@@ -1560,7 +1677,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(DEFAULT_PADDING_HALF).fillMaxWidth(),
|
||||
fontSize = 14.sp,
|
||||
fontWeight = FontWeight.Medium,
|
||||
textAlign = TextAlign.Center,
|
||||
|
||||
Reference in New Issue
Block a user