Выпуск 1.1.7: слияние dev в master
All checks were successful
Android Kernel Build / build (push) Successful in 16h25m22s
All checks were successful
Android Kernel Build / build (push) Successful in 16h25m22s
This commit is contained in:
@@ -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"
|
||||
|
||||
@@ -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">
|
||||
<intent-filter>
|
||||
|
||||
@@ -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}")
|
||||
|
||||
@@ -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 =
|
||||
|
||||
@@ -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(
|
||||
"""
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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())
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user