Релиз 1.1.6: сессии, аватарки и интерфейсные исправления
This commit is contained in:
@@ -23,8 +23,8 @@ val gitShortSha = safeGitOutput("rev-parse", "--short", "HEAD") ?: "unknown"
|
|||||||
// ═══════════════════════════════════════════════════════════
|
// ═══════════════════════════════════════════════════════════
|
||||||
// Rosetta versioning — bump here on each release
|
// Rosetta versioning — bump here on each release
|
||||||
// ═══════════════════════════════════════════════════════════
|
// ═══════════════════════════════════════════════════════════
|
||||||
val rosettaVersionName = "1.1.5"
|
val rosettaVersionName = "1.1.6"
|
||||||
val rosettaVersionCode = 17 // Increment on each release
|
val rosettaVersionCode = 18 // Increment on each release
|
||||||
|
|
||||||
android {
|
android {
|
||||||
namespace = "com.rosetta.messenger"
|
namespace = "com.rosetta.messenger"
|
||||||
|
|||||||
@@ -72,6 +72,8 @@ class MainActivity : FragmentActivity() {
|
|||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
private const val TAG = "MainActivity"
|
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
|
// 🔔 FCM Логи для отображения в UI
|
||||||
private val _fcmLogs = mutableStateListOf<String>()
|
private val _fcmLogs = mutableStateListOf<String>()
|
||||||
@@ -87,8 +89,18 @@ class MainActivity : FragmentActivity() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun clearFcmLogs() {
|
fun clearFcmLogs() {
|
||||||
_fcmLogs.clear()
|
_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 showSplash by remember { mutableStateOf(true) }
|
||||||
var showOnboarding by remember { mutableStateOf(true) }
|
var showOnboarding by remember { mutableStateOf(true) }
|
||||||
var hasExistingAccount by remember { mutableStateOf<Boolean?>(null) }
|
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 accountInfoList by remember { mutableStateOf<List<AccountInfo>>(emptyList()) }
|
||||||
var startCreateAccountFlow by remember { mutableStateOf(false) }
|
var startCreateAccountFlow by remember { mutableStateOf(false) }
|
||||||
|
|
||||||
// Check for existing accounts and build AccountInfo list
|
// Check for existing accounts and build AccountInfo list
|
||||||
// Also force logout so user always sees unlock screen on app restart
|
|
||||||
LaunchedEffect(Unit) {
|
LaunchedEffect(Unit) {
|
||||||
accountManager.logout() // Always start logged out
|
|
||||||
val accounts = accountManager.getAllAccounts()
|
val accounts = accountManager.getAllAccounts()
|
||||||
hasExistingAccount = accounts.isNotEmpty()
|
hasExistingAccount = accounts.isNotEmpty()
|
||||||
accountInfoList = accounts.map { it.toAccountInfo() }
|
accountInfoList = accounts.map { it.toAccountInfo() }
|
||||||
@@ -239,6 +249,7 @@ class MainActivity : FragmentActivity() {
|
|||||||
onAuthComplete = { account ->
|
onAuthComplete = { account ->
|
||||||
startCreateAccountFlow = false
|
startCreateAccountFlow = false
|
||||||
currentAccount = account
|
currentAccount = account
|
||||||
|
cacheSessionAccount(account)
|
||||||
hasExistingAccount = true
|
hasExistingAccount = true
|
||||||
// Save as last logged account
|
// Save as last logged account
|
||||||
account?.let {
|
account?.let {
|
||||||
@@ -256,6 +267,7 @@ class MainActivity : FragmentActivity() {
|
|||||||
// Set currentAccount to null immediately to prevent UI
|
// Set currentAccount to null immediately to prevent UI
|
||||||
// lag
|
// lag
|
||||||
currentAccount = null
|
currentAccount = null
|
||||||
|
clearCachedSessionAccount()
|
||||||
scope.launch {
|
scope.launch {
|
||||||
com.rosetta.messenger.network.ProtocolManager
|
com.rosetta.messenger.network.ProtocolManager
|
||||||
.disconnect()
|
.disconnect()
|
||||||
@@ -283,6 +295,7 @@ class MainActivity : FragmentActivity() {
|
|||||||
// Set currentAccount to null immediately to prevent UI
|
// Set currentAccount to null immediately to prevent UI
|
||||||
// lag
|
// lag
|
||||||
currentAccount = null
|
currentAccount = null
|
||||||
|
clearCachedSessionAccount()
|
||||||
scope.launch {
|
scope.launch {
|
||||||
com.rosetta.messenger.network.ProtocolManager
|
com.rosetta.messenger.network.ProtocolManager
|
||||||
.disconnect()
|
.disconnect()
|
||||||
@@ -315,6 +328,7 @@ class MainActivity : FragmentActivity() {
|
|||||||
hasExistingAccount = accounts.isNotEmpty()
|
hasExistingAccount = accounts.isNotEmpty()
|
||||||
// 8. Navigate away last
|
// 8. Navigate away last
|
||||||
currentAccount = null
|
currentAccount = null
|
||||||
|
clearCachedSessionAccount()
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
android.util.Log.e("DeleteAccount", "Failed to delete account", e)
|
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
|
// 9. If current account is deleted, return to main login screen
|
||||||
if (currentAccount?.publicKey == targetPublicKey) {
|
if (currentAccount?.publicKey == targetPublicKey) {
|
||||||
currentAccount = null
|
currentAccount = null
|
||||||
|
clearCachedSessionAccount()
|
||||||
}
|
}
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
android.util.Log.e("DeleteAccount", "Failed to delete account from sidebar", e)
|
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.
|
// Switch to another account: logout current, then show unlock.
|
||||||
currentAccount = null
|
currentAccount = null
|
||||||
|
clearCachedSessionAccount()
|
||||||
scope.launch {
|
scope.launch {
|
||||||
com.rosetta.messenger.network.ProtocolManager.disconnect()
|
com.rosetta.messenger.network.ProtocolManager.disconnect()
|
||||||
accountManager.logout()
|
accountManager.logout()
|
||||||
@@ -375,6 +391,7 @@ class MainActivity : FragmentActivity() {
|
|||||||
onAddAccount = {
|
onAddAccount = {
|
||||||
startCreateAccountFlow = true
|
startCreateAccountFlow = true
|
||||||
currentAccount = null
|
currentAccount = null
|
||||||
|
clearCachedSessionAccount()
|
||||||
scope.launch {
|
scope.launch {
|
||||||
com.rosetta.messenger.network.ProtocolManager.disconnect()
|
com.rosetta.messenger.network.ProtocolManager.disconnect()
|
||||||
accountManager.logout()
|
accountManager.logout()
|
||||||
@@ -387,6 +404,7 @@ class MainActivity : FragmentActivity() {
|
|||||||
isDarkTheme = isDarkTheme,
|
isDarkTheme = isDarkTheme,
|
||||||
onExit = {
|
onExit = {
|
||||||
currentAccount = null
|
currentAccount = null
|
||||||
|
clearCachedSessionAccount()
|
||||||
scope.launch {
|
scope.launch {
|
||||||
ProtocolManager.disconnect()
|
ProtocolManager.disconnect()
|
||||||
accountManager.logout()
|
accountManager.logout()
|
||||||
|
|||||||
@@ -17,17 +17,18 @@ object ReleaseNotes {
|
|||||||
val RELEASE_NOTICE = """
|
val RELEASE_NOTICE = """
|
||||||
Update v$VERSION_PLACEHOLDER
|
Update v$VERSION_PLACEHOLDER
|
||||||
|
|
||||||
Подключение
|
Профили и аватарки
|
||||||
- Ускорен старт соединения и handshake при входе в аккаунт
|
- Добавлен полноэкранный просмотр аватарок в чужом профиле (включая системные аккаунты)
|
||||||
- Логика reconnect синхронизирована с desktop-поведением
|
- Исправлено отображение даты аватарки: устранён некорректный год (например, 58154)
|
||||||
- Обновлён серверный endpoint на основной production (wss)
|
|
||||||
|
|
||||||
Группы
|
Сессии и вход
|
||||||
- Добавлено предзагруженное кэширование участников группы
|
- Добавлен кэш сессии в памяти процесса: повторный пароль не запрашивается, пока процесс жив
|
||||||
- Убран скачок "0 members" при повторном открытии группы
|
- Кэш сессии корректно очищается при выходе, переключении и удалении аккаунта
|
||||||
|
|
||||||
Интерфейс
|
Интерфейс
|
||||||
- Исправлено вертикальное выравнивание verified-галочки в списке чатов
|
- Исправлено центрирование blur-фона у системных аватарок
|
||||||
|
- Унифицировано определение темы для verified-галочек
|
||||||
|
- В списке чатов verified-галочки сделаны синими в светлой теме (включая system light)
|
||||||
""".trimIndent()
|
""".trimIndent()
|
||||||
|
|
||||||
fun getNotice(version: String): String =
|
fun getNotice(version: String): String =
|
||||||
|
|||||||
@@ -935,7 +935,7 @@ fun ChatsListScreen(
|
|||||||
VerifiedBadge(
|
VerifiedBadge(
|
||||||
verified = if (accountVerified > 0) accountVerified else 1,
|
verified = if (accountVerified > 0) accountVerified else 1,
|
||||||
size = 15,
|
size = 15,
|
||||||
badgeTint = Color(0xFFACD2F9)
|
badgeTint = PrimaryBlue
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -3891,7 +3891,9 @@ fun DialogItemContent(
|
|||||||
VerifiedBadge(
|
VerifiedBadge(
|
||||||
verified = if (dialog.verified > 0) dialog.verified else 1,
|
verified = if (dialog.verified > 0) dialog.verified else 1,
|
||||||
size = 16,
|
size = 16,
|
||||||
modifier = Modifier.offset(y = (-2).dp)
|
modifier = Modifier.offset(y = (-2).dp),
|
||||||
|
isDarkTheme = isDarkTheme,
|
||||||
|
badgeTint = PrimaryBlue
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
// 🔒 Красная иконка замочка для заблокированных пользователей
|
// 🔒 Красная иконка замочка для заблокированных пользователей
|
||||||
|
|||||||
@@ -2,6 +2,11 @@ package com.rosetta.messenger.ui.components
|
|||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.graphics.Bitmap
|
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.os.Build
|
||||||
import android.renderscript.Allocation
|
import android.renderscript.Allocation
|
||||||
import android.renderscript.Element
|
import android.renderscript.Element
|
||||||
@@ -32,6 +37,7 @@ import com.rosetta.messenger.repository.AvatarRepository
|
|||||||
import com.rosetta.messenger.utils.AvatarFileManager
|
import com.rosetta.messenger.utils.AvatarFileManager
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
|
import kotlin.math.abs
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Компонент для отображения размытого фона аватарки
|
* Компонент для отображения размытого фона аватарки
|
||||||
@@ -63,7 +69,11 @@ fun BoxScope.BlurredAvatarBackground(
|
|||||||
LaunchedEffect(avatarKey, publicKey) {
|
LaunchedEffect(avatarKey, publicKey) {
|
||||||
val currentAvatars = avatars
|
val currentAvatars = avatars
|
||||||
val newOriginal = withContext(Dispatchers.IO) {
|
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)
|
AvatarFileManager.base64ToBitmap(currentAvatars.first().base64Data)
|
||||||
} else {
|
} else {
|
||||||
loadSystemAvatarBitmap(context, publicKey)
|
loadSystemAvatarBitmap(context, publicKey)
|
||||||
@@ -183,7 +193,78 @@ private fun loadSystemAvatarBitmap(context: Context, publicKey: String): Bitmap?
|
|||||||
val drawable = AppCompatResources.getDrawable(context, resId) ?: return null
|
val drawable = AppCompatResources.getDrawable(context, resId) ?: return null
|
||||||
val width = drawable.intrinsicWidth.takeIf { it > 0 } ?: 256
|
val width = drawable.intrinsicWidth.takeIf { it > 0 } ?: 256
|
||||||
val height = drawable.intrinsicHeight.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
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
package com.rosetta.messenger.ui.components
|
package com.rosetta.messenger.ui.components
|
||||||
|
|
||||||
import androidx.compose.foundation.clickable
|
import androidx.compose.foundation.clickable
|
||||||
import androidx.compose.foundation.isSystemInDarkTheme
|
|
||||||
import androidx.compose.foundation.layout.*
|
import androidx.compose.foundation.layout.*
|
||||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
import androidx.compose.material3.*
|
import androidx.compose.material3.*
|
||||||
@@ -16,6 +15,7 @@ import androidx.compose.ui.unit.sp
|
|||||||
import androidx.compose.ui.window.Dialog
|
import androidx.compose.ui.window.Dialog
|
||||||
import com.rosetta.messenger.R
|
import com.rosetta.messenger.R
|
||||||
import com.rosetta.messenger.ui.onboarding.PrimaryBlue
|
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 verified Уровень верификации (0 = нет, 1 = стандартная, 2+ = особая)
|
||||||
* @param size Размер значка в dp
|
* @param size Размер значка в dp
|
||||||
* @param isDarkTheme Тема приложения (если не передано - используется системная)
|
* @param isDarkTheme Тема приложения (если не передано - вычисляется из MaterialTheme)
|
||||||
*/
|
*/
|
||||||
@Composable
|
@Composable
|
||||||
fun VerifiedBadge(
|
fun VerifiedBadge(
|
||||||
verified: Int,
|
verified: Int,
|
||||||
size: Int = 16,
|
size: Int = 16,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
isDarkTheme: Boolean = isSystemInDarkTheme(),
|
isDarkTheme: Boolean = LocalRosettaIsDarkTheme.current,
|
||||||
badgeTint: Color? = null
|
badgeTint: Color? = null
|
||||||
) {
|
) {
|
||||||
if (verified <= 0) return
|
if (verified <= 0) return
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ import androidx.compose.animation.core.animateFloatAsState
|
|||||||
import androidx.compose.animation.core.spring
|
import androidx.compose.animation.core.spring
|
||||||
import androidx.compose.animation.core.tween
|
import androidx.compose.animation.core.tween
|
||||||
import androidx.compose.foundation.ExperimentalFoundationApi
|
import androidx.compose.foundation.ExperimentalFoundationApi
|
||||||
|
import androidx.compose.foundation.gestures.detectTapGestures
|
||||||
import androidx.compose.foundation.pager.HorizontalPager
|
import androidx.compose.foundation.pager.HorizontalPager
|
||||||
import androidx.compose.foundation.pager.rememberPagerState
|
import androidx.compose.foundation.pager.rememberPagerState
|
||||||
import androidx.compose.foundation.BorderStroke
|
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.NestedScrollConnection
|
||||||
import androidx.compose.ui.input.nestedscroll.NestedScrollSource
|
import androidx.compose.ui.input.nestedscroll.NestedScrollSource
|
||||||
import androidx.compose.ui.input.nestedscroll.nestedScroll
|
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.ContentScale
|
||||||
import androidx.compose.ui.layout.onGloballyPositioned
|
import androidx.compose.ui.layout.onGloballyPositioned
|
||||||
import androidx.compose.ui.layout.onSizeChanged
|
import androidx.compose.ui.layout.onSizeChanged
|
||||||
@@ -192,6 +194,9 @@ fun OtherProfileScreen(
|
|||||||
var isBlocked by remember { mutableStateOf(false) }
|
var isBlocked by remember { mutableStateOf(false) }
|
||||||
var showAvatarMenu by remember { mutableStateOf(false) }
|
var showAvatarMenu by remember { mutableStateOf(false) }
|
||||||
var showImageViewer 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 imageViewerInitialIndex by remember { mutableIntStateOf(0) }
|
||||||
var selectedTab by remember { mutableStateOf(OtherProfileTab.MEDIA) }
|
var selectedTab by remember { mutableStateOf(OtherProfileTab.MEDIA) }
|
||||||
val pagerState = rememberPagerState(
|
val pagerState = rememberPagerState(
|
||||||
@@ -212,12 +217,12 @@ fun OtherProfileScreen(
|
|||||||
snapshotFlow { pagerState.currentPage }.collect { page ->
|
snapshotFlow { pagerState.currentPage }.collect { page ->
|
||||||
selectedTab = OtherProfileTab.entries[page]
|
selectedTab = OtherProfileTab.entries[page]
|
||||||
// Swipe-back only on first tab (Media); on other tabs pager handles swipe
|
// Swipe-back only on first tab (Media); on other tabs pager handles swipe
|
||||||
onSwipeBackEnabledChanged(page == 0 && !showImageViewer)
|
onSwipeBackEnabledChanged(page == 0 && !showImageViewer && !showAvatarViewer)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
LaunchedEffect(showImageViewer) {
|
LaunchedEffect(showImageViewer, showAvatarViewer) {
|
||||||
onSwipeBackEnabledChanged(!showImageViewer && pagerState.currentPage == 0)
|
onSwipeBackEnabledChanged(!showImageViewer && !showAvatarViewer && pagerState.currentPage == 0)
|
||||||
}
|
}
|
||||||
|
|
||||||
val backgroundColor = if (isDarkTheme) Color(0xFF1A1A1A) else Color(0xFFFFFFFF)
|
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
|
backgroundBlurColorId = backgroundBlurColorId
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -1135,6 +1177,16 @@ fun OtherProfileScreen(
|
|||||||
isDarkTheme = isDarkTheme
|
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,
|
onBlockToggle: () -> Unit,
|
||||||
avatarRepository: AvatarRepository? = null,
|
avatarRepository: AvatarRepository? = null,
|
||||||
onClearChat: () -> Unit,
|
onClearChat: () -> Unit,
|
||||||
|
onAvatarLongPress: () -> Unit = {},
|
||||||
backgroundBlurColorId: String = "avatar"
|
backgroundBlurColorId: String = "avatar"
|
||||||
) {
|
) {
|
||||||
val density = LocalDensity.current
|
val density = LocalDensity.current
|
||||||
@@ -1943,7 +1996,12 @@ private fun CollapsingOtherProfileHeader(
|
|||||||
shape = RoundedCornerShape(cornerRadius)
|
shape = RoundedCornerShape(cornerRadius)
|
||||||
clip = true
|
clip = true
|
||||||
}
|
}
|
||||||
.background(avatarColors.backgroundColor),
|
.background(avatarColors.backgroundColor)
|
||||||
|
.pointerInput(Unit) {
|
||||||
|
detectTapGestures(
|
||||||
|
onLongPress = { onAvatarLongPress() }
|
||||||
|
)
|
||||||
|
},
|
||||||
contentAlignment = Alignment.Center
|
contentAlignment = Alignment.Center
|
||||||
) {
|
) {
|
||||||
if (publicKey == MessageRepository.SYSTEM_SAFE_PUBLIC_KEY) {
|
if (publicKey == MessageRepository.SYSTEM_SAFE_PUBLIC_KEY) {
|
||||||
|
|||||||
@@ -2074,7 +2074,13 @@ fun FullScreenAvatarViewer(
|
|||||||
val dateText = remember(avatarTimestamp) {
|
val dateText = remember(avatarTimestamp) {
|
||||||
if (avatarTimestamp > 0) {
|
if (avatarTimestamp > 0) {
|
||||||
val sdf = SimpleDateFormat("MMMM d, yyyy 'at' h:mm a", Locale.ENGLISH)
|
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 ""
|
} else ""
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -16,6 +16,8 @@ import androidx.core.view.WindowCompat
|
|||||||
import com.rosetta.messenger.ui.utils.NavigationModeUtils
|
import com.rosetta.messenger.ui.utils.NavigationModeUtils
|
||||||
import kotlinx.coroutines.delay
|
import kotlinx.coroutines.delay
|
||||||
|
|
||||||
|
val LocalRosettaIsDarkTheme = staticCompositionLocalOf { true }
|
||||||
|
|
||||||
private val DarkColorScheme = darkColorScheme(
|
private val DarkColorScheme = darkColorScheme(
|
||||||
primary = DarkPrimary,
|
primary = DarkPrimary,
|
||||||
secondary = Accent,
|
secondary = Accent,
|
||||||
@@ -78,9 +80,11 @@ fun RosettaAndroidTheme(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
MaterialTheme(
|
CompositionLocalProvider(LocalRosettaIsDarkTheme provides darkTheme) {
|
||||||
colorScheme = colorScheme,
|
MaterialTheme(
|
||||||
typography = Typography,
|
colorScheme = colorScheme,
|
||||||
content = content
|
typography = Typography,
|
||||||
)
|
content = content
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user