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
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
)
}
}

View File

@@ -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<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,
onUserSelect: (com.rosetta.messenger.network.SearchUser) -> Unit = {},
backgroundBlurColorId: String = "avatar",
pinnedChats: Set<String> = 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<DialogUiModel> { pinnedChats.contains(it.opponentKey) }
.thenByDescending { it.lastMessageTimestamp }
)
}
// Telegram-style: only one item can be swiped open at a time
var swipedItemKey by remember { mutableStateOf<String?>(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)
)
}
}
}
}

View File

@@ -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)
)
}
}

View File

@@ -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 - плавно возвращаемся