Refactor image handling and decoding logic

- Introduced a maximum bitmap decode dimension to prevent excessive memory usage.
- Enhanced base64 to bitmap conversion by extracting payload and applying EXIF orientation.
- Improved error handling for image downloads and decoding processes.
- Simplified media picker and chat input components to manage keyboard visibility more effectively.
- Updated color selection grid to adaptively adjust based on available width.
- Added safety checks for notifications and call actions in profile screens.
- Optimized bitmap decoding in uriToBase64Image to handle large images more efficiently.
This commit is contained in:
2026-02-20 02:45:00 +05:00
parent 5cf8b2866f
commit 88e2084f8b
26 changed files with 943 additions and 464 deletions

View File

@@ -32,6 +32,7 @@ import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.platform.LocalView
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import com.rosetta.messenger.repository.AvatarRepository
@@ -405,7 +406,7 @@ private fun ProfileBlurPreview(
// ═══════════════════════════════════════════════════════════════════
// 🎨 COLOR GRID — сетка выбора цветов (8 в ряду)
// 🎨 COLOR GRID — адаптивная сетка выбора цветов
// ═══════════════════════════════════════════════════════════════════
@Composable
@@ -415,33 +416,59 @@ private fun ColorSelectionGrid(
onSelect: (String) -> Unit
) {
val allOptions = BackgroundBlurPresets.allWithDefault
val columns = 8
val horizontalPadding = 12.dp
val preferredColumns = 8
val minColumns = 6
val maxCircleSize = 40.dp
val minCircleSize = 32.dp
val minItemSpacing = 6.dp
Column(
BoxWithConstraints(
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 12.dp)
.padding(horizontal = horizontalPadding)
) {
val maxColumnsThatFit =
((maxWidth + minItemSpacing) / (maxCircleSize + minItemSpacing))
.toInt()
.coerceAtLeast(1)
val columns =
maxOf(minColumns, minOf(preferredColumns, maxColumnsThatFit))
val circleSize =
((maxWidth - minItemSpacing * (columns - 1)) / columns)
.coerceIn(minCircleSize, maxCircleSize)
val itemSpacing =
if (columns > 1) {
((maxWidth - circleSize * columns) / (columns - 1)).coerceAtLeast(minItemSpacing)
} else {
0.dp
}
Column(modifier = Modifier.fillMaxWidth()) {
allOptions.chunked(columns).forEach { rowItems ->
Row(
modifier = Modifier
.fillMaxWidth()
.padding(vertical = 6.dp),
horizontalArrangement = Arrangement.SpaceEvenly
horizontalArrangement = Arrangement.spacedBy(itemSpacing, Alignment.CenterHorizontally)
) {
rowItems.forEach { option ->
ColorCircleItem(
option = option,
isSelected = option.id == selectedId,
isDarkTheme = isDarkTheme,
circleSize = circleSize,
onClick = { onSelect(option.id) }
)
}
repeat(columns - rowItems.size) {
Spacer(modifier = Modifier.size(40.dp))
Spacer(modifier = Modifier.size(circleSize))
}
}
}
}
}
}
@@ -450,10 +477,11 @@ private fun ColorCircleItem(
option: BackgroundBlurOption,
isSelected: Boolean,
isDarkTheme: Boolean,
circleSize: Dp,
onClick: () -> Unit
) {
val scale by animateFloatAsState(
targetValue = if (isSelected) 1.15f else 1.0f,
targetValue = if (isSelected) 1.08f else 1.0f,
animationSpec = tween(200),
label = "scale"
)
@@ -470,7 +498,7 @@ private fun ColorCircleItem(
Box(
modifier = Modifier
.size(40.dp)
.size(circleSize)
.scale(scale)
.clip(CircleShape)
.border(
@@ -496,7 +524,7 @@ private fun ColorCircleItem(
imageVector = TablerIcons.X,
contentDescription = "None",
tint = Color.White.copy(alpha = 0.9f),
modifier = Modifier.size(18.dp)
modifier = Modifier.size((circleSize * 0.45f).coerceAtLeast(14.dp))
)
}
}
@@ -515,7 +543,7 @@ private fun ColorCircleItem(
imageVector = TablerIcons.CircleOff,
contentDescription = "Default",
tint = Color.White.copy(alpha = 0.9f),
modifier = Modifier.size(18.dp)
modifier = Modifier.size((circleSize * 0.45f).coerceAtLeast(14.dp))
)
}
}
@@ -549,7 +577,7 @@ private fun ColorCircleItem(
painter = TelegramIcons.Done,
contentDescription = "Selected",
tint = Color.White,
modifier = Modifier.size(18.dp)
modifier = Modifier.size((circleSize * 0.45f).coerceAtLeast(14.dp))
)
}
}

View File

