feat: implement swipe back navigation and integrate VerifiedBadge in chat dialogs

This commit is contained in:
2026-02-05 03:25:20 +05:00
parent a03e267050
commit 9010d1c975
6 changed files with 555 additions and 295 deletions

View File

@@ -41,6 +41,7 @@ import com.rosetta.messenger.ui.chats.ChatDetailScreen
import com.rosetta.messenger.ui.chats.ChatsListScreen
import com.rosetta.messenger.ui.chats.SearchScreen
import com.rosetta.messenger.ui.components.OptimizedEmojiCache
import com.rosetta.messenger.ui.components.SwipeBackContainer
import com.rosetta.messenger.ui.onboarding.OnboardingScreen
import com.rosetta.messenger.ui.settings.BackupScreen
import com.rosetta.messenger.ui.settings.BiometricEnableScreen
@@ -551,17 +552,9 @@ fun MainScreen(
// Coroutine scope for profile updates
val mainScreenScope = rememberCoroutineScope()
// 🔥 Простая навигация с fade-in анимацией
// 🔥 Простая навигация с swipe back
Box(modifier = Modifier.fillMaxSize()) {
// Base layer - chats list
androidx.compose.animation.AnimatedVisibility(
visible = !showBackupScreen && !showSafetyScreen && !showThemeScreen &&
!showUpdatesScreen && selectedUser == null && !showSearchScreen &&
!showProfileScreen && !showOtherProfileScreen && !showLogsScreen &&
!showCrashLogsScreen && !showBiometricScreen,
enter = fadeIn(animationSpec = tween(300)),
exit = fadeOut(animationSpec = tween(200))
) {
// Base layer - chats list (всегда видимый, чтобы его было видно при свайпе)
ChatsListScreen(
isDarkTheme = isDarkTheme,
accountName = accountName,
@@ -606,15 +599,16 @@ fun MainScreen(
avatarRepository = avatarRepository,
onLogout = onLogout
)
}
// Other screens with fade animation
androidx.compose.animation.AnimatedVisibility(
visible = showBackupScreen,
enter = fadeIn(animationSpec = tween(300)),
exit = fadeOut(animationSpec = tween(200))
// Other screens with swipe back
SwipeBackContainer(
isVisible = showBackupScreen,
onBack = {
showBackupScreen = false
showSafetyScreen = true
},
isDarkTheme = isDarkTheme
) {
if (showBackupScreen) {
BackupScreen(
isDarkTheme = isDarkTheme,
onBack = {
@@ -653,14 +647,15 @@ fun MainScreen(
}
)
}
}
androidx.compose.animation.AnimatedVisibility(
visible = showSafetyScreen,
enter = fadeIn(animationSpec = tween(300)),
exit = fadeOut(animationSpec = tween(200))
SwipeBackContainer(
isVisible = showSafetyScreen,
onBack = {
showSafetyScreen = false
showProfileScreen = true
},
isDarkTheme = isDarkTheme
) {
if (showSafetyScreen) {
SafetyScreen(
isDarkTheme = isDarkTheme,
accountPublicKey = accountPublicKey,
@@ -678,14 +673,15 @@ fun MainScreen(
}
)
}
}
androidx.compose.animation.AnimatedVisibility(
visible = showThemeScreen,
enter = fadeIn(animationSpec = tween(300)),
exit = fadeOut(animationSpec = tween(200))
SwipeBackContainer(
isVisible = showThemeScreen,
onBack = {
showThemeScreen = false
showProfileScreen = true
},
isDarkTheme = isDarkTheme
) {
if (showThemeScreen) {
ThemeScreen(
isDarkTheme = isDarkTheme,
currentThemeMode = themeMode,
@@ -696,25 +692,22 @@ fun MainScreen(
onThemeModeChange = onThemeModeChange
)
}
}
androidx.compose.animation.AnimatedVisibility(
visible = showUpdatesScreen,
enter = fadeIn(animationSpec = tween(300)),
exit = fadeOut(animationSpec = tween(200))
SwipeBackContainer(
isVisible = showUpdatesScreen,
onBack = { showUpdatesScreen = false },
isDarkTheme = isDarkTheme
) {
if (showUpdatesScreen) {
UpdatesScreen(
isDarkTheme = isDarkTheme,
onBack = { showUpdatesScreen = false }
)
}
}
androidx.compose.animation.AnimatedVisibility(
visible = selectedUser != null,
enter = fadeIn(animationSpec = tween(300)),
exit = fadeOut(animationSpec = tween(200))
SwipeBackContainer(
isVisible = selectedUser != null,
onBack = { selectedUser = null },
isDarkTheme = isDarkTheme
) {
if (selectedUser != null) {
// Экран чата
@@ -738,12 +731,11 @@ fun MainScreen(
}
}
androidx.compose.animation.AnimatedVisibility(
visible = showSearchScreen,
enter = fadeIn(animationSpec = tween(300)),
exit = fadeOut(animationSpec = tween(200))
SwipeBackContainer(
isVisible = showSearchScreen,
onBack = { showSearchScreen = false },
isDarkTheme = isDarkTheme
) {
if (showSearchScreen) {
// Экран поиска
SearchScreen(
privateKeyHash = privateKeyHash,
@@ -757,14 +749,12 @@ fun MainScreen(
}
)
}
}
androidx.compose.animation.AnimatedVisibility(
visible = showProfileScreen,
enter = fadeIn(animationSpec = tween(300)),
exit = fadeOut(animationSpec = tween(200))
SwipeBackContainer(
isVisible = showProfileScreen,
onBack = { showProfileScreen = false },
isDarkTheme = isDarkTheme
) {
if (showProfileScreen) {
// Экран профиля
ProfileScreen(
isDarkTheme = isDarkTheme,
@@ -811,14 +801,15 @@ fun MainScreen(
dialogDao = RosettaDatabase.getDatabase(context).dialogDao()
)
}
}
androidx.compose.animation.AnimatedVisibility(
visible = showLogsScreen,
enter = fadeIn(animationSpec = tween(300)),
exit = fadeOut(animationSpec = tween(200))
SwipeBackContainer(
isVisible = showLogsScreen,
onBack = {
showLogsScreen = false
showProfileScreen = true
},
isDarkTheme = isDarkTheme
) {
if (showLogsScreen) {
com.rosetta.messenger.ui.settings.ProfileLogsScreen(
isDarkTheme = isDarkTheme,
logs = profileState.logs,
@@ -831,14 +822,15 @@ fun MainScreen(
}
)
}
}
androidx.compose.animation.AnimatedVisibility(
visible = showCrashLogsScreen,
enter = fadeIn(animationSpec = tween(300)),
exit = fadeOut(animationSpec = tween(200))
SwipeBackContainer(
isVisible = showCrashLogsScreen,
onBack = {
showCrashLogsScreen = false
showProfileScreen = true
},
isDarkTheme = isDarkTheme
) {
if (showCrashLogsScreen) {
CrashLogsScreen(
onBackClick = {
showCrashLogsScreen = false
@@ -846,14 +838,16 @@ fun MainScreen(
}
)
}
}
androidx.compose.animation.AnimatedVisibility(
visible = showOtherProfileScreen,
enter = fadeIn(animationSpec = tween(300)),
exit = fadeOut(animationSpec = tween(200))
SwipeBackContainer(
isVisible = showOtherProfileScreen,
onBack = {
showOtherProfileScreen = false
selectedOtherUser = null
},
isDarkTheme = isDarkTheme
) {
if (showOtherProfileScreen && selectedOtherUser != null) {
if (selectedOtherUser != null) {
OtherProfileScreen(
user = selectedOtherUser!!,
isDarkTheme = isDarkTheme,
@@ -867,12 +861,14 @@ fun MainScreen(
}
// Biometric Enable Screen
androidx.compose.animation.AnimatedVisibility(
visible = showBiometricScreen,
enter = fadeIn(animationSpec = tween(300)),
exit = fadeOut(animationSpec = tween(200))
SwipeBackContainer(
isVisible = showBiometricScreen,
onBack = {
showBiometricScreen = false
showProfileScreen = true
},
isDarkTheme = isDarkTheme
) {
if (showBiometricScreen) {
val biometricManager = remember { com.rosetta.messenger.biometric.BiometricAuthManager(context) }
val biometricPrefs = remember { com.rosetta.messenger.biometric.BiometricPreferences(context) }
val activity = context as? FragmentActivity
@@ -909,5 +905,4 @@ fun MainScreen(
)
}
}
}
}

View File

@@ -2115,6 +2115,7 @@ fun ChatDetailScreen(
dialogs = dialogsList,
isDarkTheme = isDarkTheme,
currentUserPublicKey = currentUserPublicKey,
avatarRepository = avatarRepository,
onDismiss = {
showForwardPicker = false
ForwardManager.clear()

View File

@@ -46,6 +46,7 @@ import com.rosetta.messenger.network.ProtocolState
import com.rosetta.messenger.ui.components.AppleEmojiText
import com.rosetta.messenger.ui.components.AvatarImage
import com.rosetta.messenger.ui.components.BlurredAvatarBackground
import com.rosetta.messenger.ui.components.VerifiedBadge
import com.rosetta.messenger.ui.onboarding.PrimaryBlue
import java.text.SimpleDateFormat
import java.util.*
@@ -1806,6 +1807,10 @@ fun DialogItemContent(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically
) {
Row(
modifier = Modifier.weight(1f),
verticalAlignment = Alignment.CenterVertically
) {
Text(
text = displayName,
@@ -1813,9 +1818,13 @@ fun DialogItemContent(
fontSize = 16.sp,
color = textColor,
maxLines = 1,
overflow = TextOverflow.Ellipsis,
modifier = Modifier.weight(1f)
overflow = TextOverflow.Ellipsis
)
if (dialog.verified > 0) {
Spacer(modifier = Modifier.width(4.dp))
VerifiedBadge(verified = dialog.verified, size = 16)
}
}
Row(
verticalAlignment = Alignment.CenterVertically,
@@ -1932,6 +1941,7 @@ fun DialogItemContent(
dialog.lastMessageAttachmentType == "Photo" -> "Photo"
dialog.lastMessageAttachmentType == "File" -> "File"
dialog.lastMessageAttachmentType == "Avatar" -> "Avatar"
dialog.lastMessageAttachmentType == "Forwarded" -> "Forwarded message"
dialog.lastMessage.isEmpty() -> "No messages"
else -> dialog.lastMessage
}

View File

@@ -174,6 +174,7 @@ class ChatsListViewModel(application: Application) : AndroidViewModel(applicatio
val type = firstAttachment.optInt("type", -1)
when (type) {
0 -> "Photo" // AttachmentType.IMAGE = 0
1 -> "Forwarded" // AttachmentType.MESSAGES = 1
2 -> "File" // AttachmentType.FILE = 2
3 -> "Avatar" // AttachmentType.AVATAR = 3
else -> null
@@ -261,6 +262,7 @@ class ChatsListViewModel(application: Application) : AndroidViewModel(applicatio
val type = firstAttachment.optInt("type", -1)
when (type) {
0 -> "Photo" // AttachmentType.IMAGE = 0
1 -> "Forwarded" // AttachmentType.MESSAGES = 1
2 -> "File" // AttachmentType.FILE = 2
3 -> "Avatar" // AttachmentType.AVATAR = 3
else -> null

View File

@@ -1,5 +1,6 @@
package com.rosetta.messenger.ui.chats
import android.view.HapticFeedbackConstants
import androidx.compose.animation.core.*
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
@@ -17,11 +18,15 @@ import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalView
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import com.rosetta.messenger.data.ForwardManager
import com.rosetta.messenger.repository.AvatarRepository
import com.rosetta.messenger.ui.components.AppleEmojiText
import com.rosetta.messenger.ui.components.AvatarImage
import com.rosetta.messenger.ui.onboarding.PrimaryBlue
import java.util.*
import kotlinx.coroutines.launch
@@ -39,11 +44,13 @@ fun ForwardChatPickerBottomSheet(
dialogs: List<DialogUiModel>,
isDarkTheme: Boolean,
currentUserPublicKey: String,
avatarRepository: AvatarRepository? = null,
onDismiss: () -> Unit,
onChatSelected: (DialogUiModel) -> Unit
) {
val sheetState = rememberModalBottomSheetState(skipPartiallyExpanded = false)
val scope = rememberCoroutineScope()
val view = LocalView.current
val backgroundColor = if (isDarkTheme) Color(0xFF1C1C1E) else Color.White
val textColor = if (isDarkTheme) Color.White else Color.Black
@@ -53,6 +60,11 @@ fun ForwardChatPickerBottomSheet(
val forwardMessages by ForwardManager.forwardMessages.collectAsState()
val messagesCount = forwardMessages.size
// 🔥 Haptic feedback при открытии
LaunchedEffect(Unit) {
view.performHapticFeedback(HapticFeedbackConstants.LONG_PRESS)
}
// 🔥 Функция для красивого закрытия с анимацией
fun dismissWithAnimation() {
scope.launch {
@@ -65,6 +77,7 @@ fun ForwardChatPickerBottomSheet(
onDismissRequest = { dismissWithAnimation() },
sheetState = sheetState,
containerColor = backgroundColor,
scrimColor = Color.Black.copy(alpha = 0.6f), // 🔥 Более тёмный overlay - перекрывает status bar
dragHandle = {
// Кастомный handle
Column(
@@ -85,7 +98,8 @@ fun ForwardChatPickerBottomSheet(
Spacer(modifier = Modifier.height(16.dp))
}
},
shape = RoundedCornerShape(topStart = 16.dp, topEnd = 16.dp)
shape = RoundedCornerShape(topStart = 16.dp, topEnd = 16.dp),
modifier = Modifier.statusBarsPadding() // 🔥 Учитываем status bar
) {
Column(modifier = Modifier.fillMaxWidth().navigationBarsPadding()) {
// Header
@@ -170,6 +184,7 @@ fun ForwardChatPickerBottomSheet(
dialog = dialog,
isDarkTheme = isDarkTheme,
isSavedMessages = dialog.opponentKey == currentUserPublicKey,
avatarRepository = avatarRepository,
onClick = { onChatSelected(dialog) }
)
@@ -197,16 +212,12 @@ private fun ForwardDialogItem(
dialog: DialogUiModel,
isDarkTheme: Boolean,
isSavedMessages: Boolean = false,
avatarRepository: AvatarRepository? = null,
onClick: () -> Unit
) {
val textColor = if (isDarkTheme) Color.White else Color.Black
val secondaryTextColor = if (isDarkTheme) Color(0xFF8E8E93) else Color(0xFF666666)
val avatarColors =
remember(dialog.opponentKey, isDarkTheme) {
getAvatarColor(dialog.opponentKey, isDarkTheme)
}
val displayName =
remember(dialog.opponentTitle, dialog.opponentUsername, dialog.opponentKey, isSavedMessages) {
when {
@@ -217,19 +228,12 @@ private fun ForwardDialogItem(
}
}
val initials =
remember(dialog.opponentTitle, dialog.opponentUsername, dialog.opponentKey, isSavedMessages) {
// Display name for avatar initials
val avatarDisplayName = remember(dialog.opponentTitle, dialog.opponentUsername) {
when {
isSavedMessages -> "📁"
dialog.opponentTitle.isNotEmpty() -> {
dialog.opponentTitle
.split(" ")
.take(2)
.mapNotNull { it.firstOrNull()?.uppercase() }
.joinToString("")
}
dialog.opponentUsername.isNotEmpty() -> dialog.opponentUsername.take(2).uppercase()
else -> dialog.opponentKey.take(2).uppercase()
dialog.opponentTitle.isNotEmpty() -> dialog.opponentTitle
dialog.opponentUsername.isNotEmpty() -> dialog.opponentUsername
else -> null
}
}
@@ -240,24 +244,36 @@ private fun ForwardDialogItem(
.padding(horizontal = 16.dp, vertical = 12.dp),
verticalAlignment = Alignment.CenterVertically
) {
// Avatar
// Avatar with real image support
if (isSavedMessages) {
// Saved Messages - special icon
val avatarColors = remember(dialog.opponentKey, isDarkTheme) {
getAvatarColor(dialog.opponentKey, isDarkTheme)
}
Box(
modifier =
Modifier.size(48.dp)
modifier = Modifier
.size(48.dp)
.clip(CircleShape)
.background(
if (isSavedMessages) PrimaryBlue.copy(alpha = 0.15f)
else avatarColors.backgroundColor
),
.background(PrimaryBlue.copy(alpha = 0.15f)),
contentAlignment = Alignment.Center
) {
Text(
text = initials,
color = if (isSavedMessages) PrimaryBlue else avatarColors.textColor,
text = "📁",
color = PrimaryBlue,
fontWeight = FontWeight.SemiBold,
fontSize = 16.sp
)
}
} else {
// Regular user - use AvatarImage with real avatar support
AvatarImage(
publicKey = dialog.opponentKey,
avatarRepository = avatarRepository,
size = 48.dp,
isDarkTheme = isDarkTheme,
displayName = avatarDisplayName
)
}
Spacer(modifier = Modifier.width(12.dp))
@@ -274,14 +290,23 @@ private fun ForwardDialogItem(
Spacer(modifier = Modifier.height(2.dp))
Text(
text =
if (isSavedMessages) "Your personal notes"
else dialog.lastMessage.ifEmpty { "No messages" },
// 📎 Определяем текст превью с учетом attachments
val previewText = when {
isSavedMessages -> "Your personal notes"
dialog.lastMessageAttachmentType == "Photo" -> "Photo"
dialog.lastMessageAttachmentType == "File" -> "File"
dialog.lastMessageAttachmentType == "Avatar" -> "Avatar"
dialog.lastMessageAttachmentType == "Forwarded" -> "Forwarded message"
dialog.lastMessage.isNotEmpty() -> dialog.lastMessage
else -> "No messages"
}
AppleEmojiText(
text = previewText,
fontSize = 14.sp,
color = secondaryTextColor,
maxLines = 1,
overflow = TextOverflow.Ellipsis
enableLinks = false
)
}

View File

@@ -0,0 +1,227 @@
package com.rosetta.messenger.ui.components
import androidx.compose.animation.core.*
import androidx.compose.foundation.background
import androidx.compose.foundation.gestures.*
import androidx.compose.foundation.layout.*
import androidx.compose.runtime.*
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.graphicsLayer
import androidx.compose.ui.input.pointer.*
import androidx.compose.ui.input.pointer.util.VelocityTracker
import androidx.compose.ui.platform.LocalConfiguration
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.unit.dp
import kotlinx.coroutines.launch
// Telegram's CubicBezierInterpolator(0.25, 0.1, 0.25, 1.0)
private val TelegramEasing = CubicBezierEasing(0.25f, 0.1f, 0.25f, 1.0f)
// Constants matching Telegram
private const val COMPLETION_THRESHOLD = 0.5f // 50% of screen width
private const val FLING_VELOCITY_THRESHOLD = 600f // px/s
private const val ANIMATION_DURATION_ENTER = 300
private const val ANIMATION_DURATION_EXIT = 250
private const val EDGE_ZONE_DP = 200
/**
* Telegram-style swipe back container (optimized)
*
* Wraps content and allows swiping from the left edge to go back.
* Features:
* - Edge-only swipe detection (left 30dp)
* - Direct state update during drag (no coroutine overhead)
* - VelocityTracker for fling detection
* - Smooth Telegram-style bezier animation
* - Scrim (dimming) on background
* - Shadow on left edge during swipe
* - Threshold: 50% of width OR 600px/s fling velocity
*/
@Composable
fun SwipeBackContainer(
isVisible: Boolean,
onBack: () -> Unit,
isDarkTheme: Boolean,
swipeEnabled: Boolean = true,
content: @Composable () -> Unit
) {
val density = LocalDensity.current
val configuration = LocalConfiguration.current
val screenWidthPx = with(density) { configuration.screenWidthDp.dp.toPx() }
val edgeZonePx = with(density) { EDGE_ZONE_DP.dp.toPx() }
// Animation state for swipe (used only for swipe animations, not during drag)
val offsetAnimatable = remember { Animatable(0f) }
// Alpha animation for fade-in entry
val alphaAnimatable = remember { Animatable(0f) }
// Drag state - direct update without animation
var dragOffset by remember { mutableFloatStateOf(0f) }
var isDragging by remember { mutableStateOf(false) }
// Visibility state
var shouldShow by remember { mutableStateOf(false) }
var isAnimatingIn by remember { mutableStateOf(false) }
var isAnimatingOut by remember { mutableStateOf(false) }
// Coroutine scope for animations
val scope = rememberCoroutineScope()
// Current offset: use drag offset during drag, animatable otherwise
val currentOffset = if (isDragging) dragOffset else offsetAnimatable.value
// Current alpha: use animatable during fade animations, otherwise 1
val currentAlpha = if (isAnimatingIn || isAnimatingOut) alphaAnimatable.value else 1f
// Scrim alpha based on swipe progress
val scrimAlpha = (1f - (currentOffset / screenWidthPx).coerceIn(0f, 1f)) * 0.4f
// Handle visibility changes
LaunchedEffect(isVisible) {
if (isVisible && !shouldShow) {
// Animate in: fade-in
shouldShow = true
isAnimatingIn = true
offsetAnimatable.snapTo(0f) // No slide for entry
alphaAnimatable.snapTo(0f)
alphaAnimatable.animateTo(
targetValue = 1f,
animationSpec = tween(
durationMillis = ANIMATION_DURATION_ENTER,
easing = FastOutSlowInEasing
)
)
isAnimatingIn = false
} else if (!isVisible && shouldShow && !isAnimatingOut) {
// Animate out: fade-out (when triggered by button, not swipe)
isAnimatingOut = true
alphaAnimatable.snapTo(1f)
alphaAnimatable.animateTo(
targetValue = 0f,
animationSpec = tween(
durationMillis = 200,
easing = FastOutSlowInEasing
)
)
shouldShow = false
isAnimatingOut = false
offsetAnimatable.snapTo(0f)
alphaAnimatable.snapTo(0f)
dragOffset = 0f
}
}
if (!shouldShow && !isAnimatingIn && !isAnimatingOut) return
Box(modifier = Modifier.fillMaxSize()) {
// Scrim (dimming layer behind the screen) - only when swiping
if (currentOffset > 0f) {
Box(
modifier = Modifier
.fillMaxSize()
.background(Color.Black.copy(alpha = scrimAlpha))
)
}
// Content with swipe gesture
Box(
modifier = Modifier
.fillMaxSize()
.graphicsLayer {
translationX = currentOffset
alpha = currentAlpha
}
.background(if (isDarkTheme) Color(0xFF1B1B1B) else Color.White)
.then(
if (swipeEnabled && !isAnimatingIn && !isAnimatingOut) {
Modifier.pointerInput(Unit) {
val velocityTracker = VelocityTracker()
awaitEachGesture {
val down = awaitFirstDown(requireUnconsumed = false)
// Edge-only detection
if (down.position.x > edgeZonePx) {
return@awaitEachGesture
}
velocityTracker.resetTracking()
var startedSwipe = false
try {
horizontalDrag(down.id) { change ->
val dragAmount = change.positionChange().x
// Only start swipe if moving right
if (!startedSwipe && dragAmount > 0) {
startedSwipe = true
isDragging = true
dragOffset = offsetAnimatable.value
}
if (startedSwipe) {
// Direct state update - NO coroutines!
dragOffset = (dragOffset + dragAmount)
.coerceIn(0f, screenWidthPx)
velocityTracker.addPosition(
change.uptimeMillis,
change.position
)
change.consume()
}
}
} catch (_: Exception) {
// Gesture was cancelled
}
// Handle drag end
if (startedSwipe) {
isDragging = false
val velocity = velocityTracker.calculateVelocity().x
val currentProgress = dragOffset / screenWidthPx
// Telegram logic: fling OR 50% threshold
val shouldComplete =
velocity > FLING_VELOCITY_THRESHOLD ||
(currentProgress > COMPLETION_THRESHOLD &&
velocity > -FLING_VELOCITY_THRESHOLD)
// Sync animatable with current drag position and animate
scope.launch {
offsetAnimatable.snapTo(dragOffset)
if (shouldComplete) {
offsetAnimatable.animateTo(
targetValue = screenWidthPx,
animationSpec = tween(
durationMillis = ANIMATION_DURATION_EXIT,
easing = TelegramEasing
)
)
onBack()
} else {
offsetAnimatable.animateTo(
targetValue = 0f,
animationSpec = tween(
durationMillis = ANIMATION_DURATION_EXIT,
easing = TelegramEasing
)
)
}
dragOffset = 0f
}
}
}
}
} else {
Modifier
}
)
) {
content()
}
}
}