Релиз 1.1.6: сессии, аватарки и интерфейсные исправления

This commit is contained in:
2026-03-10 23:25:03 +05:00
parent 9d0cab3f53
commit 810913b28e
9 changed files with 202 additions and 32 deletions

View File

@@ -23,8 +23,8 @@ val gitShortSha = safeGitOutput("rev-parse", "--short", "HEAD") ?: "unknown"
// ═══════════════════════════════════════════════════════════
// Rosetta versioning — bump here on each release
// ═══════════════════════════════════════════════════════════
val rosettaVersionName = "1.1.5"
val rosettaVersionCode = 17 // Increment on each release
val rosettaVersionName = "1.1.6"
val rosettaVersionCode = 18 // Increment on each release
android {
namespace = "com.rosetta.messenger"

View File

@@ -72,6 +72,8 @@ class MainActivity : FragmentActivity() {
companion object {
private const val TAG = "MainActivity"
// Process-memory session cache: lets app return without password while process is alive.
private var cachedDecryptedAccount: DecryptedAccount? = null
// 🔔 FCM Логи для отображения в UI
private val _fcmLogs = mutableStateListOf<String>()
@@ -87,8 +89,18 @@ class MainActivity : FragmentActivity() {
}
}
fun clearFcmLogs() {
_fcmLogs.clear()
fun clearFcmLogs() {
_fcmLogs.clear()
}
private fun cacheSessionAccount(account: DecryptedAccount?) {
cachedDecryptedAccount = account
}
private fun getCachedSessionAccount(): DecryptedAccount? = cachedDecryptedAccount
private fun clearCachedSessionAccount() {
cachedDecryptedAccount = null
}
}
@@ -152,14 +164,12 @@ class MainActivity : FragmentActivity() {
var showSplash by remember { mutableStateOf(true) }
var showOnboarding by remember { mutableStateOf(true) }
var hasExistingAccount by remember { mutableStateOf<Boolean?>(null) }
var currentAccount by remember { mutableStateOf<DecryptedAccount?>(null) }
var currentAccount by remember { mutableStateOf(getCachedSessionAccount()) }
var accountInfoList by remember { mutableStateOf<List<AccountInfo>>(emptyList()) }
var startCreateAccountFlow by remember { mutableStateOf(false) }
// Check for existing accounts and build AccountInfo list
// Also force logout so user always sees unlock screen on app restart
LaunchedEffect(Unit) {
accountManager.logout() // Always start logged out
val accounts = accountManager.getAllAccounts()
hasExistingAccount = accounts.isNotEmpty()
accountInfoList = accounts.map { it.toAccountInfo() }
@@ -239,6 +249,7 @@ class MainActivity : FragmentActivity() {
onAuthComplete = { account ->
startCreateAccountFlow = false
currentAccount = account
cacheSessionAccount(account)
hasExistingAccount = true
// Save as last logged account
account?.let {
@@ -256,6 +267,7 @@ class MainActivity : FragmentActivity() {
// Set currentAccount to null immediately to prevent UI
// lag
currentAccount = null
clearCachedSessionAccount()
scope.launch {
com.rosetta.messenger.network.ProtocolManager
.disconnect()
@@ -283,6 +295,7 @@ class MainActivity : FragmentActivity() {
// Set currentAccount to null immediately to prevent UI
// lag
currentAccount = null
clearCachedSessionAccount()
scope.launch {
com.rosetta.messenger.network.ProtocolManager
.disconnect()
@@ -315,6 +328,7 @@ class MainActivity : FragmentActivity() {
hasExistingAccount = accounts.isNotEmpty()
// 8. Navigate away last
currentAccount = null
clearCachedSessionAccount()
} catch (e: Exception) {
android.util.Log.e("DeleteAccount", "Failed to delete account", e)
}
@@ -353,6 +367,7 @@ class MainActivity : FragmentActivity() {
// 9. If current account is deleted, return to main login screen
if (currentAccount?.publicKey == targetPublicKey) {
currentAccount = null
clearCachedSessionAccount()
}
} catch (e: Exception) {
android.util.Log.e("DeleteAccount", "Failed to delete account from sidebar", e)
@@ -367,6 +382,7 @@ class MainActivity : FragmentActivity() {
// Switch to another account: logout current, then show unlock.
currentAccount = null
clearCachedSessionAccount()
scope.launch {
com.rosetta.messenger.network.ProtocolManager.disconnect()
accountManager.logout()
@@ -375,6 +391,7 @@ class MainActivity : FragmentActivity() {
onAddAccount = {
startCreateAccountFlow = true
currentAccount = null
clearCachedSessionAccount()
scope.launch {
com.rosetta.messenger.network.ProtocolManager.disconnect()
accountManager.logout()
@@ -387,6 +404,7 @@ class MainActivity : FragmentActivity() {
isDarkTheme = isDarkTheme,
onExit = {
currentAccount = null
clearCachedSessionAccount()
scope.launch {
ProtocolManager.disconnect()
accountManager.logout()

View File

@@ -17,17 +17,18 @@ object ReleaseNotes {
val RELEASE_NOTICE = """
Update v$VERSION_PLACEHOLDER
Подключение
- Ускорен старт соединения и handshake при входе в аккаунт
- Логика reconnect синхронизирована с desktop-поведением
- Обновлён серверный endpoint на основной production (wss)
Профили и аватарки
- Добавлен полноэкранный просмотр аватарок в чужом профиле (включая системные аккаунты)
- Исправлено отображение даты аватарки: устранён некорректный год (например, 58154)
Группы
- Добавлено предзагруженное кэширование участников группы
- Убран скачок "0 members" при повторном открытии группы
Сессии и вход
- Добавлен кэш сессии в памяти процесса: повторный пароль не запрашивается, пока процесс жив
- Кэш сессии корректно очищается при выходе, переключении и удалении аккаунта
Интерфейс
- Исправлено вертикальное выравнивание verified-галочки в списке чатов
- Исправлено центрирование blur-фона у системных аватарок
- Унифицировано определение темы для verified-галочек
- В списке чатов verified-галочки сделаны синими в светлой теме (включая system light)
""".trimIndent()
fun getNotice(version: String): String =

View File

@@ -935,7 +935,7 @@ fun ChatsListScreen(
VerifiedBadge(
verified = if (accountVerified > 0) accountVerified else 1,
size = 15,
badgeTint = Color(0xFFACD2F9)
badgeTint = PrimaryBlue
)
}
}
@@ -3891,7 +3891,9 @@ fun DialogItemContent(
VerifiedBadge(
verified = if (dialog.verified > 0) dialog.verified else 1,
size = 16,
modifier = Modifier.offset(y = (-2).dp)
modifier = Modifier.offset(y = (-2).dp),
isDarkTheme = isDarkTheme,
badgeTint = PrimaryBlue
)
}
// 🔒 Красная иконка замочка для заблокированных пользователей

View File

@@ -2,6 +2,11 @@ package com.rosetta.messenger.ui.components
import android.content.Context
import android.graphics.Bitmap
import android.graphics.BitmapShader
import android.graphics.Canvas
import android.graphics.Matrix
import android.graphics.Paint
import android.graphics.Shader
import android.os.Build
import android.renderscript.Allocation
import android.renderscript.Element
@@ -32,6 +37,7 @@ import com.rosetta.messenger.repository.AvatarRepository
import com.rosetta.messenger.utils.AvatarFileManager
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import kotlin.math.abs
/**
* Компонент для отображения размытого фона аватарки
@@ -63,7 +69,11 @@ fun BoxScope.BlurredAvatarBackground(
LaunchedEffect(avatarKey, publicKey) {
val currentAvatars = avatars
val newOriginal = withContext(Dispatchers.IO) {
if (currentAvatars.isNotEmpty()) {
// Keep system account blur source identical to the visible avatar drawable.
// This prevents offset/parallax mismatches when server avatar payload differs.
if (MessageRepository.isSystemAccount(publicKey)) {
loadSystemAvatarBitmap(context, publicKey)
} else if (currentAvatars.isNotEmpty()) {
AvatarFileManager.base64ToBitmap(currentAvatars.first().base64Data)
} else {
loadSystemAvatarBitmap(context, publicKey)
@@ -183,7 +193,78 @@ private fun loadSystemAvatarBitmap(context: Context, publicKey: String): Bitmap?
val drawable = AppCompatResources.getDrawable(context, resId) ?: return null
val width = drawable.intrinsicWidth.takeIf { it > 0 } ?: 256
val height = drawable.intrinsicHeight.takeIf { it > 0 } ?: 256
return drawable.toBitmap(width = width, height = height, config = Bitmap.Config.ARGB_8888)
val source = drawable.toBitmap(width = width, height = height, config = Bitmap.Config.ARGB_8888)
return recenterSystemBlurSource(source)
}
private data class FocusCenter(val x: Float, val y: Float)
private fun recenterSystemBlurSource(source: Bitmap): Bitmap {
val focusCenter = detectSystemFocusCenter(source) ?: return source
val dx = source.width / 2f - focusCenter.x
val dy = source.height / 2f - focusCenter.y
if (abs(dx) < 0.5f && abs(dy) < 0.5f) return source
val output = Bitmap.createBitmap(source.width, source.height, Bitmap.Config.ARGB_8888)
val canvas = Canvas(output)
val shader = BitmapShader(source, Shader.TileMode.CLAMP, Shader.TileMode.CLAMP)
val matrix = Matrix().apply { setTranslate(dx, dy) }
shader.setLocalMatrix(matrix)
val paint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
this.shader = shader
isFilterBitmap = true
isDither = true
}
canvas.drawRect(0f, 0f, source.width.toFloat(), source.height.toFloat(), paint)
return output
}
private fun detectSystemFocusCenter(source: Bitmap): FocusCenter? {
val width = source.width
val height = source.height
if (width < 2 || height < 2) return null
val cornerColors = intArrayOf(
source.getPixel(0, 0),
source.getPixel(width - 1, 0),
source.getPixel(0, height - 1),
source.getPixel(width - 1, height - 1)
)
val bgR = cornerColors.map { android.graphics.Color.red(it) }.average().toFloat()
val bgG = cornerColors.map { android.graphics.Color.green(it) }.average().toFloat()
val bgB = cornerColors.map { android.graphics.Color.blue(it) }.average().toFloat()
val step = (minOf(width, height) / 140).coerceAtLeast(1)
val threshold = 54f
var weightedX = 0f
var weightedY = 0f
var totalWeight = 0f
var y = 0
while (y < height) {
var x = 0
while (x < width) {
val pixel = source.getPixel(x, y)
val dr = abs(android.graphics.Color.red(pixel) - bgR)
val dg = abs(android.graphics.Color.green(pixel) - bgG)
val db = abs(android.graphics.Color.blue(pixel) - bgB)
val weight = (dr + dg + db) - threshold
if (weight > 0f) {
weightedX += x * weight
weightedY += y * weight
totalWeight += weight
}
x += step
}
y += step
}
if (totalWeight <= 0f) return null
return FocusCenter(
x = weightedX / totalWeight,
y = weightedY / totalWeight
)
}
/**

View File

@@ -1,7 +1,6 @@
package com.rosetta.messenger.ui.components
import androidx.compose.foundation.clickable
import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.*
@@ -16,6 +15,7 @@ import androidx.compose.ui.unit.sp
import androidx.compose.ui.window.Dialog
import com.rosetta.messenger.R
import com.rosetta.messenger.ui.onboarding.PrimaryBlue
import com.rosetta.messenger.ui.theme.LocalRosettaIsDarkTheme
/**
* Значок верификации пользователя
@@ -23,14 +23,14 @@ import com.rosetta.messenger.ui.onboarding.PrimaryBlue
*
* @param verified Уровень верификации (0 = нет, 1 = стандартная, 2+ = особая)
* @param size Размер значка в dp
* @param isDarkTheme Тема приложения (если не передано - используется системная)
* @param isDarkTheme Тема приложения (если не передано - вычисляется из MaterialTheme)
*/
@Composable
fun VerifiedBadge(
verified: Int,
size: Int = 16,
modifier: Modifier = Modifier,
isDarkTheme: Boolean = isSystemInDarkTheme(),
isDarkTheme: Boolean = LocalRosettaIsDarkTheme.current,
badgeTint: Color? = null
) {
if (verified <= 0) return

View File

@@ -19,6 +19,7 @@ import androidx.compose.animation.core.animateFloatAsState
import androidx.compose.animation.core.spring
import androidx.compose.animation.core.tween
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.gestures.detectTapGestures
import androidx.compose.foundation.pager.HorizontalPager
import androidx.compose.foundation.pager.rememberPagerState
import androidx.compose.foundation.BorderStroke
@@ -51,6 +52,7 @@ import androidx.compose.ui.hapticfeedback.HapticFeedbackType
import androidx.compose.ui.input.nestedscroll.NestedScrollConnection
import androidx.compose.ui.input.nestedscroll.NestedScrollSource
import androidx.compose.ui.input.nestedscroll.nestedScroll
import androidx.compose.ui.input.pointer.pointerInput
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.layout.onGloballyPositioned
import androidx.compose.ui.layout.onSizeChanged
@@ -192,6 +194,9 @@ fun OtherProfileScreen(
var isBlocked by remember { mutableStateOf(false) }
var showAvatarMenu by remember { mutableStateOf(false) }
var showImageViewer by remember { mutableStateOf(false) }
var showAvatarViewer by remember { mutableStateOf(false) }
var avatarViewerBitmap by remember { mutableStateOf<android.graphics.Bitmap?>(null) }
var avatarViewerTimestamp by remember { mutableStateOf(0L) }
var imageViewerInitialIndex by remember { mutableIntStateOf(0) }
var selectedTab by remember { mutableStateOf(OtherProfileTab.MEDIA) }
val pagerState = rememberPagerState(
@@ -212,12 +217,12 @@ fun OtherProfileScreen(
snapshotFlow { pagerState.currentPage }.collect { page ->
selectedTab = OtherProfileTab.entries[page]
// Swipe-back only on first tab (Media); on other tabs pager handles swipe
onSwipeBackEnabledChanged(page == 0 && !showImageViewer)
onSwipeBackEnabledChanged(page == 0 && !showImageViewer && !showAvatarViewer)
}
}
LaunchedEffect(showImageViewer) {
onSwipeBackEnabledChanged(!showImageViewer && pagerState.currentPage == 0)
LaunchedEffect(showImageViewer, showAvatarViewer) {
onSwipeBackEnabledChanged(!showImageViewer && !showAvatarViewer && pagerState.currentPage == 0)
}
val backgroundColor = if (isDarkTheme) Color(0xFF1A1A1A) else Color(0xFFFFFFFF)
@@ -1123,6 +1128,43 @@ fun OtherProfileScreen(
}
}
},
onAvatarLongPress = {
coroutineScope.launch {
when {
hasAvatar && avatars.isNotEmpty() -> {
val first = avatars.first()
avatarViewerTimestamp = first.timestamp
val bitmap = withContext(Dispatchers.IO) {
AvatarFileManager.base64ToBitmap(first.base64Data)
}
if (bitmap != null) {
avatarViewerBitmap = bitmap
showAvatarViewer = true
}
}
user.publicKey == MessageRepository.SYSTEM_SAFE_PUBLIC_KEY -> {
val bitmap = withContext(Dispatchers.IO) {
BitmapFactory.decodeResource(context.resources, R.drawable.safe_account)
}
if (bitmap != null) {
avatarViewerTimestamp = 0L
avatarViewerBitmap = bitmap
showAvatarViewer = true
}
}
user.publicKey == MessageRepository.SYSTEM_UPDATES_PUBLIC_KEY -> {
val bitmap = withContext(Dispatchers.IO) {
BitmapFactory.decodeResource(context.resources, R.drawable.updates_account)
}
if (bitmap != null) {
avatarViewerTimestamp = 0L
avatarViewerBitmap = bitmap
showAvatarViewer = true
}
}
}
}
},
backgroundBlurColorId = backgroundBlurColorId
)
@@ -1135,6 +1177,16 @@ fun OtherProfileScreen(
isDarkTheme = isDarkTheme
)
}
FullScreenAvatarViewer(
isVisible = showAvatarViewer,
onDismiss = { showAvatarViewer = false },
displayName = user.title.ifBlank { user.publicKey.take(10) },
avatarTimestamp = avatarViewerTimestamp,
avatarBitmap = avatarViewerBitmap,
publicKey = user.publicKey,
isDarkTheme = isDarkTheme
)
}
}
@@ -1815,6 +1867,7 @@ private fun CollapsingOtherProfileHeader(
onBlockToggle: () -> Unit,
avatarRepository: AvatarRepository? = null,
onClearChat: () -> Unit,
onAvatarLongPress: () -> Unit = {},
backgroundBlurColorId: String = "avatar"
) {
val density = LocalDensity.current
@@ -1943,7 +1996,12 @@ private fun CollapsingOtherProfileHeader(
shape = RoundedCornerShape(cornerRadius)
clip = true
}
.background(avatarColors.backgroundColor),
.background(avatarColors.backgroundColor)
.pointerInput(Unit) {
detectTapGestures(
onLongPress = { onAvatarLongPress() }
)
},
contentAlignment = Alignment.Center
) {
if (publicKey == MessageRepository.SYSTEM_SAFE_PUBLIC_KEY) {

View File

@@ -2074,7 +2074,13 @@ fun FullScreenAvatarViewer(
val dateText = remember(avatarTimestamp) {
if (avatarTimestamp > 0) {
val sdf = SimpleDateFormat("MMMM d, yyyy 'at' h:mm a", Locale.ENGLISH)
sdf.format(Date(avatarTimestamp * 1000))
val normalizedTimestampMs = when {
avatarTimestamp >= 1_000_000_000_000_000_000L -> avatarTimestamp / 1_000_000
avatarTimestamp >= 1_000_000_000_000_000L -> avatarTimestamp / 1_000
avatarTimestamp >= 1_000_000_000_000L -> avatarTimestamp
else -> avatarTimestamp * 1_000
}
sdf.format(Date(normalizedTimestampMs))
} else ""
}

View File

@@ -16,6 +16,8 @@ import androidx.core.view.WindowCompat
import com.rosetta.messenger.ui.utils.NavigationModeUtils
import kotlinx.coroutines.delay
val LocalRosettaIsDarkTheme = staticCompositionLocalOf { true }
private val DarkColorScheme = darkColorScheme(
primary = DarkPrimary,
secondary = Accent,
@@ -78,9 +80,11 @@ fun RosettaAndroidTheme(
}
}
MaterialTheme(
colorScheme = colorScheme,
typography = Typography,
content = content
)
CompositionLocalProvider(LocalRosettaIsDarkTheme provides darkTheme) {
MaterialTheme(
colorScheme = colorScheme,
typography = Typography,
content = content
)
}
}