diff --git a/app/build.gradle.kts b/app/build.gradle.kts index a38218b..994b4e0 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.1.6" -val rosettaVersionCode = 18 // Increment on each release +val rosettaVersionName = "1.1.7" +val rosettaVersionCode = 19 // Increment on each release android { namespace = "com.rosetta.messenger" diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index d67623a..5044024 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -32,6 +32,8 @@ android:exported="true" android:label="@string/app_name" android:theme="@style/Theme.RosettaAndroid" + android:launchMode="singleTask" + android:configChanges="keyboard|keyboardHidden|orientation|screenSize|uiMode|smallestScreenSize|screenLayout" android:windowSoftInputMode="adjustResize" android:screenOrientation="portrait"> diff --git a/app/src/main/java/com/rosetta/messenger/MainActivity.kt b/app/src/main/java/com/rosetta/messenger/MainActivity.kt index 1d23041..07ff4cd 100644 --- a/app/src/main/java/com/rosetta/messenger/MainActivity.kt +++ b/app/src/main/java/com/rosetta/messenger/MainActivity.kt @@ -458,15 +458,24 @@ class MainActivity : FragmentActivity() { // Сохраняем токен локально saveFcmToken(token) addFcmLog("💾 Токен сохранен локально") - - // Token will be sent by ProtocolManager.onAuthenticated() - // when protocol reaches AUTHENTICATED state + if (ProtocolManager.isAuthenticated()) { + runCatching { + ProtocolManager.subscribePushTokenIfAvailable( + forceToken = token + ) + } + .onSuccess { + addFcmLog("🔔 Push token отправлен на сервер сразу") + } + .onFailure { error -> + addFcmLog( + "❌ Ошибка отправки push token: ${error.message}" + ) + } + } } else { addFcmLog("⚠️ Токен пустой") } - - // Токен будет отправлен через ProtocolManager.subscribePushTokenIfAvailable() - // при достижении состояния AUTHENTICATED } } catch (e: Exception) { addFcmLog("❌ Ошибка Firebase: ${e.message}") 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 855a028..ef3623f 100644 --- a/app/src/main/java/com/rosetta/messenger/data/ReleaseNotes.kt +++ b/app/src/main/java/com/rosetta/messenger/data/ReleaseNotes.kt @@ -17,18 +17,14 @@ object ReleaseNotes { val RELEASE_NOTICE = """ Update v$VERSION_PLACEHOLDER - Профили и аватарки - - Добавлен полноэкранный просмотр аватарок в чужом профиле (включая системные аккаунты) - - Исправлено отображение даты аватарки: устранён некорректный год (например, 58154) - - Сессии и вход - - Добавлен кэш сессии в памяти процесса: повторный пароль не запрашивается, пока процесс жив - - Кэш сессии корректно очищается при выходе, переключении и удалении аккаунта + Уведомления + - Исправлена регистрация push-токена после переподключений + - Добавлен fallback для нестандартных payload, чтобы push-уведомления не терялись + - Улучшена отправка push-токена сразу после получения FCM токена Интерфейс - - Исправлено центрирование blur-фона у системных аватарок - - Унифицировано определение темы для verified-галочек - - В списке чатов verified-галочки сделаны синими в светлой теме (включая system light) + - Улучшено поведение сворачивания приложения в стиле Telegram + - Стабилизировано отображение нижней системной панели навигации """.trimIndent() fun getNotice(version: String): String = diff --git a/app/src/main/java/com/rosetta/messenger/database/MessageEntities.kt b/app/src/main/java/com/rosetta/messenger/database/MessageEntities.kt index 94e405d..e592a88 100644 --- a/app/src/main/java/com/rosetta/messenger/database/MessageEntities.kt +++ b/app/src/main/java/com/rosetta/messenger/database/MessageEntities.kt @@ -682,6 +682,10 @@ interface DialogDao { lastSeen: Long ) + /** Сбросить online-флаг у всех диалогов аккаунта (защита от устаревшего кэша при старте) */ + @Query("UPDATE dialogs SET is_online = 0 WHERE account = :account AND is_online != 0") + suspend fun clearOnlineStatuses(account: String) + /** Получить онлайн статус пользователя */ @Query( """ diff --git a/app/src/main/java/com/rosetta/messenger/network/ProtocolManager.kt b/app/src/main/java/com/rosetta/messenger/network/ProtocolManager.kt index 554469b..841e2b0 100644 --- a/app/src/main/java/com/rosetta/messenger/network/ProtocolManager.kt +++ b/app/src/main/java/com/rosetta/messenger/network/ProtocolManager.kt @@ -187,11 +187,15 @@ object ProtocolManager { getProtocol().state.collect { newState -> val previous = lastProtocolState if (newState == ProtocolState.AUTHENTICATED && previous != ProtocolState.AUTHENTICATED) { + // New authenticated websocket session: always allow fresh push subscribe. + lastSubscribedToken = null onAuthenticated() } if (newState != ProtocolState.AUTHENTICATED && newState != ProtocolState.HANDSHAKING) { syncRequestInFlight = false setSyncInProgress(false) + // Connection/session dropped: force re-subscribe on next AUTHENTICATED. + lastSubscribedToken = null } lastProtocolState = newState } @@ -616,10 +620,6 @@ object ProtocolManager { * [com.rosetta.messenger.push.RosettaFirebaseMessagingService.onNewToken] * when Firebase rotates the token mid-session. * - * On each connect we send UNSUBSCRIBE first to clear any duplicate - * registrations that may have accumulated on the server, then SUBSCRIBE - * once — guaranteeing exactly one active push binding per device. - * * @param forceToken if non-null, use this token instead of reading SharedPreferences * (used by onNewToken which already has the fresh token). */ @@ -638,23 +638,13 @@ object ProtocolManager { return } - // 1) UNSUBSCRIBE — clears ALL existing registrations for this token on the server. - // This removes duplicates that may have been created before the dedup fix. - val unsubPacket = PacketPushNotification().apply { - notificationsToken = token - action = PushNotificationAction.UNSUBSCRIBE - } - send(unsubPacket) - addLog("🔕 Push token UNSUBSCRIBE sent (clearing duplicates)") - - // 2) SUBSCRIBE — register exactly once. val subPacket = PacketPushNotification().apply { notificationsToken = token action = PushNotificationAction.SUBSCRIBE } send(subPacket) lastSubscribedToken = token - addLog("🔔 Push token SUBSCRIBE sent — single registration") + addLog("🔔 Push token SUBSCRIBE sent") } private fun requestSynchronize() { 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 3cd1c06..ae24d72 100644 --- a/app/src/main/java/com/rosetta/messenger/push/RosettaFirebaseMessagingService.kt +++ b/app/src/main/java/com/rosetta/messenger/push/RosettaFirebaseMessagingService.kt @@ -132,6 +132,19 @@ class RosettaFirebaseMessagingService : FirebaseMessagingService() { handledMessageData = true } } + + val looksLikeMessagePayload = + type.contains("message") || + data.keys.any { key -> + val lower = key.lowercase(Locale.ROOT) + lower.contains("message") || + lower.contains("text") || + lower.contains("body") + } + if (!handledMessageData && !isReadEvent && looksLikeMessagePayload) { + showSimpleNotification(senderName, messagePreview) + handledMessageData = true + } } // Обрабатываем notification payload (если есть). @@ -164,11 +177,6 @@ class RosettaFirebaseMessagingService : FirebaseMessagingService() { } lastNotifTimestamps[dedupKey] = now Log.d(TAG, "\u2705 Showing notification for key=$dedupKey") - // Desktop parity: suppress notifications during sync (useDialogFiber.ts checks - // protocolState != ProtocolState.SYNCHRONIZATION before calling notify()). - if (ProtocolManager.syncInProgress.value) { - return - } val senderKey = senderPublicKey?.trim().orEmpty() if (senderKey.isNotEmpty() && isDialogMuted(senderKey)) { return diff --git a/app/src/main/java/com/rosetta/messenger/ui/chats/ChatsListViewModel.kt b/app/src/main/java/com/rosetta/messenger/ui/chats/ChatsListViewModel.kt index 8126bbc..e539ed1 100644 --- a/app/src/main/java/com/rosetta/messenger/ui/chats/ChatsListViewModel.kt +++ b/app/src/main/java/com/rosetta/messenger/ui/chats/ChatsListViewModel.kt @@ -266,6 +266,12 @@ class ChatsListViewModel(application: Application) : AndroidViewModel(applicatio // Запускаем все подписки в одном родительском Job для отмены при смене аккаунта accountSubscriptionsJob = viewModelScope.launch { + // 🟢 Сбрасываем устаревшие online-флаги из прошлого сеанса. + // Актуальные статусы придут сразу после PacketOnlineSubscribe. + withContext(Dispatchers.IO) { + runCatching { dialogDao.clearOnlineStatuses(publicKey) } + } + // Подписываемся на обычные диалоги @OptIn(FlowPreview::class) launch { diff --git a/app/src/main/java/com/rosetta/messenger/ui/chats/components/InAppCameraScreen.kt b/app/src/main/java/com/rosetta/messenger/ui/chats/components/InAppCameraScreen.kt index dfe93b6..15ace5b 100644 --- a/app/src/main/java/com/rosetta/messenger/ui/chats/components/InAppCameraScreen.kt +++ b/app/src/main/java/com/rosetta/messenger/ui/chats/components/InAppCameraScreen.kt @@ -187,15 +187,9 @@ fun InAppCameraScreen( window.statusBarColor = originalStatusBarColor insetsController.isAppearanceLightStatusBars = originalLightStatusBars - // Navigation bar: восстанавливаем только если есть нативные кнопки - if (com.rosetta.messenger.ui.utils.NavigationModeUtils.hasNativeNavigationBar(context)) { - window.navigationBarColor = originalNavigationBarColor - insetsController.isAppearanceLightNavigationBars = originalLightNavigationBars - } else { - insetsController.hide(androidx.core.view.WindowInsetsCompat.Type.navigationBars()) - insetsController.systemBarsBehavior = - androidx.core.view.WindowInsetsControllerCompat.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE - } + window.navigationBarColor = originalNavigationBarColor + insetsController.isAppearanceLightNavigationBars = originalLightNavigationBars + insetsController.show(androidx.core.view.WindowInsetsCompat.Type.navigationBars()) } } diff --git a/app/src/main/java/com/rosetta/messenger/ui/onboarding/OnboardingScreen.kt b/app/src/main/java/com/rosetta/messenger/ui/onboarding/OnboardingScreen.kt index 9b214d4..b438d5c 100644 --- a/app/src/main/java/com/rosetta/messenger/ui/onboarding/OnboardingScreen.kt +++ b/app/src/main/java/com/rosetta/messenger/ui/onboarding/OnboardingScreen.kt @@ -173,15 +173,10 @@ fun OnboardingScreen( if (!view.isInEditMode) { val window = (view.context as android.app.Activity).window val insetsController = WindowCompat.getInsetsController(window, view) - if (NavigationModeUtils.hasNativeNavigationBar(view.context)) { - window.navigationBarColor = - if (isDarkTheme) 0xFF1E1E1E.toInt() else 0xFFFFFFFF.toInt() - } else { - // Жестовая навигация — прячем бар - insetsController.hide(WindowInsetsCompat.Type.navigationBars()) - insetsController.systemBarsBehavior = - WindowInsetsControllerCompat.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE - } + window.navigationBarColor = + if (isDarkTheme) 0xFF1E1E1E.toInt() else 0xFFFFFFFF.toInt() + insetsController.show(WindowInsetsCompat.Type.navigationBars()) + insetsController.isAppearanceLightNavigationBars = !isDarkTheme } } diff --git a/app/src/main/java/com/rosetta/messenger/ui/utils/NavigationModeUtils.kt b/app/src/main/java/com/rosetta/messenger/ui/utils/NavigationModeUtils.kt index 128b742..d4f12b7 100644 --- a/app/src/main/java/com/rosetta/messenger/ui/utils/NavigationModeUtils.kt +++ b/app/src/main/java/com/rosetta/messenger/ui/utils/NavigationModeUtils.kt @@ -55,25 +55,15 @@ object NavigationModeUtils { } /** - * Показывает или прячет navigation bar в зависимости от типа навигации. - * - Кнопочная навигация → показываем бар - * - Жестовая навигация → прячем бар, свайп снизу временно покажет + * Показывает navigation bar на всех устройствах. */ fun applyNavigationBarVisibility( insetsController: WindowInsetsControllerCompat, context: Context, isDarkTheme: Boolean ) { - if (hasNativeNavigationBar(context)) { - // Есть нативные кнопки — показываем навигационный бар - insetsController.show(WindowInsetsCompat.Type.navigationBars()) - insetsController.isAppearanceLightNavigationBars = !isDarkTheme - } else { - // Жестовая навигация — прячем навигационный бар - insetsController.hide(WindowInsetsCompat.Type.navigationBars()) - insetsController.systemBarsBehavior = - WindowInsetsControllerCompat.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE - } + insetsController.show(WindowInsetsCompat.Type.navigationBars()) + insetsController.isAppearanceLightNavigationBars = !isDarkTheme } } diff --git a/app/src/main/java/com/rosetta/messenger/ui/utils/SystemBarsStyleUtils.kt b/app/src/main/java/com/rosetta/messenger/ui/utils/SystemBarsStyleUtils.kt index 5014cef..1b05fe4 100644 --- a/app/src/main/java/com/rosetta/messenger/ui/utils/SystemBarsStyleUtils.kt +++ b/app/src/main/java/com/rosetta/messenger/ui/utils/SystemBarsStyleUtils.kt @@ -6,7 +6,6 @@ import android.view.View import android.view.Window import androidx.core.view.WindowCompat import androidx.core.view.WindowInsetsCompat -import androidx.core.view.WindowInsetsControllerCompat data class SystemBarsState( val statusBarColor: Int, @@ -48,16 +47,10 @@ object SystemBarsStyleUtils { if (window == null || view == null) return val insetsController = WindowCompat.getInsetsController(window, view) - if (NavigationModeUtils.hasNativeNavigationBar(context)) { - insetsController.show(WindowInsetsCompat.Type.navigationBars()) - if (state != null) { - window.navigationBarColor = state.navigationBarColor - insetsController.isAppearanceLightNavigationBars = state.isLightNavigationBars - } - } else { - insetsController.hide(WindowInsetsCompat.Type.navigationBars()) - insetsController.systemBarsBehavior = - WindowInsetsControllerCompat.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE + insetsController.show(WindowInsetsCompat.Type.navigationBars()) + if (state != null) { + window.navigationBarColor = state.navigationBarColor + insetsController.isAppearanceLightNavigationBars = state.isLightNavigationBars } }