From 7d4b9a8fc438bac09412e8ddd7c4a2f899d2c599 Mon Sep 17 00:00:00 2001 From: k1ngsterr1 Date: Sat, 4 Apr 2026 15:52:54 +0500 Subject: [PATCH] =?UTF-8?q?=D0=A0=D0=B5=D0=BB=D0=B8=D0=B7=201.4.5:=20?= =?UTF-8?q?=D1=81=D1=82=D0=B0=D0=B1=D0=B8=D0=BB=D0=B8=D0=B7=D0=B0=D1=86?= =?UTF-8?q?=D0=B8=D1=8F=20=D0=B7=D0=B2=D0=BE=D0=BD=D0=BA=D0=BE=D0=B2,=20?= =?UTF-8?q?=D1=84=D0=B8=D0=BA=D1=81=D1=8B=20UI?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Звонок не сбрасывается при переподключении WebSocket - Убрано мелькание "Unknown" при завершении (флаг resetting) - Фикс placeholderColor в ChatDetailScreen (release build) - ReleaseNotes.kt обновлён с детальным описанием всех изменений --- app/build.gradle.kts | 4 +- .../com/rosetta/messenger/MainActivity.kt | 9 - .../rosetta/messenger/data/ReleaseNotes.kt | 30 ++- .../messenger/ui/chats/ChatDetailScreen.kt | 211 ++++++++---------- .../messenger/ui/chats/ChatsListScreen.kt | 66 +----- .../messenger/ui/chats/RequestsListScreen.kt | 1 - .../ui/chats/input/ChatDetailInput.kt | 16 +- .../ui/components/AppleEmojiEditText.kt | 18 ++ 8 files changed, 150 insertions(+), 205 deletions(-) diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 9b5279a..9ff5a1a 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -23,8 +23,8 @@ val gitShortSha = safeGitOutput("rev-parse", "--short", "HEAD") ?: "unknown" // ═══════════════════════════════════════════════════════════ // Rosetta versioning — bump here on each release // ═══════════════════════════════════════════════════════════ -val rosettaVersionName = "1.4.4" -val rosettaVersionCode = 46 // Increment on each release +val rosettaVersionName = "1.4.5" +val rosettaVersionCode = 47 // Increment on each release val customWebRtcAar = file("libs/libwebrtc-custom.aar") android { diff --git a/app/src/main/java/com/rosetta/messenger/MainActivity.kt b/app/src/main/java/com/rosetta/messenger/MainActivity.kt index 971c3a4..bff1edb 100644 --- a/app/src/main/java/com/rosetta/messenger/MainActivity.kt +++ b/app/src/main/java/com/rosetta/messenger/MainActivity.kt @@ -1122,18 +1122,13 @@ fun MainScreen( accountName = accountName, accountUsername = accountUsername, accountVerified = accountVerified, - accountPhone = accountPhone, accountPublicKey = accountPublicKey, accountPrivateKey = accountPrivateKey, - privateKeyHash = privateKeyHash, onToggleTheme = onToggleTheme, onProfileClick = { pushScreen(Screen.Profile) }, onNewGroupClick = { pushScreen(Screen.GroupSetup) }, - onContactsClick = { - // TODO: Navigate to contacts - }, onCallsClick = { // TODO: Navigate to calls }, @@ -1152,9 +1147,6 @@ fun MainScreen( ) }, onSettingsClick = { pushScreen(Screen.Profile) }, - onInviteFriendsClick = { - // TODO: Share invite link - }, onSearchClick = { pushScreen(Screen.Search) }, onRequestsClick = { pushScreen(Screen.Requests) }, onNewChat = { @@ -1166,7 +1158,6 @@ fun MainScreen( onStartCall = { user -> startCallWithPermission(user) }, - backgroundBlurColorId = backgroundBlurColorId, pinnedChats = pinnedChats, onTogglePin = { opponentKey -> mainScreenScope.launch { prefsManager.togglePinChat(opponentKey) } 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 3767654..919d48a 100644 --- a/app/src/main/java/com/rosetta/messenger/data/ReleaseNotes.kt +++ b/app/src/main/java/com/rosetta/messenger/data/ReleaseNotes.kt @@ -18,15 +18,31 @@ object ReleaseNotes { Update v$VERSION_PLACEHOLDER Звонки - - Обновлён протокол WebRTC: publicKey и deviceId в каждом пакете - - Полноэкранный входящий звонок на экране блокировки - - Фикс бесконечного "Exchanging keys" при принятии звонка - - Фикс краша ForegroundService при исходящем звонке - - Кастомный WebRTC с E2EE теперь работает в CI-сборках + - Полноэкранный входящий звонок (IncomingCallActivity) поверх экрана блокировки с кнопками Принять/Отклонить + - Обновлён протокол WebRTC: publicKey и deviceId в каждом пакете (совместимость с новым сервером) + - Звонок больше не сбрасывается при переподключении WebSocket + - Исправлен бесконечный статус "Exchanging keys" — KEY_EXCHANGE отправляется с ретраем до 6 сек + - Автоматическая привязка аккаунта при принятии звонка из push-уведомления + - Исправлен краш ForegroundService при исходящем звонке (safeStopForeground) + - Убрано мелькание "Unknown" при завершении звонка + - Кнопка Decline теперь работает во всех фазах звонка + - Баннер активного звонка теперь отображается внутри диалога + - Дедупликация push + WebSocket сигналов (без мерцания уведомлений) + - Защита от фантомных звонков при принятии на другом устройстве + - Корректное освобождение PeerConnection (dispose) при завершении звонка + - Кастомный WebRTC AAR с E2EE добавлен в репозиторий для CI-сборок + - Диагностические логи звонков и уведомлений в rosettadev1 Уведомления - - Аватарки и имена в уведомлениях - - Настройка отключения аватарок в уведомлениях + - Аватарки и имена пользователей в уведомлениях о сообщениях и звонках + - Настройка включения/выключения аватарок в уведомлениях (Notifications → Avatars in Notifications) + - Сохранение FCM токена в rosettadev1 для диагностики + - Поддержка tokenType и deviceId в push-подписке + + Интерфейс + - Ограничение масштаба шрифта до 1.3x — вёрстка не ломается на телефонах с огромным текстом + - Новые обои: Light 1-3 для светлой темы, Dark 1-3 для тёмной темы + - Убраны старые обои, исправлено растяжение превью обоев """.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 6985719..7b6d209 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 @@ -368,9 +368,6 @@ fun ChatDetailScreen( // 🎨 Window reference для управления статус баром val window = remember { (view.context as? Activity)?.window } - // 🔥 Focus state for input - val inputFocusRequester = remember { FocusRequester() } - // 🔥 Emoji picker state var showEmojiPicker by remember { mutableStateOf(false) } @@ -539,7 +536,7 @@ fun ChatDetailScreen( } else { val isOverlayControllingSystemBars = showMediaPicker - if (!isOverlayControllingSystemBars && window != null && view != null) { + if (!isOverlayControllingSystemBars && window != null) { val ic = androidx.core.view.WindowCompat.getInsetsController(window, view) window.statusBarColor = android.graphics.Color.TRANSPARENT ic.isAppearanceLightStatusBars = false @@ -557,7 +554,7 @@ fun ChatDetailScreen( DisposableEffect(Unit) { onDispose { // Восстановить белые иконки статус-бара для chat list header - if (window != null && view != null) { + if (window != null) { val ic = androidx.core.view.WindowCompat.getInsetsController(window, view) window.statusBarColor = android.graphics.Color.TRANSPARENT ic.isAppearanceLightStatusBars = false @@ -571,9 +568,6 @@ fun ChatDetailScreen( } } - // 📷 Camera: URI для сохранения фото - var cameraImageUri by remember { mutableStateOf(null) } - // 📷 Состояние для flow камеры: фото → редактор с caption → отправка var pendingCameraPhotoUri by remember { mutableStateOf(null) @@ -648,47 +642,6 @@ fun ChatDetailScreen( onDispose { onImageViewerChanged(false) } } - // �📷 Camera launcher - val cameraLauncher = - rememberLauncherForActivityResult( - contract = ActivityResultContracts.TakePicture() - ) { success -> - if (success && cameraImageUri != null) { - // Очищаем фокус чтобы клавиатура не появилась - keyboardController?.hide() - focusManager.clearFocus() - // Открываем редактор вместо прямой отправки - pendingCameraPhotoUri = cameraImageUri - } - } - - // �️ Gallery-as-file launcher (sends images as compressed files, not as photos) - val galleryAsFileLauncher = - rememberLauncherForActivityResult( - contract = ActivityResultContracts.GetContent() - ) { uri -> - if (uri != null) { - scope.launch { - val fileName = MediaUtils.getFileName(context, uri) - val fileSize = MediaUtils.getFileSize(context, uri) - - if (fileSize > MediaUtils.MAX_FILE_SIZE_MB * 1024 * 1024) { - android.widget.Toast.makeText( - context, - "File too large (max ${MediaUtils.MAX_FILE_SIZE_MB} MB)", - android.widget.Toast.LENGTH_LONG - ).show() - return@launch - } - - val base64 = MediaUtils.uriToBase64File(context, uri) - if (base64 != null) { - viewModel.sendFileMessage(base64, fileName, fileSize) - } - } - } - } - // �📄 File picker launcher val filePickerLauncher = rememberLauncherForActivityResult( @@ -827,7 +780,6 @@ fun ChatDetailScreen( // Подключаем к ViewModel val messages by viewModel.messages.collectAsState() - val inputText by viewModel.inputText.collectAsState() val isTyping by viewModel.opponentTyping.collectAsState() val typingDisplayName by viewModel.typingDisplayName.collectAsState() val typingDisplayPublicKey by viewModel.typingDisplayPublicKey.collectAsState() @@ -2704,66 +2656,40 @@ fun ChatDetailScreen( } else if (!isSystemAccount) { // INPUT BAR Column { - MessageInputBar( - value = inputText, - onValueChange = { - viewModel - .updateInputText( - it - ) - if (it.isNotEmpty() && - !isSavedMessages - ) { - viewModel - .sendTypingIndicator() - } - }, + ChatInputBarSection( + viewModel = viewModel, + isSavedMessages = isSavedMessages, onSend = { - isSendingMessage = - true - viewModel - .sendMessage() + isSendingMessage = true + viewModel.sendMessage() scope.launch { delay(100) - listState - .animateScrollToItem( - 0 - ) + listState.animateScrollToItem(0) delay(300) - isSendingMessage = - false + isSendingMessage = false } }, isDarkTheme = isDarkTheme, - backgroundColor = - backgroundColor, + backgroundColor = backgroundColor, textColor = textColor, - placeholderColor = - secondaryTextColor, secondaryTextColor = secondaryTextColor, - replyMessages = - replyMessages, - isForwardMode = - isForwardMode, + replyMessages = replyMessages, + isForwardMode = isForwardMode, onCloseReply = { - viewModel - .clearReplyMessages() + viewModel.clearReplyMessages() }, onShowForwardOptions = { panelMessages -> if (panelMessages.isEmpty()) { - return@MessageInputBar + return@ChatInputBarSection } val forwardMessages = panelMessages.map { msg -> ForwardManager.ForwardMessage( - messageId = - msg.messageId, + messageId = msg.messageId, text = msg.text, - timestamp = - msg.timestamp, - isOutgoing = - msg.isOutgoing, + timestamp = msg.timestamp, + isOutgoing = msg.isOutgoing, senderPublicKey = msg.publicKey.ifEmpty { if (msg.isOutgoing) currentUserPublicKey @@ -2793,43 +2719,28 @@ fun ChatDetailScreen( }, chatTitle = chatTitle, isBlocked = isBlocked, - showEmojiPicker = - showEmojiPicker, + showEmojiPicker = showEmojiPicker, onToggleEmojiPicker = { showEmojiPicker = it }, - focusRequester = - inputFocusRequester, coordinator = coordinator, displayReplyMessages = displayReplyMessages, - onReplyClick = - scrollToMessage, + onReplyClick = scrollToMessage, onAttachClick = { // Telegram-style: - // галерея - // открывается - // ПОВЕРХ клавиатуры - // НЕ скрываем - // клавиатуру! - showMediaPicker = - true + // галерея открывается поверх клавиатуры. + showMediaPicker = true }, myPublicKey = - viewModel - .myPublicKey - ?: "", - opponentPublicKey = - user.publicKey, - myPrivateKey = - currentUserPrivateKey, + viewModel.myPublicKey ?: "", + opponentPublicKey = user.publicKey, + myPrivateKey = currentUserPrivateKey, isGroupChat = isGroupChat, mentionCandidates = mentionCandidates, avatarRepository = avatarRepository, - inputFocusTrigger = - inputFocusTrigger, - suppressKeyboard = - showInAppCamera, + inputFocusTrigger = inputFocusTrigger, + suppressKeyboard = showInAppCamera, hasNativeNavigationBar = hasNativeNavigationBar ) @@ -4311,6 +4222,76 @@ fun ChatDetailScreen( } // Закрытие outer Box } +@Composable +private fun ChatInputBarSection( + viewModel: ChatViewModel, + isSavedMessages: Boolean, + onSend: () -> Unit, + isDarkTheme: Boolean, + backgroundColor: Color, + textColor: Color, + secondaryTextColor: Color, + replyMessages: List, + isForwardMode: Boolean, + onCloseReply: () -> Unit, + onShowForwardOptions: (List) -> Unit, + chatTitle: String, + isBlocked: Boolean, + showEmojiPicker: Boolean, + onToggleEmojiPicker: (Boolean) -> Unit, + coordinator: KeyboardTransitionCoordinator, + displayReplyMessages: List, + onReplyClick: (String) -> Unit, + onAttachClick: () -> Unit, + myPublicKey: String, + opponentPublicKey: String, + myPrivateKey: String, + isGroupChat: Boolean, + mentionCandidates: List, + avatarRepository: AvatarRepository?, + inputFocusTrigger: Int, + suppressKeyboard: Boolean, + hasNativeNavigationBar: Boolean +) { + val inputText by viewModel.inputText.collectAsState() + + MessageInputBar( + value = inputText, + onValueChange = { + viewModel.updateInputText(it) + if (it.isNotEmpty() && !isSavedMessages) { + viewModel.sendTypingIndicator() + } + }, + onSend = onSend, + isDarkTheme = isDarkTheme, + backgroundColor = backgroundColor, + textColor = textColor, + secondaryTextColor = secondaryTextColor, + replyMessages = replyMessages, + isForwardMode = isForwardMode, + onCloseReply = onCloseReply, + onShowForwardOptions = onShowForwardOptions, + chatTitle = chatTitle, + isBlocked = isBlocked, + showEmojiPicker = showEmojiPicker, + onToggleEmojiPicker = onToggleEmojiPicker, + coordinator = coordinator, + displayReplyMessages = displayReplyMessages, + onReplyClick = onReplyClick, + onAttachClick = onAttachClick, + myPublicKey = myPublicKey, + opponentPublicKey = opponentPublicKey, + myPrivateKey = myPrivateKey, + isGroupChat = isGroupChat, + mentionCandidates = mentionCandidates, + avatarRepository = avatarRepository, + inputFocusTrigger = inputFocusTrigger, + suppressKeyboard = suppressKeyboard, + hasNativeNavigationBar = hasNativeNavigationBar + ) +} + @Composable private fun GroupMembersSubtitleSkeleton() { val transition = rememberInfiniteTransition(label = "groupMembersSkeleton") 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 99c707c..969050a 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 @@ -264,24 +264,19 @@ fun ChatsListScreen( accountName: String, accountUsername: String, accountVerified: Int = 0, - accountPhone: String, accountPublicKey: String, accountPrivateKey: String = "", - privateKeyHash: String = "", onToggleTheme: () -> Unit, onProfileClick: () -> Unit, onNewGroupClick: () -> Unit, - onContactsClick: () -> Unit, onCallsClick: () -> Unit, onSavedMessagesClick: () -> Unit, onSettingsClick: () -> Unit, - onInviteFriendsClick: () -> Unit, onSearchClick: () -> Unit, onRequestsClick: () -> Unit = {}, onNewChat: () -> Unit, onUserSelect: (com.rosetta.messenger.network.SearchUser) -> Unit = {}, onStartCall: (com.rosetta.messenger.network.SearchUser) -> Unit = {}, - backgroundBlurColorId: String = "avatar", pinnedChats: Set = emptySet(), onTogglePin: (String) -> Unit = {}, chatsViewModel: ChatsListViewModel = androidx.lifecycle.viewmodel.compose.viewModel(), @@ -462,7 +457,6 @@ fun ChatsListScreen( // 🔥 КРИТИЧНО: Инициализируем MessageRepository для обработки входящих // сообщений - val initStart = System.currentTimeMillis() ProtocolManager.initializeAccount(accountPublicKey, accountPrivateKey) android.util.Log.d( "ChatsListScreen", @@ -569,10 +563,6 @@ fun ChatsListScreen( allAccounts = accounts.sortedByDescending { it.publicKey == accountPublicKey } } - // 🔥 Используем rememberSaveable чтобы сохранить состояние при навигации - // Header сразу visible = true, без анимации при возврате из чата - var visible by rememberSaveable { mutableStateOf(true) } - // Confirmation dialogs state var dialogsToDelete by remember { mutableStateOf>(emptyList()) } var dialogToLeave by remember { mutableStateOf(null) } @@ -794,13 +784,6 @@ fun ChatsListScreen( // ═══════════════════════════════════════════════════════════ // 🎨 DRAWER HEADER // ═══════════════════════════════════════════════════════════ - val avatarColors = - getAvatarColor( - accountPublicKey, - isDarkTheme - ) - val headerColor = avatarColors.backgroundColor - // Header: цвет шапки сайдбара Box(modifier = Modifier.fillMaxWidth()) { Box( @@ -3462,9 +3445,6 @@ fun ChatItem( val secondaryTextColor = if (isDarkTheme) Color(0xFF8E8E93) else Color(0xFF666666) val dividerColor = if (isDarkTheme) Color(0xFF3A3A3A) else Color(0xFFE8E8E8) - val avatarColors = getAvatarColor(chat.publicKey, isDarkTheme) - val avatarText = getAvatarText(chat.publicKey) - Column { Row( modifier = @@ -3939,13 +3919,11 @@ fun SwipeableDialogItem( velocityTracker.resetTracking() var totalDragX = 0f var totalDragY = 0f - var passedSlop = false var claimed = false // Phase 1: Determine gesture type (tap / long-press / drag) // Wait up to longPressTimeout; if no up or slop → long press var gestureType = "unknown" - var fingerIsUp = false val result = withTimeoutOrNull(longPressTimeoutMs) { while (true) { @@ -3982,19 +3960,17 @@ fun SwipeableDialogItem( // Timeout → check if finger lifted during the race window if (result == null) { // Grace period: check if up event arrived just as timeout fired - val graceResult = withTimeoutOrNull(32L) { + withTimeoutOrNull(32L) { while (true) { val event = awaitPointerEvent() val change = event.changes.firstOrNull { it.id == down.id } if (change == null) { gestureType = "cancelled" - fingerIsUp = true return@withTimeoutOrNull Unit } if (change.changedToUpIgnoreConsumed()) { change.consume() gestureType = "tap" - fingerIsUp = true return@withTimeoutOrNull Unit } // Still moving/holding — it's a real long press @@ -4034,13 +4010,11 @@ fun SwipeableDialogItem( when { // Horizontal left swipe — reveal action buttons currentSwipeEnabled && dominated && totalDragX < 0 -> { - passedSlop = true claimed = true currentOnSwipeStarted() } // Horizontal right swipe with buttons open — close them dominated && totalDragX > 0 && offsetX != 0f -> { - passedSlop = true claimed = true } // Right swipe with buttons closed — let drawer handle @@ -4144,10 +4118,6 @@ fun DialogItemContent( val secondaryTextColor = remember(isDarkTheme) { if (isDarkTheme) Color(0xFF8E8E93) else Color(0xFF666666) } - val avatarColors = - remember(dialog.opponentKey, isDarkTheme) { - getAvatarColor(dialog.opponentKey, isDarkTheme) - } val isGroupDialog = remember(dialog.opponentKey) { isGroupDialogKey(dialog.opponentKey) } // 📁 Для Saved Messages показываем специальное имя @@ -4182,38 +4152,6 @@ fun DialogItemContent( } } - // 📁 Для Saved Messages показываем иконку закладки - // 🔥 Как в Архиве: инициалы из title или username или DELETED - val initials = - remember( - dialog.opponentTitle, - dialog.opponentUsername, - dialog.opponentKey, - dialog.isSavedMessages - ) { - if (dialog.isSavedMessages) { - "" // Для Saved Messages - пустая строка, будет использоваться - // иконка - } else if (dialog.opponentTitle.isNotEmpty() && - dialog.opponentTitle != dialog.opponentKey && - dialog.opponentTitle != dialog.opponentKey.take(7) && - dialog.opponentTitle != dialog.opponentKey.take(8) - ) { - // Используем title для инициалов - dialog.opponentTitle - .split(" ") - .take(2) - .mapNotNull { it.firstOrNull()?.uppercase() } - .joinToString("") - .ifEmpty { dialog.opponentTitle.take(2).uppercase() } - } else if (dialog.opponentUsername.isNotEmpty()) { - // Если только username - берем первые 2 символа - dialog.opponentUsername.take(2).uppercase() - } else { - dialog.opponentKey.take(2).uppercase() - } - } - Row( modifier = Modifier.fillMaxWidth() @@ -5003,7 +4941,6 @@ private fun RequestsRouteContent( RequestsScreen( requests = requests, isDarkTheme = isDarkTheme, - onBack = onBack, onRequestClick = onRequestClick, avatarRepository = avatarRepository, blockedUsers = blockedUsers, @@ -5138,7 +5075,6 @@ fun RequestsSection( fun RequestsScreen( requests: List, isDarkTheme: Boolean, - onBack: () -> Unit, onRequestClick: (DialogUiModel) -> Unit, avatarRepository: com.rosetta.messenger.repository.AvatarRepository? = null, blockedUsers: Set = emptySet(), diff --git a/app/src/main/java/com/rosetta/messenger/ui/chats/RequestsListScreen.kt b/app/src/main/java/com/rosetta/messenger/ui/chats/RequestsListScreen.kt index 3bdc6de..c103ec8 100644 --- a/app/src/main/java/com/rosetta/messenger/ui/chats/RequestsListScreen.kt +++ b/app/src/main/java/com/rosetta/messenger/ui/chats/RequestsListScreen.kt @@ -87,7 +87,6 @@ fun RequestsListScreen( RequestsScreen( requests = requests, isDarkTheme = isDarkTheme, - onBack = onBack, onRequestClick = { request -> onUserSelect(chatsViewModel.dialogToSearchUser(request)) }, diff --git a/app/src/main/java/com/rosetta/messenger/ui/chats/input/ChatDetailInput.kt b/app/src/main/java/com/rosetta/messenger/ui/chats/input/ChatDetailInput.kt index 11a9b22..5c7f431 100644 --- a/app/src/main/java/com/rosetta/messenger/ui/chats/input/ChatDetailInput.kt +++ b/app/src/main/java/com/rosetta/messenger/ui/chats/input/ChatDetailInput.kt @@ -23,7 +23,6 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.ExperimentalComposeUiApi import androidx.compose.ui.draw.clip -import androidx.compose.ui.focus.FocusRequester import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.graphicsLayer import androidx.compose.ui.layout.ContentScale @@ -56,6 +55,7 @@ import com.rosetta.messenger.ui.chats.components.* import com.rosetta.messenger.ui.chats.utils.* import com.rosetta.messenger.ui.chats.ChatViewModel import kotlinx.coroutines.delay +import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.launch import java.util.Locale @@ -89,7 +89,6 @@ fun MessageInputBar( isDarkTheme: Boolean, backgroundColor: Color, textColor: Color, - placeholderColor: Color, secondaryTextColor: Color, replyMessages: List = emptyList(), isForwardMode: Boolean = false, @@ -99,7 +98,6 @@ fun MessageInputBar( isBlocked: Boolean = false, showEmojiPicker: Boolean = false, onToggleEmojiPicker: (Boolean) -> Unit = {}, - focusRequester: FocusRequester? = null, coordinator: KeyboardTransitionCoordinator, displayReplyMessages: List = emptyList(), onReplyClick: (String) -> Unit = {}, @@ -189,7 +187,9 @@ fun MessageInputBar( // Update coordinator through snapshotFlow (no recomposition) LaunchedEffect(Unit) { - snapshotFlow { with(density) { imeInsets.getBottom(density).toDp() } }.collect { currentImeHeight -> + snapshotFlow { with(density) { imeInsets.getBottom(density).toDp() } } + .distinctUntilChanged() + .collect { currentImeHeight -> val now = System.currentTimeMillis() val heightChanged = kotlin.math.abs((currentImeHeight - lastStableKeyboardHeight).value) > 5f if (heightChanged && currentImeHeight.value > 0) { @@ -202,9 +202,13 @@ fun MessageInputBar( isKeyboardVisible = currentImeHeight > 50.dp coordinator.updateKeyboardHeight(currentImeHeight) - if (currentImeHeight > 100.dp) { + // Update "stable" height only after IME animation settles, + // otherwise low-end devices get many unnecessary recompositions. + if (!isKeyboardAnimating && currentImeHeight > 100.dp) { coordinator.syncHeights() - lastStableKeyboardHeight = currentImeHeight + if (kotlin.math.abs((currentImeHeight - lastStableKeyboardHeight).value) > 2f) { + lastStableKeyboardHeight = currentImeHeight + } } } } diff --git a/app/src/main/java/com/rosetta/messenger/ui/components/AppleEmojiEditText.kt b/app/src/main/java/com/rosetta/messenger/ui/components/AppleEmojiEditText.kt index a0cf4d3..988ee82 100644 --- a/app/src/main/java/com/rosetta/messenger/ui/components/AppleEmojiEditText.kt +++ b/app/src/main/java/com/rosetta/messenger/ui/components/AppleEmojiEditText.kt @@ -357,6 +357,15 @@ class AppleEmojiEditTextView @JvmOverloads constructor( try { val textStr = editable.toString() + val hasEmojiHints = containsEmojiHints(textStr) + if (!hasEmojiHints) { + // Fast path for plain text: skip heavy grapheme/asset pipeline. + // Also drop stale spans if user removed emoji content. + editable.getSpans(0, editable.length, ImageSpan::class.java).forEach { + editable.removeSpan(it) + } + return + } val cursorPosition = selectionStart // 🔥 Собираем все позиции эмодзи (и Unicode, и :emoji_code:) @@ -430,6 +439,15 @@ class AppleEmojiEditTextView @JvmOverloads constructor( } } + private fun containsEmojiHints(text: String): Boolean { + if (text.isEmpty()) return false + if (text.indexOf(":emoji_") >= 0) return true + for (ch in text) { + if (Character.isSurrogate(ch) || ch == '\u200D' || ch == '\uFE0F') return true + } + return false + } + private fun loadFromAssets(unified: String): Bitmap? { return try { val inputStream = getContext().assets.open("emoji/$unified.png")