feat: Update send icon to ArrowUp in TelegramCaptionBar and MultiImageEditorScreen

This commit is contained in:
k1ngsterr1
2026-02-07 19:34:16 +05:00
parent fdc4f42e1d
commit 0eddd448c7
5 changed files with 169 additions and 42 deletions

View File

@@ -551,6 +551,7 @@ fun MainScreen(
// Appearance: background blur color preference // Appearance: background blur color preference
val prefsManager = remember { com.rosetta.messenger.data.PreferencesManager(context) } val prefsManager = remember { com.rosetta.messenger.data.PreferencesManager(context) }
val backgroundBlurColorId by prefsManager.backgroundBlurColorId.collectAsState(initial = "avatar") val backgroundBlurColorId by prefsManager.backgroundBlurColorId.collectAsState(initial = "avatar")
val pinnedChats by prefsManager.pinnedChats.collectAsState(initial = emptySet())
// AvatarRepository для работы с аватарами // AvatarRepository для работы с аватарами
val avatarRepository = remember(accountPublicKey) { val avatarRepository = remember(accountPublicKey) {
@@ -614,6 +615,12 @@ fun MainScreen(
}, },
onUserSelect = { selectedChatUser -> selectedUser = selectedChatUser }, onUserSelect = { selectedChatUser -> selectedUser = selectedChatUser },
backgroundBlurColorId = backgroundBlurColorId, backgroundBlurColorId = backgroundBlurColorId,
pinnedChats = pinnedChats,
onTogglePin = { opponentKey ->
mainScreenScope.launch {
prefsManager.togglePinChat(opponentKey)
}
},
avatarRepository = avatarRepository, avatarRepository = avatarRepository,
onLogout = onLogout onLogout = onLogout
) )
@@ -905,8 +912,7 @@ fun MainScreen(
showOtherProfileScreen = false showOtherProfileScreen = false
selectedOtherUser = null selectedOtherUser = null
}, },
avatarRepository = avatarRepository, avatarRepository = avatarRepository
backgroundBlurColorId = backgroundBlurColorId
) )
} }
} }

View File

@@ -7,6 +7,7 @@ import androidx.datastore.preferences.core.booleanPreferencesKey
import androidx.datastore.preferences.core.edit import androidx.datastore.preferences.core.edit
import androidx.datastore.preferences.core.intPreferencesKey import androidx.datastore.preferences.core.intPreferencesKey
import androidx.datastore.preferences.core.stringPreferencesKey import androidx.datastore.preferences.core.stringPreferencesKey
import androidx.datastore.preferences.core.stringSetPreferencesKey
import androidx.datastore.preferences.preferencesDataStore import androidx.datastore.preferences.preferencesDataStore
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.map
@@ -45,6 +46,9 @@ class PreferencesManager(private val context: Context) {
// Appearance / Customization // Appearance / Customization
val BACKGROUND_BLUR_COLOR_ID = stringPreferencesKey("background_blur_color_id") // id from BackgroundBlurPresets 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) { suspend fun setBackgroundBlurColorId(value: String) {
context.dataStore.edit { preferences -> preferences[BACKGROUND_BLUR_COLOR_ID] = value } context.dataStore.edit { preferences -> preferences[BACKGROUND_BLUR_COLOR_ID] = value }
} }
// ═════════════════════════════════════════════════════════════
// 📌 PINNED CHATS
// ═════════════════════════════════════════════════════════════
val pinnedChats: Flow<Set<String>> =
context.dataStore.data.map { preferences ->
preferences[PINNED_CHATS] ?: emptySet()
}
suspend fun setPinnedChats(value: Set<String>) {
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
}
} }

View File

