diff --git a/app/src/main/java/com/rosetta/messenger/ui/chats/ChatsListScreen.kt b/app/src/main/java/com/rosetta/messenger/ui/chats/ChatsListScreen.kt index d050733..8a038ed 100644 --- a/app/src/main/java/com/rosetta/messenger/ui/chats/ChatsListScreen.kt +++ b/app/src/main/java/com/rosetta/messenger/ui/chats/ChatsListScreen.kt @@ -1794,6 +1794,7 @@ fun ChatsListScreen( val showSkeleton = isLoading val chatListState = rememberLazyListState() var isRequestsVisible by remember { mutableStateOf(true) } + var requestsPullProgress by remember { mutableStateOf(0f) } var lastAutoScrolledVerificationId by remember { mutableStateOf(null) } @@ -2099,6 +2100,8 @@ fun ChatsListScreen( ) { var accumulatedPullDown = 0f var accumulatedPullUp = 0f + val pullDownLimit = + requestsRevealThresholdPx * 1.25f object : androidx.compose.ui.input.nestedscroll.NestedScrollConnection { override fun onPreScroll( available: androidx.compose.ui.geometry.Offset, @@ -2109,6 +2112,7 @@ fun ChatsListScreen( ) { accumulatedPullDown = 0f accumulatedPullUp = 0f + requestsPullProgress = 0f return androidx.compose.ui.geometry.Offset.Zero } @@ -2123,6 +2127,7 @@ fun ChatsListScreen( ) { accumulatedPullUp += -available.y accumulatedPullDown = 0f + requestsPullProgress = 0f if (accumulatedPullUp >= requestsHideThresholdPx) { isRequestsVisible = false accumulatedPullUp = 0f @@ -2135,18 +2140,26 @@ fun ChatsListScreen( accumulatedPullDown = (accumulatedPullDown + available.y) .coerceAtMost( - requestsRevealThresholdPx + pullDownLimit + ) + requestsPullProgress = + (accumulatedPullDown / requestsRevealThresholdPx) + .coerceIn( + 0f, + 1.15f ) if (accumulatedPullDown >= requestsRevealThresholdPx) { isRequestsVisible = true accumulatedPullDown = 0f accumulatedPullUp = 0f + requestsPullProgress = 0f hapticFeedback.performHapticFeedback( HapticFeedbackType.LongPress ) } } else if (available.y <= 0f || !atTop) { accumulatedPullDown = 0f + requestsPullProgress = 0f } return androidx.compose.ui.geometry.Offset.Zero @@ -2158,11 +2171,24 @@ fun ChatsListScreen( ): androidx.compose.ui.unit.Velocity { accumulatedPullDown = 0f accumulatedPullUp = 0f + requestsPullProgress = 0f return androidx.compose.ui.unit.Velocity.Zero } } } + LaunchedEffect( + chatListState.isScrollInProgress, + isRequestsVisible + ) { + if (!chatListState.isScrollInProgress && + !isRequestsVisible && + requestsPullProgress != 0f + ) { + requestsPullProgress = 0f + } + } + LazyColumn( state = chatListState, modifier = @@ -2203,15 +2229,75 @@ fun ChatsListScreen( if (requestsCount > 0) { if (!isRequestsVisible) { item(key = "requests_reopen_handle") { + val animatedPullProgress by + animateFloatAsState( + targetValue = + requestsPullProgress, + animationSpec = + spring( + dampingRatio = + Spring.DampingRatioNoBouncy, + stiffness = + Spring.StiffnessMediumLow + ), + label = + "requestsPullProgress" + ) RequestsRevealHandle( isDarkTheme = isDarkTheme, + pullProgress = + animatedPullProgress, onClick = { isRequestsVisible = true + requestsPullProgress = + 0f hapticFeedback.performHapticFeedback( HapticFeedbackType.LongPress ) } ) + val revealProgress = + FastOutSlowInEasing + .transform( + animatedPullProgress + .coerceIn( + 0f, + 1f + ) + ) + if (revealProgress > 0.001f) { + Box( + modifier = + Modifier.fillMaxWidth() + .height( + 76.dp * + revealProgress + ) + .clipToBounds() + .graphicsLayer { + alpha = + revealProgress + } + ) { + RequestsSection( + count = + requestsCount, + requests = + requests, + isDarkTheme = + isDarkTheme, + onClick = { + isRequestsVisible = + true + requestsPullProgress = + 0f + hapticFeedback.performHapticFeedback( + HapticFeedbackType.LongPress + ) + } + ) + } + } Divider( color = dividerColor, thickness = 0.5.dp @@ -4550,16 +4636,36 @@ fun TypingIndicatorSmall() { } @Composable -private fun RequestsRevealHandle(isDarkTheme: Boolean, onClick: () -> Unit) { - val textColor = if (isDarkTheme) Color(0xFF8E8E93) else Color(0xFF6C6C70) - val iconColor = if (isDarkTheme) Color(0xFF7E7E84) else Color(0xFF8A8A90) +private fun RequestsRevealHandle( + isDarkTheme: Boolean, + pullProgress: Float, + onClick: () -> Unit +) { + val clampedProgress = pullProgress.coerceIn(0f, 1.15f) + val revealProgress = FastOutSlowInEasing.transform(clampedProgress.coerceIn(0f, 1f)) + val stretchOvershoot = (clampedProgress - 1f).coerceAtLeast(0f) + + val textColor = + if (isDarkTheme) Color(0xFF8E8E93).copy(alpha = 0.84f + revealProgress * 0.16f) + else Color(0xFF6C6C70).copy(alpha = 0.84f + revealProgress * 0.16f) + val iconColor = + if (isDarkTheme) Color(0xFF7E7E84).copy(alpha = 0.88f + revealProgress * 0.12f) + else Color(0xFF8A8A90).copy(alpha = 0.88f + revealProgress * 0.12f) + val verticalPadding = 10.dp + (6.dp * revealProgress) + (4.dp * stretchOvershoot) Row( modifier = - Modifier.fillMaxWidth().clickable(onClick = onClick).padding( - horizontal = TELEGRAM_DIALOG_AVATAR_START, - vertical = 10.dp - ), + Modifier.fillMaxWidth() + .graphicsLayer { + val scaleBoost = + revealProgress * 0.015f + stretchOvershoot * 0.06f + scaleY = 1f + scaleBoost + } + .clickable(onClick = onClick) + .padding( + horizontal = TELEGRAM_DIALOG_AVATAR_START, + vertical = verticalPadding + ), horizontalArrangement = Arrangement.Center, verticalAlignment = Alignment.CenterVertically ) { @@ -4567,11 +4673,16 @@ private fun RequestsRevealHandle(isDarkTheme: Boolean, onClick: () -> Unit) { imageVector = TablerIcons.ChevronDown, contentDescription = "Show requests", tint = iconColor, - modifier = Modifier.size(18.dp) + modifier = + Modifier.size(18.dp + (2.dp * revealProgress)).graphicsLayer { + rotationZ = 180f * revealProgress + } ) Spacer(modifier = Modifier.width(6.dp)) Text( - text = "Pull down to show requests", + text = + if (clampedProgress >= 0.92f) "Release to open requests" + else "Pull down to show requests", color = textColor, fontSize = 13.sp, fontWeight = FontWeight.Medium