feat: Update send icon to ArrowUp in TelegramCaptionBar and MultiImageEditorScreen
This commit is contained in:
@@ -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
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 - плавно возвращаемся
|
||||
|
||||
Reference in New Issue
Block a user