@@ -169,6 +169,8 @@ fun ChatsListScreen(
onNewChat: () -> Unit, onNewChat: () -> Unit,
onUserSelect: (com.rosetta.messenger.network.SearchUser) -> Unit = {}, onUserSelect: (com.rosetta.messenger.network.SearchUser) -> Unit = {},
backgroundBlurColorId: String = "avatar", backgroundBlurColorId: String = "avatar",
pinnedChats: Set<String> = emptySet(),
onTogglePin: (String) -> Unit = {},
chatsViewModel: ChatsListViewModel = androidx.lifecycle.viewmodel.compose.viewModel(), chatsViewModel: ChatsListViewModel = androidx.lifecycle.viewmodel.compose.viewModel(),
avatarRepository: com.rosetta.messenger.repository.AvatarRepository? = null, avatarRepository: com.rosetta.messenger.repository.AvatarRepository? = null,
onLogout: () -> Unit onLogout: () -> Unit
@@ -961,7 +963,13 @@ android.util.Log.d("ChatsListScreen", "✅ Total LaunchedEffect: ${System.curren
val listBackgroundColor = if (isDarkTheme) Color(0xFF1A1A1A) else Color(0xFFF2F2F7) val listBackgroundColor = if (isDarkTheme) Color(0xFF1A1A1A) else Color(0xFFF2F2F7)
// 🔥 Берем dialogs из chatsState для // 🔥 Берем dialogs из chatsState для
// консистентности // консистентности
val currentDialogs = chatsState.dialogs // 📌 Сортируем: pinned сначала, потом по времени
val currentDialogs = remember(chatsState.dialogs, pinnedChats) {
chatsState.dialogs.sortedWith(
compareByDescending<DialogUiModel> { pinnedChats.contains(it.opponentKey) }
.thenByDescending { it.lastMessageTimestamp }
)
}
// Telegram-style: only one item can be swiped open at a time // Telegram-style: only one item can be swiped open at a time
var swipedItemKey by remember { mutableStateOf<String?>(null) } var swipedItemKey by remember { mutableStateOf<String?>(null) }
@@ -1074,7 +1082,9 @@ android.util.Log.d("ChatsListScreen", "✅ Total LaunchedEffect: ${System.curren
onUnblock = { onUnblock = {
dialogToUnblock = dialogToUnblock =
dialog dialog
} },
isPinned = pinnedChats.contains(dialog.opponentKey),
onPin = { onTogglePin(dialog.opponentKey) }
) )
// 🔥 СЕПАРАТОР - // 🔥 СЕПАРАТОР -
@@ -1516,11 +1526,19 @@ fun SwipeableDialogItem(
onClick: () -> Unit, onClick: () -> Unit,
onDelete: () -> Unit = {}, onDelete: () -> Unit = {},
onBlock: () -> 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) } 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 density = androidx.compose.ui.platform.LocalDensity.current
val swipeWidthPx = with(density) { swipeWidthDp.toPx() } val swipeWidthPx = with(density) { swipeWidthDp.toPx() }
@@ -1551,6 +1569,43 @@ fun SwipeableDialogItem(
.height(itemHeight) .height(itemHeight)
.width(swipeWidthDp) .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) // Кнопка Block/Unblock (только если не Saved Messages)
if (!isSavedMessages) { if (!isSavedMessages) {
Box( Box(
@@ -1604,7 +1659,6 @@ fun SwipeableDialogItem(
.fillMaxHeight() .fillMaxHeight()
.background(PrimaryBlue) .background(PrimaryBlue)
.clickable { .clickable {
// Закрываем свайп мгновенно перед удалением
offsetX = 0f offsetX = 0f
onSwipeClosed() onSwipeClosed()
onDelete() onDelete()
@@ -1693,6 +1747,7 @@ fun SwipeableDialogItem(
dialog = dialog, dialog = dialog,
isDarkTheme = isDarkTheme, isDarkTheme = isDarkTheme,
isTyping = isTyping, isTyping = isTyping,
isPinned = isPinned,
avatarRepository = avatarRepository, avatarRepository = avatarRepository,
onClick = onClick onClick = onClick
) )
@@ -1714,6 +1769,7 @@ fun DialogItemContent(
dialog: DialogUiModel, dialog: DialogUiModel,
isDarkTheme: Boolean, isDarkTheme: Boolean,
isTyping: Boolean = false, isTyping: Boolean = false,
isPinned: Boolean = false,
avatarRepository: com.rosetta.messenger.repository.AvatarRepository? = null, avatarRepository: com.rosetta.messenger.repository.AvatarRepository? = null,
onClick: () -> Unit onClick: () -> Unit
) { ) {
@@ -1944,9 +2000,8 @@ fun DialogItemContent(
Modifier.width(4.dp) Modifier.width(4.dp)
) )
} }
else -> { 1 -> {
// DELIVERED (1) или SENDING (0) - // DELIVERED - одна серая галочка
// одна серая галочка
Icon( Icon(
imageVector = imageVector =
TablerIcons.Check, TablerIcons.Check,
@@ -1965,6 +2020,26 @@ fun DialogItemContent(
Modifier.width(4.dp) 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)
)
}
} }
} }
} }

View File