@@ -57,6 +57,7 @@ import androidx.compose.ui.platform.LocalFocusManager
import androidx.compose.ui.platform.LocalHapticFeedback
import androidx.compose.ui.platform.LocalSoftwareKeyboardController
import androidx.compose.ui.platform.LocalView
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.text.style.TextDecoration
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextAlign
@@ -72,6 +73,7 @@ import com.airbnb.lottie.compose.LottieCompositionSpec
import com.airbnb.lottie.compose.LottieConstants
import com.airbnb.lottie.compose.animateLottieCompositionAsState
import com.airbnb.lottie.compose.rememberLottieComposition
import com.rosetta.messenger.R
import com.rosetta.messenger.crypto.CryptoManager
import com.rosetta.messenger.crypto.MessageCrypto
import com.rosetta.messenger.data.MessageRepository
@@ -196,6 +198,9 @@ fun OtherProfileScreen(
val backgroundColor = if (isDarkTheme) Color(0xFF1A1A1A) else Color(0xFFFFFFFF)
val avatarColors = getAvatarColor(user.publicKey, isDarkTheme)
val isSafetyProfile = remember(user.publicKey) {
user.publicKey == MessageRepository.SYSTEM_SAFE_PUBLIC_KEY
}
val context = LocalContext.current
val view = LocalView.current
val window = remember { (view.context as? Activity)?.window }
@@ -585,35 +590,37 @@ fun OtherProfileScreen(
)
}
// Call
Button(
onClick = { /* TODO: call action */ },
modifier = Modifier
.weight(1f)
.height(48.dp),
shape = RoundedCornerShape(12.dp),
colors = ButtonDefaults.buttonColors(
containerColor = if (isDarkTheme) Color(0xFF2C2C2E) else Color(0xFFE8E8ED),
contentColor = if (isDarkTheme) Color.White else Color.Black
),
elevation = ButtonDefaults.buttonElevation(
defaultElevation = 0.dp,
pressedElevation = 0.dp
if (!isSafetyProfile) {
// Call
Button(
onClick = { /* TODO: call action */ },
modifier = Modifier
.weight(1f)
.height(48.dp),
shape = RoundedCornerShape(12.dp),
colors = ButtonDefaults.buttonColors(
containerColor = if (isDarkTheme) Color(0xFF2C2C2E) else Color(0xFFE8E8ED),
contentColor = if (isDarkTheme) Color.White else Color.Black
),
elevation = ButtonDefaults.buttonElevation(
defaultElevation = 0.dp,
pressedElevation = 0.dp
)
) {
Icon(
imageVector = TablerIcons.Phone,
contentDescription = null,
modifier = Modifier.size(20.dp),
tint = if (isDarkTheme) Color.White else PrimaryBlue
)
) {
Icon(
imageVector = TablerIcons.Phone,
contentDescription = null,
modifier = Modifier.size(20.dp),
tint = if (isDarkTheme) Color.White else PrimaryBlue
)
Spacer(modifier = Modifier.width(8.dp))
Text(
text = "Call",
fontSize = 15.sp,
fontWeight = FontWeight.SemiBold,
color = if (isDarkTheme) Color.White else Color.Black
)
Spacer(modifier = Modifier.width(8.dp))
Text(
text = "Call",
fontSize = 15.sp,
fontWeight = FontWeight.SemiBold,
color = if (isDarkTheme) Color.White else Color.Black
)
}
}
}
@@ -663,23 +670,25 @@ fun OtherProfileScreen(
// ═══════════════════════════════════════════════════════════
// 🔔 NOTIFICATIONS SECTION
// ═══════════════════════════════════════════════════════════
TelegramToggleItem(
icon = if (notificationsEnabled) TelegramIcons.Notifications else TelegramIcons.Mute,
title = "Notifications",
subtitle = if (notificationsEnabled) "On" else "Off",
isEnabled = notificationsEnabled,
onToggle = {
notificationsEnabled = !notificationsEnabled
coroutineScope.launch {
preferencesManager.setChatMuted(
activeAccountPublicKey,
user.publicKey,
!notificationsEnabled
)
}
},
isDarkTheme = isDarkTheme
)
if (!isSafetyProfile) {
TelegramToggleItem(
icon = if (notificationsEnabled) TelegramIcons.Notifications else TelegramIcons.Mute,
title = "Notifications",
subtitle = if (notificationsEnabled) "On" else "Off",
isEnabled = notificationsEnabled,
onToggle = {
notificationsEnabled = !notificationsEnabled
coroutineScope.launch {
preferencesManager.setChatMuted(
activeAccountPublicKey,
user.publicKey,
!notificationsEnabled
)
}
},
isDarkTheme = isDarkTheme
)
}
// ═══════════════════════════════════════════════════════════
// 📚 SHARED CONTENT (без разделителя — сразу табы)
@@ -1862,7 +1871,14 @@ private fun CollapsingOtherProfileHeader(
.background(avatarColors.backgroundColor),
contentAlignment = Alignment.Center
) {
if (hasAvatar && avatarRepository != null) {
if (publicKey == MessageRepository.SYSTEM_SAFE_PUBLIC_KEY) {
Image(
painter = painterResource(id = R.drawable.safe_account),
contentDescription = "Safe avatar",
modifier = Modifier.fillMaxSize(),
contentScale = ContentScale.Crop
)
} else if (hasAvatar && avatarRepository != null) {
OtherProfileFullSizeAvatar(
publicKey = publicKey,
avatarRepository = avatarRepository,

View File

@@ -437,6 +437,7 @@ fun ProfileScreen(
// Scroll state for collapsing header + overscroll avatar expansion
val density = LocalDensity.current
val statusBarHeight = WindowInsets.statusBars.asPaddingValues().calculateTopPadding()
val navigationBarHeight = WindowInsets.navigationBars.asPaddingValues().calculateBottomPadding()
// Header heights
val expandedHeightPx = with(density) { (EXPANDED_HEADER_HEIGHT + statusBarHeight).toPx() }
@@ -759,7 +760,11 @@ fun ProfileScreen(
LazyColumn(
state = listState,
modifier = Modifier.fillMaxSize(),
contentPadding = PaddingValues(top = collapsedHeightDp)
contentPadding =
PaddingValues(
top = collapsedHeightDp,
bottom = navigationBarHeight + 28.dp
)
) {
// Item 0: spacer = ровно сколько нужно проскроллить для collapse
item {
@@ -905,7 +910,7 @@ fun ProfileScreen(
// ═════════════════════════════════════════════════════════════
TelegramLogoutItem(onClick = onLogout, isDarkTheme = isDarkTheme)
Spacer(modifier = Modifier.height(32.dp))
Spacer(modifier = Modifier.height(20.dp))
}
}
@@ -1127,6 +1132,7 @@ private fun CollapsingProfileHeader(
Box(modifier = Modifier.fillMaxWidth().height(headerHeight).clipToBounds()) {
// Expansion fraction — computed early so gradient can fade during expansion
val expandFraction = expansionProgress.coerceIn(0f, 1f)
val headerBaseColor = if (isDarkTheme) Color(0xFF2A2A2A) else Color(0xFF0D8CF4)
// ═══════════════════════════════════════════════════════════
// 🎨 BLURRED AVATAR BACKGROUND — ВСЕГДА видим
@@ -1134,12 +1140,13 @@ private fun CollapsingProfileHeader(
// и естественно перекрывает его. Без мерцания.
// ═══════════════════════════════════════════════════════════
Box(modifier = Modifier.matchParentSize()) {
Box(modifier = Modifier.matchParentSize().background(headerBaseColor))
if (backgroundBlurColorId == "none") {
// None — стандартный цвет шапки без blur
Box(
modifier = Modifier
.fillMaxSize()
.background(if (isDarkTheme) Color(0xFF2A2A2A) else Color(0xFF0D8CF4))
.background(headerBaseColor)
)
} else {
BlurredAvatarBackground(
@@ -1147,7 +1154,7 @@ private fun CollapsingProfileHeader(
avatarRepository = avatarRepository,
fallbackColor = avatarColors.backgroundColor,
blurRadius = 20f,
alpha = 0.9f,
alpha = 1f,
overlayColors = BackgroundBlurPresets.getOverlayColors(backgroundBlurColorId),
isDarkTheme = isDarkTheme
)
@@ -1606,38 +1613,13 @@ private fun TelegramTextField(
val dividerColor = if (isDarkTheme) Color(0xFF3A3A3A) else Color(0xFFE0E0E0)
val hasError = !errorText.isNullOrBlank()
val errorColor = Color(0xFFFF3B30)
val labelText = if (hasError) errorText.orEmpty() else label
val labelColor by
animateColorAsState(
targetValue = if (hasError) errorColor else secondaryTextColor,
label = "profile_field_label_color"
)
val containerColor by
animateColorAsState(
targetValue =
if (hasError) {
if (isDarkTheme) errorColor.copy(alpha = 0.18f)
else errorColor.copy(alpha = 0.08f)
} else {
Color.Transparent
},
label = "profile_field_container_color"
)
val borderColor by
animateColorAsState(
targetValue = if (hasError) errorColor else Color.Transparent,
label = "profile_field_border_color"
)
val fieldModifier =
if (hasError) {
Modifier.fillMaxWidth()
.padding(horizontal = 16.dp, vertical = 8.dp)
.clip(RoundedCornerShape(12.dp))
.background(containerColor)
.border(1.dp, borderColor, RoundedCornerShape(12.dp))
.padding(horizontal = 12.dp, vertical = 8.dp)
} else {
Modifier.fillMaxWidth().padding(horizontal = 16.dp, vertical = 12.dp)
}
val fieldModifier = Modifier.fillMaxWidth().padding(horizontal = 16.dp, vertical = 12.dp)
Column {
Column(modifier = fieldModifier) {
@@ -1672,17 +1654,7 @@ private fun TelegramTextField(
Spacer(modifier = Modifier.height(4.dp))
Text(text = label, fontSize = 13.sp, color = labelColor)
if (hasError) {
Spacer(modifier = Modifier.height(4.dp))
Text(
text = errorText ?: "",
fontSize = 12.sp,
color = errorColor,
lineHeight = 14.sp
)
}
Text(text = labelText, fontSize = 13.sp, color = labelColor)
}
if (showDivider) {