From b918b4560306f887a2bba1299d4868556d168e84 Mon Sep 17 00:00:00 2001 From: k1ngsterr1 Date: Wed, 18 Mar 2026 23:35:38 +0500 Subject: [PATCH] =?UTF-8?q?=D0=A1=D0=B4=D0=B5=D0=BB=D0=B0=D0=BD=20=D0=B8?= =?UTF-8?q?=D0=BD=D1=82=D0=B5=D1=80=D0=B0=D0=BA=D1=82=D0=B8=D0=B2=D0=BD?= =?UTF-8?q?=D1=8B=D0=B9=20drag=20=D0=BC=D0=B5=D0=B4=D0=B8=D0=B0-=D0=BF?= =?UTF-8?q?=D0=B8=D0=BA=D0=B5=D1=80=D0=BE=D0=B2=20=D0=B8=20=D0=BE=D0=B1?= =?UTF-8?q?=D0=BD=D0=BE=D0=B2=D0=BB=D0=B5=D0=BD=D1=8B=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../rosetta/messenger/data/ReleaseNotes.kt | 11 ++--- .../messenger/ui/chats/ChatDetailScreen.kt | 10 ++-- .../ui/chats/attach/ChatAttachAlert.kt | 49 ++++++++++++------- .../components/MediaPickerBottomSheet.kt | 49 ++++++++++++------- .../ui/settings/ProfilePhotoPicker.kt | 48 ++++++++++++------ 5 files changed, 108 insertions(+), 59 deletions(-) diff --git a/app/src/main/java/com/rosetta/messenger/data/ReleaseNotes.kt b/app/src/main/java/com/rosetta/messenger/data/ReleaseNotes.kt index d8cf458..a35037d 100644 --- a/app/src/main/java/com/rosetta/messenger/data/ReleaseNotes.kt +++ b/app/src/main/java/com/rosetta/messenger/data/ReleaseNotes.kt @@ -17,14 +17,13 @@ object ReleaseNotes { val RELEASE_NOTICE = """ Update v$VERSION_PLACEHOLDER - Синхронизация Android ↔ iOS - - Исправлена проблема: сообщения зависали на «часиках» при одновременном использовании Android и iOS - - Добавлен механизм автоматического повтора отправки: 3 попытки с интервалом 4 сек, таймаут 80 сек - - Исправлена нормализация sync-курсора для корректной синхронизации между устройствами + Медиа-пикер + - Добавлено интерактивное поведение: панель теперь следует за пальцем во время drag (как в Telegram) + - Одинаковая drag-логика применена для нового пикера, fallback-пикера и пикера аватарки + - Улучшен snap после отпускания: корректный доскок к ближайшему состоянию Интерфейс - - Дата «today/yesterday» и пустой стейт чата теперь белые при тёмных обоях или тёмной теме - - Исправлена обрезка имени отправителя в групповых чатах + - Исправлена светлая линия между статус-баром и медиа-пикером в тёмной теме """.trimIndent() fun getNotice(version: String): String = diff --git a/app/src/main/java/com/rosetta/messenger/ui/chats/ChatDetailScreen.kt b/app/src/main/java/com/rosetta/messenger/ui/chats/ChatDetailScreen.kt index 53cdcf0..adcc67a 100644 --- a/app/src/main/java/com/rosetta/messenger/ui/chats/ChatDetailScreen.kt +++ b/app/src/main/java/com/rosetta/messenger/ui/chats/ChatDetailScreen.kt @@ -1717,14 +1717,18 @@ fun ChatDetailScreen( } // Закрытие Crossfade // Bottom line для unified header + val headerDividerColor = + when { + showMediaPicker -> Color.Transparent + isDarkTheme -> Color.Black.copy(alpha = 0.26f) + else -> Color.White.copy(alpha = 0.15f) + } Box( modifier = Modifier.align(Alignment.BottomCenter) .fillMaxWidth() .height(0.5.dp) - .background( - Color.White.copy(alpha = 0.15f) - ) + .background(headerDividerColor) ) } // Закрытие Box unified header diff --git a/app/src/main/java/com/rosetta/messenger/ui/chats/attach/ChatAttachAlert.kt b/app/src/main/java/com/rosetta/messenger/ui/chats/attach/ChatAttachAlert.kt index 0f5d63b..3e084b0 100644 --- a/app/src/main/java/com/rosetta/messenger/ui/chats/attach/ChatAttachAlert.kt +++ b/app/src/main/java/com/rosetta/messenger/ui/chats/attach/ChatAttachAlert.kt @@ -47,7 +47,6 @@ import app.rosette.android.ui.keyboard.AnimatedKeyboardTransition import app.rosette.android.ui.keyboard.rememberKeyboardTransitionCoordinator import com.rosetta.messenger.ui.onboarding.PrimaryBlue import com.rosetta.messenger.ui.utils.NavigationModeUtils -import kotlinx.coroutines.Job import kotlinx.coroutines.launch import android.net.Uri import android.util.Log @@ -352,6 +351,12 @@ fun ChatAttachAlert( ) val sheetHeightPx = remember { Animatable(collapsedHeightPx) } + var dragSheetHeightPx by remember { mutableFloatStateOf(Float.NaN) } + val interactiveSheetHeightPx by remember { + derivedStateOf { + if (dragSheetHeightPx.isNaN()) sheetHeightPx.value else dragSheetHeightPx + } + } val animationScope = rememberCoroutineScope() // ═══════════════════════════════════════════════════════════ @@ -409,6 +414,7 @@ fun ChatAttachAlert( val action = closeAction closeAction = null animationScope.launch { + dragSheetHeightPx = Float.NaN sheetHeightPx.snapTo(collapsedHeightPx) } action?.invoke() @@ -421,7 +427,7 @@ fun ChatAttachAlert( val scrimAlpha by animateFloatAsState( targetValue = if (shouldShow && !isClosing) { val expandRange = (expandedHeightPx - collapsedStateHeightPx).coerceAtLeast(1f) - val expandProgress = (sheetHeightPx.value - collapsedStateHeightPx) / expandRange + val expandProgress = (interactiveSheetHeightPx - collapsedStateHeightPx) / expandRange 0.25f + 0.15f * expandProgress.coerceIn(0f, 1f) } else 0f, animationSpec = tween( @@ -508,19 +514,20 @@ fun ChatAttachAlert( shouldShow = true isClosing = false showAlbumMenu = false + dragSheetHeightPx = Float.NaN sheetHeightPx.snapTo(collapsedStateHeightPx) } } // Grow sheet when items selected (collapsed only) - LaunchedEffect(showSheet, state.selectedItemOrder.isNotEmpty(), isExpanded, isClosing, state.editingItem) { - if (showSheet && state.editingItem == null && !isClosing && !isExpanded) { + LaunchedEffect(showSheet, state.selectedItemOrder.isNotEmpty(), isExpanded, isClosing, state.editingItem, dragSheetHeightPx) { + if (showSheet && state.editingItem == null && !isClosing && !isExpanded && dragSheetHeightPx.isNaN()) { val targetHeight = if (state.selectedItemOrder.isNotEmpty()) { collapsedStateHeightPx + selectionHeaderExtraPx } else { collapsedStateHeightPx } - if (kotlin.math.abs(sheetHeightPx.value - targetHeight) > 1f) { + if (kotlin.math.abs(interactiveSheetHeightPx - targetHeight) > 1f) { sheetHeightPx.stop() sheetHeightPx.animateTo( targetHeight, @@ -630,7 +637,12 @@ fun ChatAttachAlert( fun snapToNearestState(velocity: Float = 0f) { animationScope.launch { - val currentHeight = sheetHeightPx.value + val currentHeight = interactiveSheetHeightPx + if (!dragSheetHeightPx.isNaN()) { + sheetHeightPx.stop() + sheetHeightPx.snapTo(currentHeight) + dragSheetHeightPx = Float.NaN + } val velocityThreshold = 180f val expandSnapThreshold = collapsedStateHeightPx + (expandedHeightPx - collapsedStateHeightPx) * 0.35f @@ -697,7 +709,7 @@ fun ChatAttachAlert( val isPickerFullScreen by remember { derivedStateOf { val expandRange = (expandedHeightPx - collapsedStateHeightPx).coerceAtLeast(1f) - val progress = ((sheetHeightPx.value - collapsedStateHeightPx) / expandRange).coerceIn(0f, 1f) + val progress = ((interactiveSheetHeightPx - collapsedStateHeightPx) / expandRange).coerceIn(0f, 1f) progress > 0.9f } } @@ -918,11 +930,11 @@ fun ChatAttachAlert( ) { // Sheet height stays constant — keyboard space is handled by // internal Spacer, not by shrinking the container (Telegram approach). - val visibleSheetHeightPx = (sheetHeightPx.value + navInsetPxForSheet).coerceAtLeast(minHeightPx) + val visibleSheetHeightPx = (interactiveSheetHeightPx + navInsetPxForSheet).coerceAtLeast(minHeightPx) val currentHeightDp = with(density) { visibleSheetHeightPx.toDp() } val slideOffset = (visibleSheetHeightPx * animatedOffset).toInt() val expandProgress = - ((sheetHeightPx.value - collapsedStateHeightPx) / + ((interactiveSheetHeightPx - collapsedStateHeightPx) / (expandedHeightPx - collapsedStateHeightPx).coerceAtLeast(1f)) .coerceIn(0f, 1f) val topCornerRadius by animateDpAsState( @@ -944,7 +956,6 @@ fun ChatAttachAlert( val selectedCount = state.selectedCount var lastDragVelocity by remember { mutableFloatStateOf(0f) } - var dragSnapJob by remember { mutableStateOf(null) } Box( modifier = Modifier @@ -965,8 +976,15 @@ fun ChatAttachAlert( ) { /* Prevent click through */ } .pointerInput(Unit) { detectVerticalDragGestures( + onDragStart = { + animationScope.launch { sheetHeightPx.stop() } + dragSheetHeightPx = interactiveSheetHeightPx + }, + onDragCancel = { + snapToNearestState() + lastDragVelocity = 0f + }, onDragEnd = { - dragSnapJob?.cancel() snapToNearestState(lastDragVelocity) lastDragVelocity = 0f }, @@ -975,12 +993,9 @@ fun ChatAttachAlert( val adjustedDragAmount = if (dragAmount < 0f) dragAmount * 1.25f else dragAmount * 0.9f lastDragVelocity = adjustedDragAmount * 1.8f - val newHeight = (sheetHeightPx.value - adjustedDragAmount) + val newHeight = (interactiveSheetHeightPx - adjustedDragAmount) .coerceIn(minHeightPx, expandedHeightPx) - dragSnapJob?.cancel() - dragSnapJob = animationScope.launch { - sheetHeightPx.snapTo(newHeight) - } + dragSheetHeightPx = newHeight } ) } @@ -995,7 +1010,7 @@ fun ChatAttachAlert( val selectionHeaderText = viewModel.selectionHeaderText() val selectionHeaderHeightPx = if (!isExpanded) { - (sheetHeightPx.value - collapsedStateHeightPx) + (interactiveSheetHeightPx - collapsedStateHeightPx) .coerceIn(0f, selectionHeaderExtraPx) } else 0f val selectionHeaderHeightDp = with(density) { selectionHeaderHeightPx.toDp() } diff --git a/app/src/main/java/com/rosetta/messenger/ui/chats/components/MediaPickerBottomSheet.kt b/app/src/main/java/com/rosetta/messenger/ui/chats/components/MediaPickerBottomSheet.kt index 3212333..d2f81e3 100644 --- a/app/src/main/java/com/rosetta/messenger/ui/chats/components/MediaPickerBottomSheet.kt +++ b/app/src/main/java/com/rosetta/messenger/ui/chats/components/MediaPickerBottomSheet.kt @@ -73,7 +73,6 @@ import com.rosetta.messenger.ui.utils.NavigationModeUtils import app.rosette.android.ui.keyboard.rememberKeyboardTransitionCoordinator import app.rosette.android.ui.keyboard.AnimatedKeyboardTransition import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.Job import kotlinx.coroutines.coroutineScope import kotlinx.coroutines.delay import kotlinx.coroutines.launch @@ -323,6 +322,12 @@ fun MediaPickerBottomSheet( // 🎬 Animatable для плавной высоты - КЛЮЧЕВОЙ ЭЛЕМЕНТ val sheetHeightPx = remember { Animatable(collapsedHeightPx) } + var dragSheetHeightPx by remember { mutableFloatStateOf(Float.NaN) } + val interactiveSheetHeightPx by remember { + derivedStateOf { + if (dragSheetHeightPx.isNaN()) sheetHeightPx.value else dragSheetHeightPx + } + } // Текущее состояние (для логики) var isExpanded by remember { mutableStateOf(false) } @@ -358,6 +363,7 @@ fun MediaPickerBottomSheet( closeAction = null // Сбрасываем высоту animationScope.launch { + dragSheetHeightPx = Float.NaN sheetHeightPx.snapTo(collapsedHeightPx) } action?.invoke() @@ -373,7 +379,7 @@ fun MediaPickerBottomSheet( targetValue = if (shouldShow && !isClosing) { // Базовое затемнение + немного больше когда развёрнуто val expandRange = (expandedHeightPx - collapsedStateHeightPx).coerceAtLeast(1f) - val expandProgress = (sheetHeightPx.value - collapsedStateHeightPx) / expandRange + val expandProgress = (interactiveSheetHeightPx - collapsedStateHeightPx) / expandRange 0.25f + 0.15f * expandProgress.coerceIn(0f, 1f) } else { 0f @@ -398,20 +404,21 @@ fun MediaPickerBottomSheet( shouldShow = true isClosing = false showAlbumMenu = false + dragSheetHeightPx = Float.NaN sheetHeightPx.snapTo(collapsedStateHeightPx) } } // Плавно синхронизируем высоту sheet с UI выбора (caption + send) в collapsed-состоянии. // When items are selected, the sheet grows UPWARD by 26dp to reveal the "N photos selected" header. - LaunchedEffect(showSheet, selectedItemOrder.isNotEmpty(), isExpanded, isClosing, editingItem) { - if (showSheet && editingItem == null && !isClosing && !isExpanded) { + LaunchedEffect(showSheet, selectedItemOrder.isNotEmpty(), isExpanded, isClosing, editingItem, dragSheetHeightPx) { + if (showSheet && editingItem == null && !isClosing && !isExpanded && dragSheetHeightPx.isNaN()) { val targetHeight = if (selectedItemOrder.isNotEmpty()) { collapsedStateHeightPx + selectionHeaderExtraPx } else { collapsedStateHeightPx } - if (kotlin.math.abs(sheetHeightPx.value - targetHeight) > 1f) { + if (kotlin.math.abs(interactiveSheetHeightPx - targetHeight) > 1f) { sheetHeightPx.stop() sheetHeightPx.animateTo( targetHeight, @@ -480,7 +487,12 @@ fun MediaPickerBottomSheet( // Используем velocity для определения направления fun snapToNearestState(velocity: Float = 0f) { animationScope.launch { - val currentHeight = sheetHeightPx.value + val currentHeight = interactiveSheetHeightPx + if (!dragSheetHeightPx.isNaN()) { + sheetHeightPx.stop() + sheetHeightPx.snapTo(currentHeight) + dragSheetHeightPx = Float.NaN + } // Пороги основаны на velocity (скорости свайпа) - не на позиции! // velocity < 0 = свайп вверх, velocity > 0 = свайп вниз @@ -560,7 +572,7 @@ fun MediaPickerBottomSheet( val isPickerFullScreen by remember { derivedStateOf { val expandRange = (expandedHeightPx - collapsedStateHeightPx).coerceAtLeast(1f) - val progress = ((sheetHeightPx.value - collapsedStateHeightPx) / expandRange).coerceIn(0f, 1f) + val progress = ((interactiveSheetHeightPx - collapsedStateHeightPx) / expandRange).coerceIn(0f, 1f) progress > 0.9f } } @@ -699,12 +711,12 @@ fun MediaPickerBottomSheet( // Subtract keyboard from sheet height so it fits in the resized viewport. // The grid (weight=1f) shrinks; caption bar stays at the bottom edge. val visibleSheetHeightPx = - (sheetHeightPx.value - appliedKeyboardInsetPx + navInsetPxForSheet) + (interactiveSheetHeightPx - appliedKeyboardInsetPx + navInsetPxForSheet) .coerceAtLeast(minHeightPx) val currentHeightDp = with(density) { visibleSheetHeightPx.toDp() } val slideOffset = (visibleSheetHeightPx * animatedOffset).toInt() val expandProgress = - ((sheetHeightPx.value - collapsedStateHeightPx) / + ((interactiveSheetHeightPx - collapsedStateHeightPx) / (expandedHeightPx - collapsedStateHeightPx).coerceAtLeast(1f)) .coerceIn(0f, 1f) val topCornerRadius by animateDpAsState( @@ -720,7 +732,6 @@ fun MediaPickerBottomSheet( // Отслеживаем velocity для плавного snap var lastDragVelocity by remember { mutableFloatStateOf(0f) } - var dragSnapJob by remember { mutableStateOf(null) } Box( modifier = Modifier @@ -742,9 +753,16 @@ fun MediaPickerBottomSheet( ) { /* Prevent click through */ } .pointerInput(Unit) { detectVerticalDragGestures( + onDragStart = { + animationScope.launch { sheetHeightPx.stop() } + dragSheetHeightPx = interactiveSheetHeightPx + }, + onDragCancel = { + snapToNearestState() + lastDragVelocity = 0f + }, onDragEnd = { // Snap с учётом velocity - dragSnapJob?.cancel() snapToNearestState(lastDragVelocity) lastDragVelocity = 0f }, @@ -756,12 +774,9 @@ fun MediaPickerBottomSheet( lastDragVelocity = adjustedDragAmount * 1.8f // 🔥 Меняем высоту в реальном времени - val newHeight = (sheetHeightPx.value - adjustedDragAmount) + val newHeight = (interactiveSheetHeightPx - adjustedDragAmount) .coerceIn(minHeightPx, expandedHeightPx) - dragSnapJob?.cancel() - dragSnapJob = animationScope.launch { - sheetHeightPx.snapTo(newHeight) - } + dragSheetHeightPx = newHeight } ) } @@ -809,7 +824,7 @@ fun MediaPickerBottomSheet( // Header height is DERIVED from how much the sheet has grown // beyond collapsedStateHeightPx — perfectly in sync, no jerk. val selectionHeaderHeightPx = if (!isExpanded) { - (sheetHeightPx.value - collapsedStateHeightPx) + (interactiveSheetHeightPx - collapsedStateHeightPx) .coerceIn(0f, selectionHeaderExtraPx) } else 0f val selectionHeaderHeightDp = with(density) { selectionHeaderHeightPx.toDp() } diff --git a/app/src/main/java/com/rosetta/messenger/ui/settings/ProfilePhotoPicker.kt b/app/src/main/java/com/rosetta/messenger/ui/settings/ProfilePhotoPicker.kt index 8582e5b..d73a404 100644 --- a/app/src/main/java/com/rosetta/messenger/ui/settings/ProfilePhotoPicker.kt +++ b/app/src/main/java/com/rosetta/messenger/ui/settings/ProfilePhotoPicker.kt @@ -88,9 +88,17 @@ fun ProfilePhotoPicker( var shouldShow by remember { mutableStateOf(false) } var isClosing by remember { mutableStateOf(false) } - // Animatable для высоты sheet с drag support - val sheetHeightPx = remember { Animatable(0f) } - val targetHeightPx = screenHeightPx * 0.92f // 92% экрана + var dragSheetOffsetPx by remember { mutableFloatStateOf(0f) } + var isDraggingSheet by remember { mutableStateOf(false) } + val animatedDragSheetOffsetPx by animateFloatAsState( + targetValue = dragSheetOffsetPx, + animationSpec = if (isDraggingSheet) tween(durationMillis = 0) + else spring( + dampingRatio = Spring.DampingRatioNoBouncy, + stiffness = Spring.StiffnessMediumLow + ), + label = "profile_picker_drag_offset" + ) // Permission launcher val permissionLauncher = rememberLauncherForActivityResult( @@ -140,9 +148,8 @@ fun ProfilePhotoPicker( if (isClosing) { isClosing = false shouldShow = false - scope.launch { - sheetHeightPx.snapTo(0f) - } + dragSheetOffsetPx = 0f + isDraggingSheet = false onDismiss() } }, @@ -164,7 +171,8 @@ fun ProfilePhotoPicker( if (isVisible) { shouldShow = true isClosing = false - sheetHeightPx.snapTo(targetHeightPx) + dragSheetOffsetPx = 0f + isDraggingSheet = false } } @@ -235,24 +243,32 @@ fun ProfilePhotoPicker( .fillMaxWidth() .fillMaxHeight(0.92f) .offset { - IntOffset(0, (screenHeightPx * animatedOffset).roundToInt()) + IntOffset( + 0, + (screenHeightPx * animatedOffset + animatedDragSheetOffsetPx).roundToInt() + ) } .clip(RoundedCornerShape(topStart = 16.dp, topEnd = 16.dp)) .pointerInput(Unit) { // Drag to dismiss - var totalDrag = 0f detectVerticalDragGestures( - onDragStart = { totalDrag = 0f }, + onDragStart = { isDraggingSheet = true }, onDragEnd = { - if (totalDrag > 150f) { + isDraggingSheet = false + if (dragSheetOffsetPx > screenHeightPx * 0.16f) { animatedClose() + } else { + dragSheetOffsetPx = 0f } }, - onDragCancel = { totalDrag = 0f }, - onVerticalDrag = { _, dragAmount -> - if (dragAmount > 0) { // Only downward - totalDrag += dragAmount - } + onDragCancel = { + isDraggingSheet = false + dragSheetOffsetPx = 0f + }, + onVerticalDrag = { change, dragAmount -> + change.consume() + dragSheetOffsetPx = + (dragSheetOffsetPx + dragAmount).coerceIn(0f, screenHeightPx) } ) },