From 0eddd448c7477640429a84e73b21f0d2a90fc043 Mon Sep 17 00:00:00 2001 From: k1ngsterr1 Date: Sat, 7 Feb 2026 19:34:16 +0500 Subject: [PATCH] feat: Update send icon to ArrowUp in TelegramCaptionBar and MultiImageEditorScreen --- .../com/rosetta/messenger/MainActivity.kt | 10 +- .../messenger/data/PreferencesManager.kt | 33 ++++++ .../messenger/ui/chats/ChatsListScreen.kt | 104 ++++++++++++++++-- .../ui/chats/components/ImageEditorScreen.kt | 6 +- .../ui/chats/components/ImageViewerScreen.kt | 58 +++++----- 5 files changed, 169 insertions(+), 42 deletions(-) diff --git a/app/src/main/java/com/rosetta/messenger/MainActivity.kt b/app/src/main/java/com/rosetta/messenger/MainActivity.kt index 11959f8..eb974e9 100644 --- a/app/src/main/java/com/rosetta/messenger/MainActivity.kt +++ b/app/src/main/java/com/rosetta/messenger/MainActivity.kt @@ -551,6 +551,7 @@ fun MainScreen( // Appearance: background blur color preference val prefsManager = remember { com.rosetta.messenger.data.PreferencesManager(context) } val backgroundBlurColorId by prefsManager.backgroundBlurColorId.collectAsState(initial = "avatar") + val pinnedChats by prefsManager.pinnedChats.collectAsState(initial = emptySet()) // AvatarRepository для работы с аватарами val avatarRepository = remember(accountPublicKey) { @@ -614,6 +615,12 @@ fun MainScreen( }, onUserSelect = { selectedChatUser -> selectedUser = selectedChatUser }, backgroundBlurColorId = backgroundBlurColorId, + pinnedChats = pinnedChats, + onTogglePin = { opponentKey -> + mainScreenScope.launch { + prefsManager.togglePinChat(opponentKey) + } + }, avatarRepository = avatarRepository, onLogout = onLogout ) @@ -905,8 +912,7 @@ fun MainScreen( showOtherProfileScreen = false selectedOtherUser = null }, - avatarRepository = avatarRepository, - backgroundBlurColorId = backgroundBlurColorId + avatarRepository = avatarRepository ) } } diff --git a/app/src/main/java/com/rosetta/messenger/data/PreferencesManager.kt b/app/src/main/java/com/rosetta/messenger/data/PreferencesManager.kt index b025494..e48caf5 100644 --- a/app/src/main/java/com/rosetta/messenger/data/PreferencesManager.kt +++ b/app/src/main/java/com/rosetta/messenger/data/PreferencesManager.kt @@ -7,6 +7,7 @@ import androidx.datastore.preferences.core.booleanPreferencesKey import androidx.datastore.preferences.core.edit import androidx.datastore.preferences.core.intPreferencesKey import androidx.datastore.preferences.core.stringPreferencesKey +import androidx.datastore.preferences.core.stringSetPreferencesKey import androidx.datastore.preferences.preferencesDataStore import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.map @@ -45,6 +46,9 @@ class PreferencesManager(private val context: Context) { // Appearance / Customization val BACKGROUND_BLUR_COLOR_ID = stringPreferencesKey("background_blur_color_id") // id from BackgroundBlurPresets + + // Pinned Chats (max 3) + val PINNED_CHATS = stringSetPreferencesKey("pinned_chats") // Set of opponent public keys } // ═════════════════════════════════════════════════════════════ @@ -205,4 +209,33 @@ class PreferencesManager(private val context: Context) { suspend fun setBackgroundBlurColorId(value: String) { context.dataStore.edit { preferences -> preferences[BACKGROUND_BLUR_COLOR_ID] = value } } + + // ═════════════════════════════════════════════════════════════ + // 📌 PINNED CHATS + // ═════════════════════════════════════════════════════════════ + + val pinnedChats: Flow> = + context.dataStore.data.map { preferences -> + preferences[PINNED_CHATS] ?: emptySet() + } + + suspend fun setPinnedChats(value: Set) { + context.dataStore.edit { preferences -> preferences[PINNED_CHATS] = value } + } + + suspend fun togglePinChat(opponentKey: String): Boolean { + var wasPinned = false + context.dataStore.edit { preferences -> + val current = preferences[PINNED_CHATS] ?: emptySet() + if (current.contains(opponentKey)) { + // Unpin + preferences[PINNED_CHATS] = current - opponentKey + wasPinned = true + } else if (current.size < 3) { + // Pin (max 3) + preferences[PINNED_CHATS] = current + opponentKey + } + } + return wasPinned + } } 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 38efb4f..df493b0 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 @@ -169,6 +169,8 @@ fun ChatsListScreen( onNewChat: () -> Unit, onUserSelect: (com.rosetta.messenger.network.SearchUser) -> Unit = {}, backgroundBlurColorId: String = "avatar", + pinnedChats: Set = emptySet(), + onTogglePin: (String) -> Unit = {}, chatsViewModel: ChatsListViewModel = androidx.lifecycle.viewmodel.compose.viewModel(), avatarRepository: com.rosetta.messenger.repository.AvatarRepository? = null, onLogout: () -> Unit @@ -961,7 +963,13 @@ android.util.Log.d("ChatsListScreen", "✅ Total LaunchedEffect: ${System.curren val listBackgroundColor = if (isDarkTheme) Color(0xFF1A1A1A) else Color(0xFFF2F2F7) // 🔥 Берем dialogs из chatsState для // консистентности - val currentDialogs = chatsState.dialogs + // 📌 Сортируем: pinned сначала, потом по времени + val currentDialogs = remember(chatsState.dialogs, pinnedChats) { + chatsState.dialogs.sortedWith( + compareByDescending { pinnedChats.contains(it.opponentKey) } + .thenByDescending { it.lastMessageTimestamp } + ) + } // Telegram-style: only one item can be swiped open at a time var swipedItemKey by remember { mutableStateOf(null) } @@ -1074,7 +1082,9 @@ android.util.Log.d("ChatsListScreen", "✅ Total LaunchedEffect: ${System.curren onUnblock = { dialogToUnblock = dialog - } + }, + isPinned = pinnedChats.contains(dialog.opponentKey), + onPin = { onTogglePin(dialog.opponentKey) } ) // 🔥 СЕПАРАТОР - @@ -1516,11 +1526,19 @@ fun SwipeableDialogItem( onClick: () -> Unit, onDelete: () -> Unit = {}, onBlock: () -> Unit = {}, - onUnblock: () -> Unit = {} + onUnblock: () -> Unit = {}, + isPinned: Boolean = false, + onPin: () -> Unit = {} ) { - val backgroundColor = if (isDarkTheme) Color(0xFF1A1A1A) else Color(0xFFF2F2F7) + val backgroundColor = if (isPinned) { + if (isDarkTheme) Color(0xFF232323) else Color(0xFFE8E8ED) + } else { + if (isDarkTheme) Color(0xFF1A1A1A) else Color(0xFFF2F2F7) + } var offsetX by remember { mutableStateOf(0f) } - val swipeWidthDp = if (isSavedMessages) 80.dp else 160.dp + // 📌 3 кнопки: Pin + Block/Unblock + Delete (для SavedMessages: Pin + Delete) + val buttonCount = if (isSavedMessages) 2 else 3 + val swipeWidthDp = (buttonCount * 80).dp val density = androidx.compose.ui.platform.LocalDensity.current val swipeWidthPx = with(density) { swipeWidthDp.toPx() } @@ -1551,6 +1569,43 @@ fun SwipeableDialogItem( .height(itemHeight) .width(swipeWidthDp) ) { + // 📌 Кнопка Pin/Unpin + Box( + modifier = + Modifier.width(80.dp) + .fillMaxHeight() + .background(Color(0xFFFF9500)) // iOS orange + .clickable { + onPin() + offsetX = 0f + onSwipeClosed() + }, + contentAlignment = Alignment.Center + ) { + Column( + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.Center + ) { + Icon( + imageVector = + if (isPinned) TablerIcons.PinnedOff + else TablerIcons.Pin, + contentDescription = + if (isPinned) "Unpin" else "Pin", + tint = Color.White, + modifier = Modifier.size(22.dp) + ) + Spacer(modifier = Modifier.height(4.dp)) + Text( + text = + if (isPinned) "Unpin" else "Pin", + color = Color.White, + fontSize = 12.sp, + fontWeight = FontWeight.SemiBold + ) + } + } + // Кнопка Block/Unblock (только если не Saved Messages) if (!isSavedMessages) { Box( @@ -1604,7 +1659,6 @@ fun SwipeableDialogItem( .fillMaxHeight() .background(PrimaryBlue) .clickable { - // Закрываем свайп мгновенно перед удалением offsetX = 0f onSwipeClosed() onDelete() @@ -1693,6 +1747,7 @@ fun SwipeableDialogItem( dialog = dialog, isDarkTheme = isDarkTheme, isTyping = isTyping, + isPinned = isPinned, avatarRepository = avatarRepository, onClick = onClick ) @@ -1714,6 +1769,7 @@ fun DialogItemContent( dialog: DialogUiModel, isDarkTheme: Boolean, isTyping: Boolean = false, + isPinned: Boolean = false, avatarRepository: com.rosetta.messenger.repository.AvatarRepository? = null, onClick: () -> Unit ) { @@ -1944,9 +2000,8 @@ fun DialogItemContent( Modifier.width(4.dp) ) } - else -> { - // DELIVERED (1) или SENDING (0) - - // одна серая галочка + 1 -> { + // DELIVERED - одна серая галочка Icon( imageVector = TablerIcons.Check, @@ -1965,6 +2020,26 @@ fun DialogItemContent( Modifier.width(4.dp) ) } + else -> { + // SENDING (0) - часики + Icon( + imageVector = + TablerIcons.Clock, + contentDescription = "Sending", + tint = + secondaryTextColor + .copy( + alpha = + 0.6f + ), + modifier = + Modifier.size(14.dp) + ) + Spacer( + modifier = + Modifier.width(4.dp) + ) + } } } @@ -2049,6 +2124,17 @@ fun DialogItemContent( ) } } + + // 📌 Pin icon + if (isPinned) { + Spacer(modifier = Modifier.width(6.dp)) + Icon( + imageVector = TablerIcons.Pin, + contentDescription = "Pinned", + tint = secondaryTextColor.copy(alpha = 0.5f), + modifier = Modifier.size(16.dp) + ) + } } } } diff --git a/app/src/main/java/com/rosetta/messenger/ui/chats/components/ImageEditorScreen.kt b/app/src/main/java/com/rosetta/messenger/ui/chats/components/ImageEditorScreen.kt index 2cc6a3d..c090e72 100644 --- a/app/src/main/java/com/rosetta/messenger/ui/chats/components/ImageEditorScreen.kt +++ b/app/src/main/java/com/rosetta/messenger/ui/chats/components/ImageEditorScreen.kt @@ -1152,12 +1152,11 @@ private fun TelegramCaptionBar( Box(contentAlignment = Alignment.Center) { // Стрелка отправки - видна когда compact (progress близок к 0) Icon( - TablerIcons.Send, + TablerIcons.ArrowUp, contentDescription = "Send", tint = Color.White.copy(alpha = 1f - progress), modifier = Modifier .size(iconSize) - .offset(x = 1.dp) .graphicsLayer { alpha = 1f - progress } ) // Галочка - видна когда expanded (progress близок к 1) @@ -1932,12 +1931,11 @@ fun MultiImageEditorScreen( contentAlignment = Alignment.Center ) { Icon( - TablerIcons.Send, + TablerIcons.ArrowUp, contentDescription = "Send", tint = Color.White, modifier = Modifier .size(22.dp) - .offset(x = 1.dp) ) } } diff --git a/app/src/main/java/com/rosetta/messenger/ui/chats/components/ImageViewerScreen.kt b/app/src/main/java/com/rosetta/messenger/ui/chats/components/ImageViewerScreen.kt index e5f2c6c..0337410 100644 --- a/app/src/main/java/com/rosetta/messenger/ui/chats/components/ImageViewerScreen.kt +++ b/app/src/main/java/com/rosetta/messenger/ui/chats/components/ImageViewerScreen.kt @@ -273,35 +273,21 @@ fun ImageViewerScreen( } } - // 🎬 Shared element dismiss - возврат к исходному положению + // 🎬 Swipe dismiss - плавный fade out на месте fun smoothDismiss() { if (isClosing) return isClosing = true - onClosingStart() // Сразу сбрасываем status bar + onClosingStart() scope.launch { - // Сначала сбрасываем offset от drag - animatedOffsetY.snapTo(0f) - - if (sourceBounds != null && pagerState.currentPage == initialIndex) { - // 🔥 Telegram-style: возвращаемся к исходному положению с shared element - animationProgress.animateTo( - targetValue = 0f, - animationSpec = tween( - durationMillis = 280, - easing = FastOutSlowInEasing - ) + // Простой быстрый fade out — как в Telegram + dismissAlpha.animateTo( + targetValue = 0f, + animationSpec = tween( + durationMillis = 200, + easing = FastOutSlowInEasing ) - } else { - // Fallback: простой fade-out если пролистнули на другое фото - dismissAlpha.animateTo( - targetValue = 0f, - animationSpec = tween( - durationMillis = 250, - easing = LinearEasing - ) - ) - } + ) onDismiss() } } @@ -321,7 +307,7 @@ fun ImageViewerScreen( // Alpha на основе drag прогресса val dismissProgress = (animatedOffsetY.value.absoluteValue / 300f).coerceIn(0f, 1f) - val backgroundAlpha = animationProgress.value * dismissAlpha.value * (1f - dismissProgress * 0.5f) + val backgroundAlpha = animationProgress.value * dismissAlpha.value * (1f - dismissProgress * 0.7f) // Current image info val currentImage = images.getOrNull(pagerState.currentPage) @@ -407,7 +393,25 @@ fun ImageViewerScreen( ZoomableImage( image = image, privateKey = privateKey, - onTap = { showControls = !showControls }, + onTap = { tapOffset -> + // 👆 Tap on left/right edge (20% zone) to navigate + val edgeZone = screenSize.width * 0.20f + val tapX = tapOffset.x + val screenW = screenSize.width.toFloat() + when { + tapX < edgeZone && pagerState.currentPage > 0 -> { + scope.launch { + pagerState.scrollToPage(pagerState.currentPage - 1) + } + } + tapX > screenW - edgeZone && pagerState.currentPage < images.size - 1 -> { + scope.launch { + pagerState.scrollToPage(pagerState.currentPage + 1) + } + } + else -> showControls = !showControls + } + }, onVerticalDrag = { dragAmount, velocity -> dragOffsetY += dragAmount dragVelocity = velocity @@ -530,7 +534,7 @@ fun ImageViewerScreen( private fun ZoomableImage( image: ViewableImage, privateKey: String, - onTap: () -> Unit, + onTap: (Offset) -> Unit, onVerticalDrag: (Float, Float) -> Unit = { _, _ -> }, // dragAmount, velocity onDragEnd: () -> Unit = {} ) { @@ -662,7 +666,7 @@ private fun ZoomableImage( .onSizeChanged { containerSize = it } .pointerInput(Unit) { detectTapGestures( - onTap = { onTap() }, + onTap = { offset -> onTap(offset) }, onDoubleTap = { tapOffset -> if (scale > 1.1f) { // Zoom out - плавно возвращаемся