From c92cb0779ae2a690f716d67d10a29cb2af9d0dbf Mon Sep 17 00:00:00 2001 From: k1ngsterr1 Date: Mon, 16 Feb 2026 03:42:43 +0500 Subject: [PATCH] feat: update notification handling and improve media selection with captions --- app/build.gradle.kts | 4 +- app/proguard-rules.pro | 14 ++- .../push/RosettaFirebaseMessagingService.kt | 21 +++- .../messenger/ui/chats/ChatDetailScreen.kt | 14 ++- .../messenger/ui/chats/ChatViewModel.kt | 12 +-- .../chats/components/AttachmentComponents.kt | 4 +- .../ui/chats/components/ImageEditorScreen.kt | 50 ++++----- .../components/MediaPickerBottomSheet.kt | 100 ++++++++++++++++-- 8 files changed, 163 insertions(+), 56 deletions(-) diff --git a/app/build.gradle.kts b/app/build.gradle.kts index f1416f5..f03b4d5 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -57,8 +57,8 @@ android { buildTypes { release { - isMinifyEnabled = true - isShrinkResources = true + isMinifyEnabled = false + isShrinkResources = false proguardFiles( getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro" diff --git a/app/proguard-rules.pro b/app/proguard-rules.pro index ec2738b..8c5458d 100644 --- a/app/proguard-rules.pro +++ b/app/proguard-rules.pro @@ -95,10 +95,10 @@ # ============================================================ # R8 VerifyError fix: prevent R8 from generating invalid -# bytecode (instance-of on unexpected class Integer) in -# app UI lambdas with primitive boxing/unboxing +# bytecode in app UI lambdas (Compose/coroutine) that interact +# with third-party libraries involving primitive boxing/unboxing # ============================================================ --keep,allowobfuscation class com.rosetta.messenger.ui.** { *; } +-keep class com.rosetta.messenger.ui.** { *; } # ============================================================ # Data Models @@ -174,3 +174,11 @@ # ============================================================ -keep class coil.** { *; } -dontwarn coil.** + +# ============================================================ +# PhotoEditor (com.burhanrashid52:photoeditor) +# Prevents R8 from generating invalid bytecode (VerifyError: +# instance-of on unexpected class Integer) in PhotoEditorImpl +# ============================================================ +-keep class ja.burhanrashid52.photoeditor.** { *; } +-dontwarn ja.burhanrashid52.photoeditor.** diff --git a/app/src/main/java/com/rosetta/messenger/push/RosettaFirebaseMessagingService.kt b/app/src/main/java/com/rosetta/messenger/push/RosettaFirebaseMessagingService.kt index 94a9652..35c534a 100644 --- a/app/src/main/java/com/rosetta/messenger/push/RosettaFirebaseMessagingService.kt +++ b/app/src/main/java/com/rosetta/messenger/push/RosettaFirebaseMessagingService.kt @@ -34,10 +34,21 @@ class RosettaFirebaseMessagingService : FirebaseMessagingService() { private const val TAG = "RosettaFCM" private const val CHANNEL_ID = "rosetta_messages" private const val CHANNEL_NAME = "Messages" - private const val NOTIFICATION_ID = 1 // 🔥 Флаг - приложение в foreground (видимо пользователю) @Volatile var isAppInForeground = false + + /** Уникальный notification ID для каждого чата (по publicKey) */ + fun getNotificationIdForChat(senderPublicKey: String): Int { + return senderPublicKey.hashCode() and 0x7FFFFFFF // positive int + } + + /** Убрать уведомление конкретного чата из шторки */ + fun cancelNotificationForChat(context: Context, senderPublicKey: String) { + val notificationManager = + context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager + notificationManager.cancel(getNotificationIdForChat(senderPublicKey)) + } } /** Вызывается когда получен новый FCM токен Отправляем его на сервер через протокол */ @@ -97,6 +108,8 @@ class RosettaFirebaseMessagingService : FirebaseMessagingService() { createNotificationChannel() + val notifId = getNotificationIdForChat(senderPublicKey ?: "") + // Intent для открытия чата val intent = Intent(this, MainActivity::class.java).apply { @@ -107,7 +120,7 @@ class RosettaFirebaseMessagingService : FirebaseMessagingService() { val pendingIntent = PendingIntent.getActivity( this, - 0, + notifId, intent, PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE ) @@ -125,7 +138,7 @@ class RosettaFirebaseMessagingService : FirebaseMessagingService() { val notificationManager = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager - notificationManager.notify(NOTIFICATION_ID, notification) + notificationManager.notify(notifId, notification) } /** Показать простое уведомление */ @@ -162,7 +175,7 @@ class RosettaFirebaseMessagingService : FirebaseMessagingService() { val notificationManager = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager - notificationManager.notify(NOTIFICATION_ID, notification) + notificationManager.notify(System.currentTimeMillis().toInt(), notification) } /** Создать notification channel для Android 8+ */ 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 7a1ee8b..0d07e53 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 @@ -459,6 +459,9 @@ fun ChatDetailScreen( Lifecycle.Event.ON_RESUME -> { isScreenActive = true viewModel.setDialogActive(true) + // 🔥 Убираем уведомление этого чата из шторки + com.rosetta.messenger.push.RosettaFirebaseMessagingService + .cancelNotificationForChat(context, user.publicKey) } Lifecycle.Event.ON_PAUSE -> { isScreenActive = false @@ -482,6 +485,9 @@ fun ChatDetailScreen( LaunchedEffect(user.publicKey, forwardTrigger) { viewModel.setUserKeys(currentUserPublicKey, currentUserPrivateKey) viewModel.openDialog(user.publicKey, user.title, user.username) + // 🔥 Убираем уведомление этого чата из шторки при заходе + com.rosetta.messenger.push.RosettaFirebaseMessagingService + .cancelNotificationForChat(context, user.publicKey) // Подписываемся на онлайн статус собеседника if (!isSavedMessages) { viewModel.subscribeToOnlineStatus() @@ -2065,13 +2071,15 @@ fun ChatDetailScreen( onDismiss = { showMediaPicker = false }, isDarkTheme = isDarkTheme, currentUserPublicKey = currentUserPublicKey, - onMediaSelected = { selectedMedia -> - // 📸 Открываем edit screen для выбранных изображений + onMediaSelected = { selectedMedia, caption -> + // 📸 Отправляем фото напрямую с caption val imageUris = selectedMedia.filter { !it.isVideo }.map { it.uri } if (imageUris.isNotEmpty()) { - pendingGalleryImages = imageUris + showMediaPicker = false + inputFocusTrigger++ + viewModel.sendImageGroupFromUris(imageUris, caption) } }, onMediaSelectedWithCaption = { mediaItem, caption -> diff --git a/app/src/main/java/com/rosetta/messenger/ui/chats/ChatViewModel.kt b/app/src/main/java/com/rosetta/messenger/ui/chats/ChatViewModel.kt index d9dabfa..fa61ccd 100644 --- a/app/src/main/java/com/rosetta/messenger/ui/chats/ChatViewModel.kt +++ b/app/src/main/java/com/rosetta/messenger/ui/chats/ChatViewModel.kt @@ -321,13 +321,7 @@ class ChatViewModel(application: Application) : AndroidViewModel(application) { // Добавляем все сразу kotlinx.coroutines.withContext(Dispatchers.Main.immediate) { val currentList = _messages.value - val newList = (currentList + newMessages).sortedBy { it.timestamp } - - // 🔍 DEBUG: Проверка на дублирующиеся ID - val allIds = newList.map { it.id } - val duplicates = allIds.groupBy { it }.filter { it.value.size > 1 }.keys - if (duplicates.isNotEmpty()) {} - + val newList = (currentList + newMessages).distinctBy { it.id }.sortedBy { it.timestamp } _messages.value = newList } @@ -800,7 +794,7 @@ class ChatViewModel(application: Application) : AndroidViewModel(application) { // 🔥 ДОБАВЛЯЕМ новые к текущим, а не заменяем! // Сортируем по timestamp чтобы новые были в конце - val updatedMessages = (currentMessages + newMessages).sortedBy { it.timestamp } + val updatedMessages = (currentMessages + newMessages).distinctBy { it.id }.sortedBy { it.timestamp } // 🔥 ИСПРАВЛЕНИЕ: Обновляем кэш сохраняя ВСЕ сообщения, не только отображаемые! // Объединяем существующий кэш с новыми сообщениями @@ -912,7 +906,7 @@ class ChatViewModel(application: Application) : AndroidViewModel(application) { // Добавляем в начало списка (старые сообщения) withContext(Dispatchers.Main) { - _messages.value = newMessages + _messages.value + _messages.value = (newMessages + _messages.value).distinctBy { it.id } } } diff --git a/app/src/main/java/com/rosetta/messenger/ui/chats/components/AttachmentComponents.kt b/app/src/main/java/com/rosetta/messenger/ui/chats/components/AttachmentComponents.kt index beeab53..75907c8 100644 --- a/app/src/main/java/com/rosetta/messenger/ui/chats/components/AttachmentComponents.kt +++ b/app/src/main/java/com/rosetta/messenger/ui/chats/components/AttachmentComponents.kt @@ -430,7 +430,7 @@ fun ImageCollage( horizontalArrangement = Arrangement.spacedBy(spacing) ) { attachments.forEachIndexed { index, attachment -> - Box(modifier = Modifier.weight(1f)) { + Box(modifier = Modifier.weight(1f).aspectRatio(1f)) { ImageAttachment( attachment = attachment, chachaKey = chachaKey, @@ -441,7 +441,7 @@ fun ImageCollage( timestamp = timestamp, messageStatus = messageStatus, showTimeOverlay = showOverlayOnLast && index == count - 1, - aspectRatio = 1f, + fillMaxSize = true, isSelectionMode = isSelectionMode, onLongClick = onLongClick, onImageClick = onImageClick 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 53558d4..3cdb4b3 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 @@ -1634,7 +1634,14 @@ fun MultiImageEditorScreen( setPadding(0, 0, 0, 0) setBackgroundColor(android.graphics.Color.BLACK) - // Загружаем изображение + // Инициализация PhotoEditor синхронно в factory (уже на main thread) + val editor = PhotoEditor.Builder(ctx, this) + .setPinchTextScalable(true) + .setClipSourceImage(true) + .build() + photoEditors[page] = editor + + // Загружаем изображение асинхронно scope.launch(Dispatchers.IO) { try { val bitmap = loadBitmapRespectExif(ctx, imagesWithCaptions[page].uri) @@ -1648,12 +1655,6 @@ fun MultiImageEditorScreen( adjustViewBounds = true setPadding(0, 0, 0, 0) } - - val editor = PhotoEditor.Builder(ctx, this@apply) - .setPinchTextScalable(true) - .setClipSourceImage(true) - .build() - photoEditors[page] = editor } } catch (e: Exception) { // Handle error @@ -1737,20 +1738,20 @@ fun MultiImageEditorScreen( } } - // Bottom section - без imePadding, фото не сжимается - Column( - modifier = Modifier - .fillMaxWidth() - .align(Alignment.BottomCenter) - .background( - Brush.verticalGradient( - colors = listOf( - Color.Transparent, - Color.Black.copy(alpha = 0.7f) - ) + // Bottom section + Column( + modifier = Modifier + .fillMaxWidth() + .align(Alignment.BottomCenter) + .background( + Brush.verticalGradient( + colors = listOf( + Color.Transparent, + Color.Black.copy(alpha = 0.7f) ) ) - ) { + ) + ) { // Color picker AnimatedVisibility( visible = showColorPicker && currentTool == EditorTool.DRAW, @@ -1862,7 +1863,6 @@ fun MultiImageEditorScreen( .clip(CircleShape) .background(PrimaryBlue) .clickable(enabled = !isSaving && !isClosing) { - // 🚀 Сохраняем копию данных перед анимацией val imagesToSend = imagesWithCaptions.toList() scope.launch { @@ -1886,11 +1886,8 @@ fun MultiImageEditorScreen( } } - // Вызываем callback (он запустит sendImageGroup с optimistic UI) onSendAll(savedImages) - isSaving = false - // Закрываем после завершения сохранения/отправки animatedDismiss() } }, @@ -1900,14 +1897,13 @@ fun MultiImageEditorScreen( TablerIcons.ArrowUp, contentDescription = "Send", tint = Color.White, - modifier = Modifier - .size(22.dp) + modifier = Modifier.size(22.dp) ) } } - Spacer(modifier = Modifier.navigationBarsPadding()) - } + Spacer(modifier = Modifier.navigationBarsPadding()) + } } } } diff --git a/app/src/main/java/com/rosetta/messenger/ui/chats/components/MediaPickerBottomSheet.kt b/app/src/main/java/com/rosetta/messenger/ui/chats/components/MediaPickerBottomSheet.kt index ac22315..3f55105 100644 --- a/app/src/main/java/com/rosetta/messenger/ui/chats/components/MediaPickerBottomSheet.kt +++ b/app/src/main/java/com/rosetta/messenger/ui/chats/components/MediaPickerBottomSheet.kt @@ -106,7 +106,7 @@ fun MediaPickerBottomSheet( isVisible: Boolean, onDismiss: () -> Unit, isDarkTheme: Boolean, - onMediaSelected: (List) -> Unit, + onMediaSelected: (List, String) -> Unit, onMediaSelectedWithCaption: ((MediaItem, String) -> Unit)? = null, onOpenCamera: () -> Unit = {}, onOpenFilePicker: () -> Unit = {}, @@ -150,6 +150,10 @@ fun MediaPickerBottomSheet( // Caption для фото var photoCaption by remember { mutableStateOf("") } + + // Caption для группы фото (внизу picker'а) + var pickerCaption by remember { mutableStateOf("") } + var captionEditTextView by remember { mutableStateOf(null) } // Permission launcher val permissionLauncher = rememberLauncherForActivityResult( @@ -426,7 +430,7 @@ fun MediaPickerBottomSheet( alignment = Alignment.TopStart, // Начинаем с верха чтобы покрыть весь экран onDismissRequest = animatedClose, properties = PopupProperties( - focusable = false, + focusable = true, dismissOnBackPress = true, dismissOnClickOutside = false ) @@ -444,6 +448,7 @@ fun MediaPickerBottomSheet( Box( modifier = Modifier .fillMaxSize() + .imePadding() .background(Color.Black.copy(alpha = scrimAlpha)) .clickable( interactionSource = remember { MutableInteractionSource() }, @@ -523,7 +528,7 @@ fun MediaPickerBottomSheet( onDismiss = animatedClose, onSend = { val selected = mediaItems.filter { it.id in selectedItems } - onMediaSelected(selected) + onMediaSelected(selected, pickerCaption.trim()) animatedClose() }, isDarkTheme = isDarkTheme, @@ -648,6 +653,89 @@ fun MediaPickerBottomSheet( modifier = Modifier.weight(1f) ) } + + // Caption bar (видна когда есть выбранные фото) + AnimatedVisibility( + visible = selectedItems.isNotEmpty(), + enter = fadeIn() + expandVertically(expandFrom = Alignment.Bottom), + exit = fadeOut() + shrinkVertically(shrinkTowards = Alignment.Bottom) + ) { + Row( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 8.dp, vertical = 6.dp), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(8.dp) + ) { + // Caption input (pill shape) + Row( + modifier = Modifier + .weight(1f) + .clip(RoundedCornerShape(20.dp)) + .background(if (isDarkTheme) Color(0xFF2C2C2E) else Color(0xFFEFEFF0)) + .clickable { + // Программно открываем клавиатуру при клике на pill + captionEditTextView?.let { view -> + view.requestFocus() + val imm = context.getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager + imm.showSoftInput(view, InputMethodManager.SHOW_IMPLICIT) + } + } + .padding(horizontal = 12.dp, vertical = 8.dp), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(8.dp) + ) { + // Emoji button + Icon( + painter = TelegramIcons.Smile, + contentDescription = "Emoji", + tint = if (isDarkTheme) Color.White.copy(alpha = 0.5f) else Color.Gray, + modifier = Modifier.size(22.dp) + ) + + // Caption text field + Box( + modifier = Modifier + .weight(1f) + .heightIn(min = 22.dp, max = 100.dp) + ) { + AppleEmojiTextField( + value = pickerCaption, + onValueChange = { pickerCaption = it }, + textColor = if (isDarkTheme) Color.White else Color.Black, + textSize = 16f, + hint = "Add a caption...", + hintColor = if (isDarkTheme) Color.White.copy(alpha = 0.35f) else Color.Gray.copy(alpha = 0.5f), + modifier = Modifier.fillMaxWidth(), + requestFocus = false, + onViewCreated = { captionEditTextView = it }, + onFocusChanged = { } + ) + } + } + + // Send button + Box( + modifier = Modifier + .size(42.dp) + .clip(CircleShape) + .background(PrimaryBlue) + .clickable { + val selected = mediaItems.filter { it.id in selectedItems } + onMediaSelected(selected, pickerCaption.trim()) + animatedClose() + }, + contentAlignment = Alignment.Center + ) { + Icon( + painter = TelegramIcons.Send, + contentDescription = "Send", + tint = Color.White, + modifier = Modifier.size(20.dp) + ) + } + } + } } } } @@ -730,7 +818,7 @@ fun MediaPickerBottomSheet( mimeType = "image/png", dateModified = System.currentTimeMillis() ) - onMediaSelected(listOf(mediaItem)) + onMediaSelected(listOf(mediaItem), "") onDismiss() } }, @@ -776,7 +864,7 @@ fun MediaPickerBottomSheet( mimeType = "image/png", dateModified = System.currentTimeMillis() ) - onMediaSelected(listOf(mediaItem)) + onMediaSelected(listOf(mediaItem), "") onDismiss() } }, @@ -811,7 +899,7 @@ fun MediaPickerBottomSheet( dateModified = System.currentTimeMillis() ) // Отправляем фото (caption можно передать отдельно если нужно) - onMediaSelected(listOf(item)) + onMediaSelected(listOf(item), "") previewPhotoUri = null photoCaption = "" onDismiss()