@@ -1152,12 +1152,11 @@ private fun TelegramCaptionBar(
Box(contentAlignment = Alignment.Center) { Box(contentAlignment = Alignment.Center) {
// Стрелка отправки - видна когда compact (progress близок к 0) // Стрелка отправки - видна когда compact (progress близок к 0)
Icon( Icon(
TablerIcons.Send, TablerIcons.ArrowUp,
contentDescription = "Send", contentDescription = "Send",
tint = Color.White.copy(alpha = 1f - progress), tint = Color.White.copy(alpha = 1f - progress),
modifier = Modifier modifier = Modifier
.size(iconSize) .size(iconSize)
.offset(x = 1.dp)
.graphicsLayer { alpha = 1f - progress } .graphicsLayer { alpha = 1f - progress }
) )
// Галочка - видна когда expanded (progress близок к 1) // Галочка - видна когда expanded (progress близок к 1)
@@ -1932,12 +1931,11 @@ fun MultiImageEditorScreen(
contentAlignment = Alignment.Center contentAlignment = Alignment.Center
) { ) {
Icon( Icon(
TablerIcons.Send, TablerIcons.ArrowUp,
contentDescription = "Send", contentDescription = "Send",
tint = Color.White, tint = Color.White,
modifier = Modifier modifier = Modifier
.size(22.dp) .size(22.dp)
.offset(x = 1.dp)
) )
} }
} }

View File

@@ -273,35 +273,21 @@ fun ImageViewerScreen(
} }
} }
// 🎬 Shared element dismiss - возврат к исходному положению // 🎬 Swipe dismiss - плавный fade out на месте
fun smoothDismiss() { fun smoothDismiss() {
if (isClosing) return if (isClosing) return
isClosing = true isClosing = true
onClosingStart() // Сразу сбрасываем status bar onClosingStart()
scope.launch { scope.launch {
// Сначала сбрасываем offset от drag // Простой быстрый fade out — как в Telegram
animatedOffsetY.snapTo(0f) dismissAlpha.animateTo(
targetValue = 0f,
if (sourceBounds != null && pagerState.currentPage == initialIndex) { animationSpec = tween(
// 🔥 Telegram-style: возвращаемся к исходному положению с shared element durationMillis = 200,
animationProgress.animateTo( easing = FastOutSlowInEasing
targetValue = 0f,
animationSpec = tween(
durationMillis = 280,
easing = FastOutSlowInEasing
)
) )
} else { )
// Fallback: простой fade-out если пролистнули на другое фото
dismissAlpha.animateTo(
targetValue = 0f,
animationSpec = tween(
durationMillis = 250,
easing = LinearEasing
)
)
}
onDismiss() onDismiss()
} }
} }
@@ -321,7 +307,7 @@ fun ImageViewerScreen(
// Alpha на основе drag прогресса // Alpha на основе drag прогресса
val dismissProgress = (animatedOffsetY.value.absoluteValue / 300f).coerceIn(0f, 1f) 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 // Current image info
val currentImage = images.getOrNull(pagerState.currentPage) val currentImage = images.getOrNull(pagerState.currentPage)
@@ -407,7 +393,25 @@ fun ImageViewerScreen(
ZoomableImage( ZoomableImage(
image = image, image = image,
privateKey = privateKey, 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 -> onVerticalDrag = { dragAmount, velocity ->
dragOffsetY += dragAmount dragOffsetY += dragAmount
dragVelocity = velocity dragVelocity = velocity
@@ -530,7 +534,7 @@ fun ImageViewerScreen(
private fun ZoomableImage( private fun ZoomableImage(
image: ViewableImage, image: ViewableImage,
privateKey: String, privateKey: String,
onTap: () -> Unit, onTap: (Offset) -> Unit,
onVerticalDrag: (Float, Float) -> Unit = { _, _ -> }, // dragAmount, velocity onVerticalDrag: (Float, Float) -> Unit = { _, _ -> }, // dragAmount, velocity
onDragEnd: () -> Unit = {} onDragEnd: () -> Unit = {}
) { ) {
@@ -662,7 +666,7 @@ private fun ZoomableImage(
.onSizeChanged { containerSize = it } .onSizeChanged { containerSize = it }
.pointerInput(Unit) { .pointerInput(Unit) {
detectTapGestures( detectTapGestures(
onTap = { onTap() }, onTap = { offset -> onTap(offset) },
onDoubleTap = { tapOffset -> onDoubleTap = { tapOffset ->
if (scale > 1.1f) { if (scale > 1.1f) {
// Zoom out - плавно возвращаемся // Zoom out - плавно возвращаемся