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:
@@ -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))
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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) {
|
||||
|
||||
Reference in New Issue
Block a user