Refactor code structure for improved readability and maintainability
This commit is contained in:
@@ -461,6 +461,10 @@ fun ImageAttachment(
|
|||||||
fillMaxSize: Boolean = false,
|
fillMaxSize: Boolean = false,
|
||||||
onImageClick: (attachmentId: String) -> Unit = {}
|
onImageClick: (attachmentId: String) -> Unit = {}
|
||||||
) {
|
) {
|
||||||
|
Log.d(
|
||||||
|
TAG,
|
||||||
|
"🖼️ ImageAttachment: id=${attachment.id}, isOutgoing=$isOutgoing, messageStatus=$messageStatus, showTimeOverlay=$showTimeOverlay"
|
||||||
|
)
|
||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
val scope = rememberCoroutineScope()
|
val scope = rememberCoroutineScope()
|
||||||
|
|
||||||
@@ -845,7 +849,7 @@ fun ImageAttachment(
|
|||||||
}
|
}
|
||||||
MessageStatus.DELIVERED -> {
|
MessageStatus.DELIVERED -> {
|
||||||
Icon(
|
Icon(
|
||||||
compose.icons.TablerIcons.Checks,
|
compose.icons.TablerIcons.Check,
|
||||||
contentDescription = null,
|
contentDescription = null,
|
||||||
tint = Color.White.copy(alpha = 0.7f),
|
tint = Color.White.copy(alpha = 0.7f),
|
||||||
modifier = Modifier.size(14.dp)
|
modifier = Modifier.size(14.dp)
|
||||||
@@ -855,7 +859,7 @@ fun ImageAttachment(
|
|||||||
Icon(
|
Icon(
|
||||||
compose.icons.TablerIcons.Checks,
|
compose.icons.TablerIcons.Checks,
|
||||||
contentDescription = null,
|
contentDescription = null,
|
||||||
tint = Color(0xFF4FC3F7),
|
tint = Color.White,
|
||||||
modifier = Modifier.size(14.dp)
|
modifier = Modifier.size(14.dp)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -1578,7 +1582,7 @@ fun AvatarAttachment(
|
|||||||
}
|
}
|
||||||
MessageStatus.DELIVERED -> {
|
MessageStatus.DELIVERED -> {
|
||||||
Icon(
|
Icon(
|
||||||
compose.icons.TablerIcons.Checks,
|
compose.icons.TablerIcons.Check,
|
||||||
contentDescription = "Delivered",
|
contentDescription = "Delivered",
|
||||||
tint = Color.White.copy(alpha = 0.6f),
|
tint = Color.White.copy(alpha = 0.6f),
|
||||||
modifier = Modifier.size(14.dp)
|
modifier = Modifier.size(14.dp)
|
||||||
@@ -1588,7 +1592,7 @@ fun AvatarAttachment(
|
|||||||
Icon(
|
Icon(
|
||||||
compose.icons.TablerIcons.Checks,
|
compose.icons.TablerIcons.Checks,
|
||||||
contentDescription = "Read",
|
contentDescription = "Read",
|
||||||
tint = Color(0xFF4FC3F7),
|
tint = Color.White,
|
||||||
modifier = Modifier.size(14.dp)
|
modifier = Modifier.size(14.dp)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,9 @@
|
|||||||
package com.rosetta.messenger.ui.chats.components
|
package com.rosetta.messenger.ui.chats.components
|
||||||
|
|
||||||
import android.graphics.Bitmap
|
import android.graphics.Bitmap
|
||||||
|
import android.graphics.BitmapFactory
|
||||||
|
import android.util.Base64
|
||||||
|
import android.util.Log
|
||||||
import androidx.compose.animation.*
|
import androidx.compose.animation.*
|
||||||
import androidx.compose.animation.core.*
|
import androidx.compose.animation.core.*
|
||||||
import androidx.compose.foundation.ExperimentalFoundationApi
|
import androidx.compose.foundation.ExperimentalFoundationApi
|
||||||
@@ -11,21 +14,16 @@ import androidx.compose.foundation.clickable
|
|||||||
import androidx.compose.foundation.combinedClickable
|
import androidx.compose.foundation.combinedClickable
|
||||||
import androidx.compose.foundation.gestures.detectHorizontalDragGestures
|
import androidx.compose.foundation.gestures.detectHorizontalDragGestures
|
||||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||||
import androidx.compose.foundation.interaction.collectIsPressedAsState
|
|
||||||
import androidx.compose.foundation.layout.*
|
import androidx.compose.foundation.layout.*
|
||||||
import androidx.compose.foundation.shape.CircleShape
|
import androidx.compose.foundation.shape.CircleShape
|
||||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
import androidx.compose.material3.*
|
import androidx.compose.material3.*
|
||||||
import compose.icons.TablerIcons
|
|
||||||
import compose.icons.tablericons.*
|
|
||||||
import androidx.compose.runtime.*
|
import androidx.compose.runtime.*
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.draw.clip
|
import androidx.compose.ui.draw.clip
|
||||||
import androidx.compose.ui.draw.scale
|
import androidx.compose.ui.draw.scale
|
||||||
import androidx.compose.ui.draw.shadow
|
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
import androidx.compose.ui.graphics.TransformOrigin
|
|
||||||
import androidx.compose.ui.graphics.asImageBitmap
|
import androidx.compose.ui.graphics.asImageBitmap
|
||||||
import androidx.compose.ui.graphics.graphicsLayer
|
import androidx.compose.ui.graphics.graphicsLayer
|
||||||
import androidx.compose.ui.graphics.vector.ImageVector
|
import androidx.compose.ui.graphics.vector.ImageVector
|
||||||
@@ -37,38 +35,34 @@ import androidx.compose.ui.text.style.TextOverflow
|
|||||||
import androidx.compose.ui.unit.IntOffset
|
import androidx.compose.ui.unit.IntOffset
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.compose.ui.unit.sp
|
import androidx.compose.ui.unit.sp
|
||||||
import androidx.compose.ui.window.Popup
|
|
||||||
import androidx.compose.ui.window.PopupProperties
|
import androidx.compose.ui.window.PopupProperties
|
||||||
import android.graphics.BitmapFactory
|
|
||||||
import android.util.Base64
|
|
||||||
import android.util.Log
|
|
||||||
import com.rosetta.messenger.crypto.MessageCrypto
|
|
||||||
import com.rosetta.messenger.network.AttachmentType
|
import com.rosetta.messenger.network.AttachmentType
|
||||||
import com.rosetta.messenger.network.MessageAttachment
|
import com.rosetta.messenger.network.MessageAttachment
|
||||||
import com.rosetta.messenger.network.TransportManager
|
|
||||||
import com.rosetta.messenger.repository.AvatarRepository
|
import com.rosetta.messenger.repository.AvatarRepository
|
||||||
import com.rosetta.messenger.ui.components.AppleEmojiText
|
|
||||||
import com.rosetta.messenger.ui.onboarding.PrimaryBlue
|
|
||||||
import com.rosetta.messenger.ui.chats.models.*
|
import com.rosetta.messenger.ui.chats.models.*
|
||||||
import com.rosetta.messenger.ui.chats.utils.*
|
import com.rosetta.messenger.ui.chats.utils.*
|
||||||
|
import com.rosetta.messenger.ui.components.AppleEmojiText
|
||||||
|
import com.rosetta.messenger.ui.onboarding.PrimaryBlue
|
||||||
import com.rosetta.messenger.utils.AttachmentFileManager
|
import com.rosetta.messenger.utils.AttachmentFileManager
|
||||||
import com.vanniktech.blurhash.BlurHash
|
import com.vanniktech.blurhash.BlurHash
|
||||||
import kotlinx.coroutines.Dispatchers
|
import compose.icons.TablerIcons
|
||||||
import kotlinx.coroutines.delay
|
import compose.icons.tablericons.*
|
||||||
import kotlinx.coroutines.withContext
|
|
||||||
import java.text.SimpleDateFormat
|
import java.text.SimpleDateFormat
|
||||||
import java.util.*
|
import java.util.*
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.withContext
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Reusable UI components for Chat Detail Screen
|
* Reusable UI components for Chat Detail Screen Extracted from ChatDetailScreen.kt for better
|
||||||
* Extracted from ChatDetailScreen.kt for better organization
|
* organization
|
||||||
*/
|
*/
|
||||||
|
|
||||||
/** Date header with fade-in animation */
|
/** Date header with fade-in animation */
|
||||||
@Composable
|
@Composable
|
||||||
fun DateHeader(dateText: String, secondaryTextColor: Color) {
|
fun DateHeader(dateText: String, secondaryTextColor: Color) {
|
||||||
var isVisible by remember { mutableStateOf(false) }
|
var isVisible by remember { mutableStateOf(false) }
|
||||||
val alpha by animateFloatAsState(
|
val alpha by
|
||||||
|
animateFloatAsState(
|
||||||
targetValue = if (isVisible) 1f else 0f,
|
targetValue = if (isVisible) 1f else 0f,
|
||||||
animationSpec = tween(durationMillis = 250, easing = TelegramEasing),
|
animationSpec = tween(durationMillis = 250, easing = TelegramEasing),
|
||||||
label = "dateAlpha"
|
label = "dateAlpha"
|
||||||
@@ -77,10 +71,10 @@ fun DateHeader(dateText: String, secondaryTextColor: Color) {
|
|||||||
LaunchedEffect(dateText) { isVisible = true }
|
LaunchedEffect(dateText) { isVisible = true }
|
||||||
|
|
||||||
Row(
|
Row(
|
||||||
modifier = Modifier
|
modifier =
|
||||||
.fillMaxWidth()
|
Modifier.fillMaxWidth().padding(vertical = 12.dp).graphicsLayer {
|
||||||
.padding(vertical = 12.dp)
|
this.alpha = alpha
|
||||||
.graphicsLayer { this.alpha = alpha },
|
},
|
||||||
horizontalArrangement = Arrangement.Center
|
horizontalArrangement = Arrangement.Center
|
||||||
) {
|
) {
|
||||||
Text(
|
Text(
|
||||||
@@ -88,8 +82,8 @@ fun DateHeader(dateText: String, secondaryTextColor: Color) {
|
|||||||
fontSize = 13.sp,
|
fontSize = 13.sp,
|
||||||
fontWeight = FontWeight.Medium,
|
fontWeight = FontWeight.Medium,
|
||||||
color = secondaryTextColor,
|
color = secondaryTextColor,
|
||||||
modifier = Modifier
|
modifier =
|
||||||
.background(
|
Modifier.background(
|
||||||
color = secondaryTextColor.copy(alpha = 0.1f),
|
color = secondaryTextColor.copy(alpha = 0.1f),
|
||||||
shape = RoundedCornerShape(12.dp)
|
shape = RoundedCornerShape(12.dp)
|
||||||
)
|
)
|
||||||
@@ -111,11 +105,14 @@ fun TypingIndicator(isDarkTheme: Boolean) {
|
|||||||
Text(text = "typing", fontSize = 13.sp, color = typingColor)
|
Text(text = "typing", fontSize = 13.sp, color = typingColor)
|
||||||
|
|
||||||
repeat(3) { index ->
|
repeat(3) { index ->
|
||||||
val offsetY by infiniteTransition.animateFloat(
|
val offsetY by
|
||||||
|
infiniteTransition.animateFloat(
|
||||||
initialValue = 0f,
|
initialValue = 0f,
|
||||||
targetValue = -4f,
|
targetValue = -4f,
|
||||||
animationSpec = infiniteRepeatable(
|
animationSpec =
|
||||||
animation = tween(
|
infiniteRepeatable(
|
||||||
|
animation =
|
||||||
|
tween(
|
||||||
durationMillis = 600,
|
durationMillis = 600,
|
||||||
delayMillis = index * 100,
|
delayMillis = index * 100,
|
||||||
easing = FastOutSlowInEasing
|
easing = FastOutSlowInEasing
|
||||||
@@ -162,7 +159,8 @@ fun MessageBubble(
|
|||||||
val swipeThreshold = 80f
|
val swipeThreshold = 80f
|
||||||
val maxSwipe = 120f
|
val maxSwipe = 120f
|
||||||
|
|
||||||
val animatedOffset by animateFloatAsState(
|
val animatedOffset by
|
||||||
|
animateFloatAsState(
|
||||||
targetValue = swipeOffset,
|
targetValue = swipeOffset,
|
||||||
animationSpec = spring(dampingRatio = 0.6f, stiffness = 400f),
|
animationSpec = spring(dampingRatio = 0.6f, stiffness = 400f),
|
||||||
label = "swipeOffset"
|
label = "swipeOffset"
|
||||||
@@ -171,19 +169,22 @@ fun MessageBubble(
|
|||||||
val swipeProgress = (kotlin.math.abs(animatedOffset) / swipeThreshold).coerceIn(0f, 1f)
|
val swipeProgress = (kotlin.math.abs(animatedOffset) / swipeThreshold).coerceIn(0f, 1f)
|
||||||
|
|
||||||
// Selection animations
|
// Selection animations
|
||||||
val selectionScale by animateFloatAsState(
|
val selectionScale by
|
||||||
|
animateFloatAsState(
|
||||||
targetValue = if (isSelected) 0.95f else 1f,
|
targetValue = if (isSelected) 0.95f else 1f,
|
||||||
animationSpec = spring(dampingRatio = 0.6f, stiffness = 400f),
|
animationSpec = spring(dampingRatio = 0.6f, stiffness = 400f),
|
||||||
label = "selectionScale"
|
label = "selectionScale"
|
||||||
)
|
)
|
||||||
val selectionAlpha by animateFloatAsState(
|
val selectionAlpha by
|
||||||
|
animateFloatAsState(
|
||||||
targetValue = if (isSelected) 0.85f else 1f,
|
targetValue = if (isSelected) 0.85f else 1f,
|
||||||
animationSpec = tween(150),
|
animationSpec = tween(150),
|
||||||
label = "selectionAlpha"
|
label = "selectionAlpha"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Colors
|
// Colors
|
||||||
val bubbleColor = remember(message.isOutgoing, isDarkTheme) {
|
val bubbleColor =
|
||||||
|
remember(message.isOutgoing, isDarkTheme) {
|
||||||
if (message.isOutgoing) {
|
if (message.isOutgoing) {
|
||||||
PrimaryBlue
|
PrimaryBlue
|
||||||
} else {
|
} else {
|
||||||
@@ -191,31 +192,35 @@ fun MessageBubble(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
val textColor = remember(message.isOutgoing, isDarkTheme) {
|
val textColor =
|
||||||
|
remember(message.isOutgoing, isDarkTheme) {
|
||||||
if (message.isOutgoing) Color.White
|
if (message.isOutgoing) Color.White
|
||||||
else if (isDarkTheme) Color.White else Color(0xFF000000)
|
else if (isDarkTheme) Color.White else Color(0xFF000000)
|
||||||
}
|
}
|
||||||
|
|
||||||
val timeColor = remember(message.isOutgoing, isDarkTheme) {
|
val timeColor =
|
||||||
|
remember(message.isOutgoing, isDarkTheme) {
|
||||||
if (message.isOutgoing) Color.White.copy(alpha = 0.7f)
|
if (message.isOutgoing) Color.White.copy(alpha = 0.7f)
|
||||||
else if (isDarkTheme) Color(0xFF8E8E93) else Color(0xFF666666)
|
else if (isDarkTheme) Color(0xFF8E8E93) else Color(0xFF666666)
|
||||||
}
|
}
|
||||||
|
|
||||||
val bubbleShape = remember(message.isOutgoing, showTail) {
|
val bubbleShape =
|
||||||
|
remember(message.isOutgoing, showTail) {
|
||||||
RoundedCornerShape(
|
RoundedCornerShape(
|
||||||
topStart = 18.dp,
|
topStart = 18.dp,
|
||||||
topEnd = 18.dp,
|
topEnd = 18.dp,
|
||||||
bottomStart = if (message.isOutgoing) 18.dp else (if (showTail) 4.dp else 18.dp),
|
bottomStart =
|
||||||
bottomEnd = if (message.isOutgoing) (if (showTail) 4.dp else 18.dp) else 18.dp
|
if (message.isOutgoing) 18.dp else (if (showTail) 4.dp else 18.dp),
|
||||||
|
bottomEnd =
|
||||||
|
if (message.isOutgoing) (if (showTail) 4.dp else 18.dp) else 18.dp
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
val timeFormat = remember { SimpleDateFormat("HH:mm", Locale.getDefault()) }
|
val timeFormat = remember { SimpleDateFormat("HH:mm", Locale.getDefault()) }
|
||||||
|
|
||||||
Box(
|
Box(
|
||||||
modifier = Modifier
|
modifier =
|
||||||
.fillMaxWidth()
|
Modifier.fillMaxWidth().pointerInput(Unit) {
|
||||||
.pointerInput(Unit) {
|
|
||||||
detectHorizontalDragGestures(
|
detectHorizontalDragGestures(
|
||||||
onDragEnd = {
|
onDragEnd = {
|
||||||
if (swipeOffset <= -swipeThreshold) {
|
if (swipeOffset <= -swipeThreshold) {
|
||||||
@@ -233,18 +238,16 @@ fun MessageBubble(
|
|||||||
) {
|
) {
|
||||||
// Reply icon
|
// Reply icon
|
||||||
Box(
|
Box(
|
||||||
modifier = Modifier
|
modifier =
|
||||||
.align(Alignment.CenterEnd)
|
Modifier.align(Alignment.CenterEnd).padding(end = 16.dp).graphicsLayer {
|
||||||
.padding(end = 16.dp)
|
|
||||||
.graphicsLayer {
|
|
||||||
alpha = swipeProgress
|
alpha = swipeProgress
|
||||||
scaleX = swipeProgress
|
scaleX = swipeProgress
|
||||||
scaleY = swipeProgress
|
scaleY = swipeProgress
|
||||||
}
|
}
|
||||||
) {
|
) {
|
||||||
Box(
|
Box(
|
||||||
modifier = Modifier
|
modifier =
|
||||||
.size(36.dp)
|
Modifier.size(36.dp)
|
||||||
.clip(CircleShape)
|
.clip(CircleShape)
|
||||||
.background(
|
.background(
|
||||||
if (swipeProgress >= 1f) PrimaryBlue
|
if (swipeProgress >= 1f) PrimaryBlue
|
||||||
@@ -256,7 +259,8 @@ fun MessageBubble(
|
|||||||
Icon(
|
Icon(
|
||||||
TablerIcons.CornerUpLeft,
|
TablerIcons.CornerUpLeft,
|
||||||
contentDescription = "Reply",
|
contentDescription = "Reply",
|
||||||
tint = if (swipeProgress >= 1f) Color.White
|
tint =
|
||||||
|
if (swipeProgress >= 1f) Color.White
|
||||||
else if (isDarkTheme) Color.White.copy(alpha = 0.7f)
|
else if (isDarkTheme) Color.White.copy(alpha = 0.7f)
|
||||||
else Color(0xFF666666),
|
else Color(0xFF666666),
|
||||||
modifier = Modifier.size(20.dp)
|
modifier = Modifier.size(20.dp)
|
||||||
@@ -264,23 +268,30 @@ fun MessageBubble(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
val selectionBackgroundColor by animateColorAsState(
|
val selectionBackgroundColor by
|
||||||
targetValue = if (isSelected) PrimaryBlue.copy(alpha = 0.15f) else Color.Transparent,
|
animateColorAsState(
|
||||||
|
targetValue =
|
||||||
|
if (isSelected) PrimaryBlue.copy(alpha = 0.15f)
|
||||||
|
else Color.Transparent,
|
||||||
animationSpec = tween(200),
|
animationSpec = tween(200),
|
||||||
label = "selectionBg"
|
label = "selectionBg"
|
||||||
)
|
)
|
||||||
|
|
||||||
val highlightBackgroundColor by animateColorAsState(
|
val highlightBackgroundColor by
|
||||||
targetValue = if (isHighlighted) PrimaryBlue.copy(alpha = 0.12f) else Color.Transparent,
|
animateColorAsState(
|
||||||
|
targetValue =
|
||||||
|
if (isHighlighted) PrimaryBlue.copy(alpha = 0.12f)
|
||||||
|
else Color.Transparent,
|
||||||
animationSpec = tween(300),
|
animationSpec = tween(300),
|
||||||
label = "highlightBg"
|
label = "highlightBg"
|
||||||
)
|
)
|
||||||
|
|
||||||
val combinedBackgroundColor = if (isSelected) selectionBackgroundColor else highlightBackgroundColor
|
val combinedBackgroundColor =
|
||||||
|
if (isSelected) selectionBackgroundColor else highlightBackgroundColor
|
||||||
|
|
||||||
Row(
|
Row(
|
||||||
modifier = Modifier
|
modifier =
|
||||||
.fillMaxWidth()
|
Modifier.fillMaxWidth()
|
||||||
.background(combinedBackgroundColor)
|
.background(combinedBackgroundColor)
|
||||||
.padding(vertical = 2.dp)
|
.padding(vertical = 2.dp)
|
||||||
.offset { IntOffset(animatedOffset.toInt(), 0) },
|
.offset { IntOffset(animatedOffset.toInt(), 0) },
|
||||||
@@ -290,15 +301,17 @@ fun MessageBubble(
|
|||||||
// Selection checkmark
|
// Selection checkmark
|
||||||
AnimatedVisibility(
|
AnimatedVisibility(
|
||||||
visible = isSelected,
|
visible = isSelected,
|
||||||
enter = fadeIn(tween(150)) + scaleIn(
|
enter =
|
||||||
|
fadeIn(tween(150)) +
|
||||||
|
scaleIn(
|
||||||
initialScale = 0.3f,
|
initialScale = 0.3f,
|
||||||
animationSpec = spring(dampingRatio = 0.6f)
|
animationSpec = spring(dampingRatio = 0.6f)
|
||||||
),
|
),
|
||||||
exit = fadeOut(tween(100)) + scaleOut(targetScale = 0.3f)
|
exit = fadeOut(tween(100)) + scaleOut(targetScale = 0.3f)
|
||||||
) {
|
) {
|
||||||
Box(
|
Box(
|
||||||
modifier = Modifier
|
modifier =
|
||||||
.padding(start = 12.dp, end = 4.dp)
|
Modifier.padding(start = 12.dp, end = 4.dp)
|
||||||
.size(24.dp)
|
.size(24.dp)
|
||||||
.clip(CircleShape)
|
.clip(CircleShape)
|
||||||
.background(Color(0xFF4CD964)),
|
.background(Color(0xFF4CD964)),
|
||||||
@@ -317,29 +330,34 @@ fun MessageBubble(
|
|||||||
visible = !isSelected,
|
visible = !isSelected,
|
||||||
enter = fadeIn(tween(100)),
|
enter = fadeIn(tween(100)),
|
||||||
exit = fadeOut(tween(100))
|
exit = fadeOut(tween(100))
|
||||||
) {
|
) { Spacer(modifier = Modifier.width(12.dp)) }
|
||||||
Spacer(modifier = Modifier.width(12.dp))
|
|
||||||
}
|
|
||||||
|
|
||||||
if (message.isOutgoing) {
|
if (message.isOutgoing) {
|
||||||
Spacer(modifier = Modifier.weight(1f))
|
Spacer(modifier = Modifier.weight(1f))
|
||||||
}
|
}
|
||||||
|
|
||||||
// Проверяем - есть ли только фотки без текста
|
// Проверяем - есть ли только фотки без текста
|
||||||
val hasOnlyMedia = message.attachments.isNotEmpty() &&
|
val hasOnlyMedia =
|
||||||
|
message.attachments.isNotEmpty() &&
|
||||||
message.text.isEmpty() &&
|
message.text.isEmpty() &&
|
||||||
message.replyData == null &&
|
message.replyData == null &&
|
||||||
message.attachments.all { it.type == com.rosetta.messenger.network.AttachmentType.IMAGE }
|
message.attachments.all {
|
||||||
|
it.type == com.rosetta.messenger.network.AttachmentType.IMAGE
|
||||||
|
}
|
||||||
|
|
||||||
// Фото + caption (как в Telegram)
|
// Фото + caption (как в Telegram)
|
||||||
val hasImageWithCaption = message.attachments.isNotEmpty() &&
|
val hasImageWithCaption =
|
||||||
|
message.attachments.isNotEmpty() &&
|
||||||
message.text.isNotEmpty() &&
|
message.text.isNotEmpty() &&
|
||||||
message.replyData == null &&
|
message.replyData == null &&
|
||||||
message.attachments.all { it.type == com.rosetta.messenger.network.AttachmentType.IMAGE }
|
message.attachments.all {
|
||||||
|
it.type == com.rosetta.messenger.network.AttachmentType.IMAGE
|
||||||
|
}
|
||||||
|
|
||||||
// Для сообщений только с фото - минимальный padding и тонкий border
|
// Для сообщений только с фото - минимальный padding и тонкий border
|
||||||
// Для фото + caption - padding только внизу для текста
|
// Для фото + caption - padding только внизу для текста
|
||||||
val bubblePadding = when {
|
val bubblePadding =
|
||||||
|
when {
|
||||||
hasOnlyMedia -> PaddingValues(0.dp)
|
hasOnlyMedia -> PaddingValues(0.dp)
|
||||||
hasImageWithCaption -> PaddingValues(0.dp)
|
hasImageWithCaption -> PaddingValues(0.dp)
|
||||||
else -> PaddingValues(horizontal = 10.dp, vertical = 8.dp)
|
else -> PaddingValues(horizontal = 10.dp, vertical = 8.dp)
|
||||||
@@ -347,8 +365,8 @@ fun MessageBubble(
|
|||||||
val bubbleBorderWidth = if (hasOnlyMedia) 1.dp else 0.dp
|
val bubbleBorderWidth = if (hasOnlyMedia) 1.dp else 0.dp
|
||||||
|
|
||||||
Box(
|
Box(
|
||||||
modifier = Modifier
|
modifier =
|
||||||
.padding(end = 12.dp)
|
Modifier.padding(end = 12.dp)
|
||||||
.widthIn(min = 60.dp, max = 280.dp)
|
.widthIn(min = 60.dp, max = 280.dp)
|
||||||
.wrapContentWidth(unbounded = false)
|
.wrapContentWidth(unbounded = false)
|
||||||
.graphicsLayer {
|
.graphicsLayer {
|
||||||
@@ -358,7 +376,8 @@ fun MessageBubble(
|
|||||||
}
|
}
|
||||||
.combinedClickable(
|
.combinedClickable(
|
||||||
indication = null,
|
indication = null,
|
||||||
interactionSource = remember { MutableInteractionSource() },
|
interactionSource =
|
||||||
|
remember { MutableInteractionSource() },
|
||||||
onClick = onClick,
|
onClick = onClick,
|
||||||
onLongClick = onLongClick
|
onLongClick = onLongClick
|
||||||
)
|
)
|
||||||
@@ -367,10 +386,18 @@ fun MessageBubble(
|
|||||||
if (hasOnlyMedia) {
|
if (hasOnlyMedia) {
|
||||||
Modifier.border(
|
Modifier.border(
|
||||||
width = bubbleBorderWidth,
|
width = bubbleBorderWidth,
|
||||||
color = if (message.isOutgoing) {
|
color =
|
||||||
|
if (message.isOutgoing) {
|
||||||
Color.White.copy(alpha = 0.15f)
|
Color.White.copy(alpha = 0.15f)
|
||||||
} else {
|
} else {
|
||||||
if (isDarkTheme) Color.White.copy(alpha = 0.1f) else Color.Black.copy(alpha = 0.08f)
|
if (isDarkTheme)
|
||||||
|
Color.White.copy(
|
||||||
|
alpha = 0.1f
|
||||||
|
)
|
||||||
|
else
|
||||||
|
Color.Black.copy(
|
||||||
|
alpha = 0.08f
|
||||||
|
)
|
||||||
},
|
},
|
||||||
shape = bubbleShape
|
shape = bubbleShape
|
||||||
)
|
)
|
||||||
@@ -393,6 +420,8 @@ fun MessageBubble(
|
|||||||
|
|
||||||
// 📎 Attachments (IMAGE, FILE, AVATAR)
|
// 📎 Attachments (IMAGE, FILE, AVATAR)
|
||||||
if (message.attachments.isNotEmpty()) {
|
if (message.attachments.isNotEmpty()) {
|
||||||
|
val attachmentDisplayStatus =
|
||||||
|
if (isSavedMessages) MessageStatus.READ else message.status
|
||||||
MessageAttachments(
|
MessageAttachments(
|
||||||
attachments = message.attachments,
|
attachments = message.attachments,
|
||||||
chachaKey = message.chachaKey,
|
chachaKey = message.chachaKey,
|
||||||
@@ -401,10 +430,11 @@ fun MessageBubble(
|
|||||||
isDarkTheme = isDarkTheme,
|
isDarkTheme = isDarkTheme,
|
||||||
senderPublicKey = senderPublicKey,
|
senderPublicKey = senderPublicKey,
|
||||||
timestamp = message.timestamp,
|
timestamp = message.timestamp,
|
||||||
messageStatus = message.status,
|
messageStatus = attachmentDisplayStatus,
|
||||||
avatarRepository = avatarRepository,
|
avatarRepository = avatarRepository,
|
||||||
currentUserPublicKey = currentUserPublicKey,
|
currentUserPublicKey = currentUserPublicKey,
|
||||||
hasCaption = hasImageWithCaption, // Если есть caption - время на пузырьке, не на фото
|
hasCaption = hasImageWithCaption, // Если есть caption - время на
|
||||||
|
// пузырьке, не на фото
|
||||||
onImageClick = onImageClick
|
onImageClick = onImageClick
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -412,10 +442,13 @@ fun MessageBubble(
|
|||||||
// 🖼️ Caption под фото (как в Telegram)
|
// 🖼️ Caption под фото (как в Telegram)
|
||||||
if (hasImageWithCaption) {
|
if (hasImageWithCaption) {
|
||||||
Column(
|
Column(
|
||||||
modifier = Modifier
|
modifier =
|
||||||
.fillMaxWidth()
|
Modifier.fillMaxWidth()
|
||||||
.background(bubbleColor)
|
.background(bubbleColor)
|
||||||
.padding(horizontal = 10.dp, vertical = 6.dp) // Уменьшил padding
|
.padding(
|
||||||
|
horizontal = 10.dp,
|
||||||
|
vertical = 6.dp
|
||||||
|
) // Уменьшил padding
|
||||||
) {
|
) {
|
||||||
Row(
|
Row(
|
||||||
verticalAlignment = Alignment.Bottom,
|
verticalAlignment = Alignment.Bottom,
|
||||||
@@ -439,10 +472,13 @@ fun MessageBubble(
|
|||||||
text = timeFormat.format(message.timestamp),
|
text = timeFormat.format(message.timestamp),
|
||||||
color = timeColor,
|
color = timeColor,
|
||||||
fontSize = 11.sp,
|
fontSize = 11.sp,
|
||||||
fontStyle = androidx.compose.ui.text.font.FontStyle.Italic
|
fontStyle =
|
||||||
|
androidx.compose.ui.text.font.FontStyle.Italic
|
||||||
)
|
)
|
||||||
if (message.isOutgoing) {
|
if (message.isOutgoing) {
|
||||||
val displayStatus = if (isSavedMessages) MessageStatus.READ else message.status
|
val displayStatus =
|
||||||
|
if (isSavedMessages) MessageStatus.READ
|
||||||
|
else message.status
|
||||||
AnimatedMessageStatus(
|
AnimatedMessageStatus(
|
||||||
status = displayStatus,
|
status = displayStatus,
|
||||||
timeColor = timeColor,
|
timeColor = timeColor,
|
||||||
@@ -486,7 +522,9 @@ fun MessageBubble(
|
|||||||
fontStyle = androidx.compose.ui.text.font.FontStyle.Italic
|
fontStyle = androidx.compose.ui.text.font.FontStyle.Italic
|
||||||
)
|
)
|
||||||
if (message.isOutgoing) {
|
if (message.isOutgoing) {
|
||||||
val displayStatus = if (isSavedMessages) MessageStatus.READ else message.status
|
val displayStatus =
|
||||||
|
if (isSavedMessages) MessageStatus.READ
|
||||||
|
else message.status
|
||||||
AnimatedMessageStatus(
|
AnimatedMessageStatus(
|
||||||
status = displayStatus,
|
status = displayStatus,
|
||||||
timeColor = timeColor,
|
timeColor = timeColor,
|
||||||
@@ -523,7 +561,9 @@ fun MessageBubble(
|
|||||||
fontStyle = androidx.compose.ui.text.font.FontStyle.Italic
|
fontStyle = androidx.compose.ui.text.font.FontStyle.Italic
|
||||||
)
|
)
|
||||||
if (message.isOutgoing) {
|
if (message.isOutgoing) {
|
||||||
val displayStatus = if (isSavedMessages) MessageStatus.READ else message.status
|
val displayStatus =
|
||||||
|
if (isSavedMessages) MessageStatus.READ
|
||||||
|
else message.status
|
||||||
AnimatedMessageStatus(
|
AnimatedMessageStatus(
|
||||||
status = displayStatus,
|
status = displayStatus,
|
||||||
timeColor = timeColor,
|
timeColor = timeColor,
|
||||||
@@ -550,18 +590,19 @@ fun AnimatedMessageStatus(
|
|||||||
onRetry: () -> Unit = {},
|
onRetry: () -> Unit = {},
|
||||||
onDelete: () -> Unit = {}
|
onDelete: () -> Unit = {}
|
||||||
) {
|
) {
|
||||||
val isTimedOut = status == MessageStatus.SENDING &&
|
val isTimedOut =
|
||||||
timestamp > 0 &&
|
status == MessageStatus.SENDING && timestamp > 0 && !isMessageDeliveredByTime(timestamp)
|
||||||
!isMessageDeliveredByTime(timestamp)
|
|
||||||
val effectiveStatus = if (isTimedOut) MessageStatus.ERROR else status
|
val effectiveStatus = if (isTimedOut) MessageStatus.ERROR else status
|
||||||
|
|
||||||
val targetColor = when (effectiveStatus) {
|
val targetColor =
|
||||||
|
when (effectiveStatus) {
|
||||||
MessageStatus.READ -> Color(0xFF4FC3F7)
|
MessageStatus.READ -> Color(0xFF4FC3F7)
|
||||||
MessageStatus.ERROR -> Color(0xFFE53935)
|
MessageStatus.ERROR -> Color(0xFFE53935)
|
||||||
else -> timeColor
|
else -> timeColor
|
||||||
}
|
}
|
||||||
|
|
||||||
val animatedColor by animateColorAsState(
|
val animatedColor by
|
||||||
|
animateColorAsState(
|
||||||
targetValue = targetColor,
|
targetValue = targetColor,
|
||||||
animationSpec = tween(durationMillis = 300, easing = FastOutSlowInEasing),
|
animationSpec = tween(durationMillis = 300, easing = FastOutSlowInEasing),
|
||||||
label = "statusColor"
|
label = "statusColor"
|
||||||
@@ -577,9 +618,11 @@ fun AnimatedMessageStatus(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
val scale by animateFloatAsState(
|
val scale by
|
||||||
|
animateFloatAsState(
|
||||||
targetValue = if (shouldAnimate) 1.2f else 1f,
|
targetValue = if (shouldAnimate) 1.2f else 1f,
|
||||||
animationSpec = spring(
|
animationSpec =
|
||||||
|
spring(
|
||||||
dampingRatio = Spring.DampingRatioMediumBouncy,
|
dampingRatio = Spring.DampingRatioMediumBouncy,
|
||||||
stiffness = Spring.StiffnessLow
|
stiffness = Spring.StiffnessLow
|
||||||
),
|
),
|
||||||
@@ -598,7 +641,8 @@ fun AnimatedMessageStatus(
|
|||||||
val iconSize = with(LocalDensity.current) { 14.sp.toDp() }
|
val iconSize = with(LocalDensity.current) { 14.sp.toDp() }
|
||||||
|
|
||||||
Icon(
|
Icon(
|
||||||
imageVector = when (currentStatus) {
|
imageVector =
|
||||||
|
when (currentStatus) {
|
||||||
MessageStatus.SENDING -> TablerIcons.Clock
|
MessageStatus.SENDING -> TablerIcons.Clock
|
||||||
MessageStatus.SENT -> TablerIcons.Check
|
MessageStatus.SENT -> TablerIcons.Check
|
||||||
MessageStatus.DELIVERED -> TablerIcons.Check
|
MessageStatus.DELIVERED -> TablerIcons.Check
|
||||||
@@ -607,8 +651,8 @@ fun AnimatedMessageStatus(
|
|||||||
},
|
},
|
||||||
contentDescription = null,
|
contentDescription = null,
|
||||||
tint = animatedColor,
|
tint = animatedColor,
|
||||||
modifier = Modifier
|
modifier =
|
||||||
.size(iconSize)
|
Modifier.size(iconSize)
|
||||||
.scale(scale)
|
.scale(scale)
|
||||||
.then(
|
.then(
|
||||||
if (currentStatus == MessageStatus.ERROR) {
|
if (currentStatus == MessageStatus.ERROR) {
|
||||||
@@ -618,10 +662,7 @@ fun AnimatedMessageStatus(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
DropdownMenu(
|
DropdownMenu(expanded = showErrorMenu, onDismissRequest = { showErrorMenu = false }) {
|
||||||
expanded = showErrorMenu,
|
|
||||||
onDismissRequest = { showErrorMenu = false }
|
|
||||||
) {
|
|
||||||
DropdownMenuItem(
|
DropdownMenuItem(
|
||||||
text = { Text("Retry") },
|
text = { Text("Retry") },
|
||||||
onClick = {
|
onClick = {
|
||||||
@@ -664,7 +705,8 @@ fun ReplyBubble(
|
|||||||
onClick: () -> Unit = {}
|
onClick: () -> Unit = {}
|
||||||
) {
|
) {
|
||||||
val context = androidx.compose.ui.platform.LocalContext.current
|
val context = androidx.compose.ui.platform.LocalContext.current
|
||||||
val backgroundColor = if (isOutgoing) {
|
val backgroundColor =
|
||||||
|
if (isOutgoing) {
|
||||||
Color.Black.copy(alpha = 0.15f)
|
Color.Black.copy(alpha = 0.15f)
|
||||||
} else {
|
} else {
|
||||||
Color.Black.copy(alpha = 0.08f)
|
Color.Black.copy(alpha = 0.08f)
|
||||||
@@ -672,7 +714,8 @@ fun ReplyBubble(
|
|||||||
|
|
||||||
val borderColor = if (isOutgoing) Color.White else PrimaryBlue
|
val borderColor = if (isOutgoing) Color.White else PrimaryBlue
|
||||||
val nameColor = if (isOutgoing) Color.White else PrimaryBlue
|
val nameColor = if (isOutgoing) Color.White else PrimaryBlue
|
||||||
val replyTextColor = if (isOutgoing) {
|
val replyTextColor =
|
||||||
|
if (isOutgoing) {
|
||||||
Color.White.copy(alpha = 0.85f)
|
Color.White.copy(alpha = 0.85f)
|
||||||
} else {
|
} else {
|
||||||
if (isDarkTheme) Color.White else Color.Black
|
if (isDarkTheme) Color.White else Color.Black
|
||||||
@@ -693,9 +736,12 @@ fun ReplyBubble(
|
|||||||
withContext(Dispatchers.IO) {
|
withContext(Dispatchers.IO) {
|
||||||
try {
|
try {
|
||||||
// Получаем blurhash из preview (может быть в формате "tag::blurhash")
|
// Получаем blurhash из preview (может быть в формате "tag::blurhash")
|
||||||
val blurhash = if (imageAttachment.preview.contains("::")) {
|
val blurhash =
|
||||||
|
if (imageAttachment.preview.contains("::")) {
|
||||||
imageAttachment.preview.substringAfter("::")
|
imageAttachment.preview.substringAfter("::")
|
||||||
} else if (!imageAttachment.preview.startsWith("http") && imageAttachment.preview.length < 50) {
|
} else if (!imageAttachment.preview.startsWith("http") &&
|
||||||
|
imageAttachment.preview.length < 50
|
||||||
|
) {
|
||||||
imageAttachment.preview
|
imageAttachment.preview
|
||||||
} else {
|
} else {
|
||||||
""
|
""
|
||||||
@@ -717,14 +763,20 @@ fun ReplyBubble(
|
|||||||
try {
|
try {
|
||||||
// Пробуем сначала из blob
|
// Пробуем сначала из blob
|
||||||
if (imageAttachment.blob.isNotEmpty()) {
|
if (imageAttachment.blob.isNotEmpty()) {
|
||||||
val decoded = try {
|
val decoded =
|
||||||
val cleanBase64 = if (imageAttachment.blob.contains(",")) {
|
try {
|
||||||
|
val cleanBase64 =
|
||||||
|
if (imageAttachment.blob.contains(",")) {
|
||||||
imageAttachment.blob.substringAfter(",")
|
imageAttachment.blob.substringAfter(",")
|
||||||
} else {
|
} else {
|
||||||
imageAttachment.blob
|
imageAttachment.blob
|
||||||
}
|
}
|
||||||
val decodedBytes = Base64.decode(cleanBase64, Base64.DEFAULT)
|
val decodedBytes = Base64.decode(cleanBase64, Base64.DEFAULT)
|
||||||
BitmapFactory.decodeByteArray(decodedBytes, 0, decodedBytes.size)
|
BitmapFactory.decodeByteArray(
|
||||||
|
decodedBytes,
|
||||||
|
0,
|
||||||
|
decodedBytes.size
|
||||||
|
)
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
null
|
null
|
||||||
}
|
}
|
||||||
@@ -735,7 +787,8 @@ fun ReplyBubble(
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Если blob нет - загружаем из локального файла
|
// Если blob нет - загружаем из локального файла
|
||||||
val localBlob = AttachmentFileManager.readAttachment(
|
val localBlob =
|
||||||
|
AttachmentFileManager.readAttachment(
|
||||||
context,
|
context,
|
||||||
imageAttachment.id,
|
imageAttachment.id,
|
||||||
replyData.senderPublicKey,
|
replyData.senderPublicKey,
|
||||||
@@ -743,14 +796,20 @@ fun ReplyBubble(
|
|||||||
)
|
)
|
||||||
|
|
||||||
if (localBlob != null) {
|
if (localBlob != null) {
|
||||||
val decoded = try {
|
val decoded =
|
||||||
val cleanBase64 = if (localBlob.contains(",")) {
|
try {
|
||||||
|
val cleanBase64 =
|
||||||
|
if (localBlob.contains(",")) {
|
||||||
localBlob.substringAfter(",")
|
localBlob.substringAfter(",")
|
||||||
} else {
|
} else {
|
||||||
localBlob
|
localBlob
|
||||||
}
|
}
|
||||||
val decodedBytes = Base64.decode(cleanBase64, Base64.DEFAULT)
|
val decodedBytes = Base64.decode(cleanBase64, Base64.DEFAULT)
|
||||||
BitmapFactory.decodeByteArray(decodedBytes, 0, decodedBytes.size)
|
BitmapFactory.decodeByteArray(
|
||||||
|
decodedBytes,
|
||||||
|
0,
|
||||||
|
decodedBytes.size
|
||||||
|
)
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
null
|
null
|
||||||
}
|
}
|
||||||
@@ -764,8 +823,8 @@ fun ReplyBubble(
|
|||||||
}
|
}
|
||||||
|
|
||||||
Row(
|
Row(
|
||||||
modifier = Modifier
|
modifier =
|
||||||
.fillMaxWidth()
|
Modifier.fillMaxWidth()
|
||||||
.height(IntrinsicSize.Min)
|
.height(IntrinsicSize.Min)
|
||||||
.clip(RoundedCornerShape(4.dp))
|
.clip(RoundedCornerShape(4.dp))
|
||||||
.clickable(onClick = onClick)
|
.clickable(onClick = onClick)
|
||||||
@@ -776,17 +835,22 @@ fun ReplyBubble(
|
|||||||
|
|
||||||
// Контент reply
|
// Контент reply
|
||||||
Row(
|
Row(
|
||||||
modifier = Modifier
|
modifier =
|
||||||
.weight(1f)
|
Modifier.weight(1f)
|
||||||
.padding(start = 8.dp, end = if (hasImage) 4.dp else 10.dp, top = 4.dp, bottom = 4.dp),
|
.padding(
|
||||||
|
start = 8.dp,
|
||||||
|
end = if (hasImage) 4.dp else 10.dp,
|
||||||
|
top = 4.dp,
|
||||||
|
bottom = 4.dp
|
||||||
|
),
|
||||||
verticalAlignment = Alignment.CenterVertically
|
verticalAlignment = Alignment.CenterVertically
|
||||||
) {
|
) {
|
||||||
// Текстовая часть
|
// Текстовая часть
|
||||||
Column(
|
Column(modifier = Modifier.weight(1f)) {
|
||||||
modifier = Modifier.weight(1f)
|
|
||||||
) {
|
|
||||||
Text(
|
Text(
|
||||||
text = if (replyData.isForwarded) "Forwarded message" else replyData.senderName,
|
text =
|
||||||
|
if (replyData.isForwarded) "Forwarded message"
|
||||||
|
else replyData.senderName,
|
||||||
color = nameColor,
|
color = nameColor,
|
||||||
fontSize = 14.sp,
|
fontSize = 14.sp,
|
||||||
fontWeight = FontWeight.Medium,
|
fontWeight = FontWeight.Medium,
|
||||||
@@ -795,7 +859,8 @@ fun ReplyBubble(
|
|||||||
)
|
)
|
||||||
|
|
||||||
// Текст или "Photo"
|
// Текст или "Photo"
|
||||||
val displayText = when {
|
val displayText =
|
||||||
|
when {
|
||||||
replyData.text.isNotEmpty() -> replyData.text
|
replyData.text.isNotEmpty() -> replyData.text
|
||||||
hasImage -> "Photo"
|
hasImage -> "Photo"
|
||||||
replyData.attachments.any { it.type == AttachmentType.FILE } -> "File"
|
replyData.attachments.any { it.type == AttachmentType.FILE } -> "File"
|
||||||
@@ -815,8 +880,8 @@ fun ReplyBubble(
|
|||||||
if (hasImage) {
|
if (hasImage) {
|
||||||
Spacer(modifier = Modifier.width(8.dp))
|
Spacer(modifier = Modifier.width(8.dp))
|
||||||
Box(
|
Box(
|
||||||
modifier = Modifier
|
modifier =
|
||||||
.size(36.dp)
|
Modifier.size(36.dp)
|
||||||
.clip(RoundedCornerShape(4.dp))
|
.clip(RoundedCornerShape(4.dp))
|
||||||
.background(Color.Gray.copy(alpha = 0.3f))
|
.background(Color.Gray.copy(alpha = 0.3f))
|
||||||
) {
|
) {
|
||||||
@@ -861,10 +926,12 @@ fun MessageSkeletonList(isDarkTheme: Boolean, modifier: Modifier = Modifier) {
|
|||||||
val skeletonColor = if (isDarkTheme) Color(0xFF3A3A3C) else Color(0xFFE0E0E0)
|
val skeletonColor = if (isDarkTheme) Color(0xFF3A3A3C) else Color(0xFFE0E0E0)
|
||||||
|
|
||||||
val infiniteTransition = rememberInfiniteTransition(label = "shimmer")
|
val infiniteTransition = rememberInfiniteTransition(label = "shimmer")
|
||||||
val shimmerAlpha by infiniteTransition.animateFloat(
|
val shimmerAlpha by
|
||||||
|
infiniteTransition.animateFloat(
|
||||||
initialValue = 0.4f,
|
initialValue = 0.4f,
|
||||||
targetValue = 0.8f,
|
targetValue = 0.8f,
|
||||||
animationSpec = infiniteRepeatable(
|
animationSpec =
|
||||||
|
infiniteRepeatable(
|
||||||
animation = tween(800, easing = FastOutSlowInEasing),
|
animation = tween(800, easing = FastOutSlowInEasing),
|
||||||
repeatMode = RepeatMode.Reverse
|
repeatMode = RepeatMode.Reverse
|
||||||
),
|
),
|
||||||
@@ -873,8 +940,8 @@ fun MessageSkeletonList(isDarkTheme: Boolean, modifier: Modifier = Modifier) {
|
|||||||
|
|
||||||
Box(modifier = modifier) {
|
Box(modifier = modifier) {
|
||||||
Column(
|
Column(
|
||||||
modifier = Modifier
|
modifier =
|
||||||
.align(Alignment.BottomCenter)
|
Modifier.align(Alignment.BottomCenter)
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.padding(horizontal = 8.dp)
|
.padding(horizontal = 8.dp)
|
||||||
.padding(bottom = 80.dp),
|
.padding(bottom = 80.dp),
|
||||||
@@ -902,8 +969,8 @@ private fun SkeletonBubble(
|
|||||||
horizontalArrangement = if (isOutgoing) Arrangement.End else Arrangement.Start
|
horizontalArrangement = if (isOutgoing) Arrangement.End else Arrangement.Start
|
||||||
) {
|
) {
|
||||||
Box(
|
Box(
|
||||||
modifier = Modifier
|
modifier =
|
||||||
.fillMaxWidth(widthFraction)
|
Modifier.fillMaxWidth(widthFraction)
|
||||||
.defaultMinSize(minHeight = 44.dp)
|
.defaultMinSize(minHeight = 44.dp)
|
||||||
.clip(
|
.clip(
|
||||||
RoundedCornerShape(
|
RoundedCornerShape(
|
||||||
@@ -931,13 +998,15 @@ fun KebabMenu(
|
|||||||
onUnblockClick: () -> Unit,
|
onUnblockClick: () -> Unit,
|
||||||
onDeleteClick: () -> Unit
|
onDeleteClick: () -> Unit
|
||||||
) {
|
) {
|
||||||
val dividerColor = if (isDarkTheme) Color.White.copy(alpha = 0.1f) else Color.Black.copy(alpha = 0.08f)
|
val dividerColor =
|
||||||
|
if (isDarkTheme) Color.White.copy(alpha = 0.1f) else Color.Black.copy(alpha = 0.08f)
|
||||||
|
|
||||||
DropdownMenu(
|
DropdownMenu(
|
||||||
expanded = expanded,
|
expanded = expanded,
|
||||||
onDismissRequest = onDismiss,
|
onDismissRequest = onDismiss,
|
||||||
modifier = Modifier.width(220.dp),
|
modifier = Modifier.width(220.dp),
|
||||||
properties = PopupProperties(
|
properties =
|
||||||
|
PopupProperties(
|
||||||
focusable = true,
|
focusable = true,
|
||||||
dismissOnBackPress = true,
|
dismissOnBackPress = true,
|
||||||
dismissOnClickOutside = true
|
dismissOnClickOutside = true
|
||||||
@@ -953,8 +1022,8 @@ fun KebabMenu(
|
|||||||
)
|
)
|
||||||
|
|
||||||
Box(
|
Box(
|
||||||
modifier = Modifier
|
modifier =
|
||||||
.fillMaxWidth()
|
Modifier.fillMaxWidth()
|
||||||
.padding(horizontal = 16.dp, vertical = 4.dp)
|
.padding(horizontal = 16.dp, vertical = 4.dp)
|
||||||
.height(0.5.dp)
|
.height(0.5.dp)
|
||||||
.background(dividerColor)
|
.background(dividerColor)
|
||||||
@@ -1027,7 +1096,8 @@ fun ReplyImagePreview(
|
|||||||
withContext(Dispatchers.IO) {
|
withContext(Dispatchers.IO) {
|
||||||
try {
|
try {
|
||||||
// Получаем blurhash из preview (может быть в формате "tag::blurhash")
|
// Получаем blurhash из preview (может быть в формате "tag::blurhash")
|
||||||
val blurhash = if (attachment.preview.contains("::")) {
|
val blurhash =
|
||||||
|
if (attachment.preview.contains("::")) {
|
||||||
attachment.preview.split("::").lastOrNull() ?: ""
|
attachment.preview.split("::").lastOrNull() ?: ""
|
||||||
} else {
|
} else {
|
||||||
attachment.preview
|
attachment.preview
|
||||||
@@ -1049,14 +1119,20 @@ fun ReplyImagePreview(
|
|||||||
try {
|
try {
|
||||||
// Пробуем сначала из blob
|
// Пробуем сначала из blob
|
||||||
if (attachment.blob.isNotEmpty()) {
|
if (attachment.blob.isNotEmpty()) {
|
||||||
val decoded = try {
|
val decoded =
|
||||||
val cleanBase64 = if (attachment.blob.contains(",")) {
|
try {
|
||||||
|
val cleanBase64 =
|
||||||
|
if (attachment.blob.contains(",")) {
|
||||||
attachment.blob.substringAfter(",")
|
attachment.blob.substringAfter(",")
|
||||||
} else {
|
} else {
|
||||||
attachment.blob
|
attachment.blob
|
||||||
}
|
}
|
||||||
val decodedBytes = Base64.decode(cleanBase64, Base64.DEFAULT)
|
val decodedBytes = Base64.decode(cleanBase64, Base64.DEFAULT)
|
||||||
BitmapFactory.decodeByteArray(decodedBytes, 0, decodedBytes.size)
|
BitmapFactory.decodeByteArray(
|
||||||
|
decodedBytes,
|
||||||
|
0,
|
||||||
|
decodedBytes.size
|
||||||
|
)
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
null
|
null
|
||||||
}
|
}
|
||||||
@@ -1067,7 +1143,8 @@ fun ReplyImagePreview(
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Если blob нет - загружаем из локального файла
|
// Если blob нет - загружаем из локального файла
|
||||||
val localBlob = AttachmentFileManager.readAttachment(
|
val localBlob =
|
||||||
|
AttachmentFileManager.readAttachment(
|
||||||
context,
|
context,
|
||||||
attachment.id,
|
attachment.id,
|
||||||
senderPublicKey,
|
senderPublicKey,
|
||||||
@@ -1075,14 +1152,20 @@ fun ReplyImagePreview(
|
|||||||
)
|
)
|
||||||
|
|
||||||
if (localBlob != null) {
|
if (localBlob != null) {
|
||||||
val decoded = try {
|
val decoded =
|
||||||
val cleanBase64 = if (localBlob.contains(",")) {
|
try {
|
||||||
|
val cleanBase64 =
|
||||||
|
if (localBlob.contains(",")) {
|
||||||
localBlob.substringAfter(",")
|
localBlob.substringAfter(",")
|
||||||
} else {
|
} else {
|
||||||
localBlob
|
localBlob
|
||||||
}
|
}
|
||||||
val decodedBytes = Base64.decode(cleanBase64, Base64.DEFAULT)
|
val decodedBytes = Base64.decode(cleanBase64, Base64.DEFAULT)
|
||||||
BitmapFactory.decodeByteArray(decodedBytes, 0, decodedBytes.size)
|
BitmapFactory.decodeByteArray(
|
||||||
|
decodedBytes,
|
||||||
|
0,
|
||||||
|
decodedBytes.size
|
||||||
|
)
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
null
|
null
|
||||||
}
|
}
|
||||||
@@ -1096,8 +1179,8 @@ fun ReplyImagePreview(
|
|||||||
}
|
}
|
||||||
|
|
||||||
Box(
|
Box(
|
||||||
modifier = modifier
|
modifier =
|
||||||
.clip(RoundedCornerShape(4.dp))
|
modifier.clip(RoundedCornerShape(4.dp))
|
||||||
.background(Color.Gray.copy(alpha = 0.3f))
|
.background(Color.Gray.copy(alpha = 0.3f))
|
||||||
) {
|
) {
|
||||||
if (fullImageBitmap != null) {
|
if (fullImageBitmap != null) {
|
||||||
@@ -1118,10 +1201,7 @@ fun ReplyImagePreview(
|
|||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
// Placeholder с иконкой только если нет ничего
|
// Placeholder с иконкой только если нет ничего
|
||||||
Box(
|
Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
|
||||||
modifier = Modifier.fillMaxSize(),
|
|
||||||
contentAlignment = Alignment.Center
|
|
||||||
) {
|
|
||||||
Icon(
|
Icon(
|
||||||
TablerIcons.Photo,
|
TablerIcons.Photo,
|
||||||
contentDescription = null,
|
contentDescription = null,
|
||||||
@@ -1131,7 +1211,8 @@ fun ReplyImagePreview(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}/** Profile photo menu for avatar */
|
}
|
||||||
|
/** Profile photo menu for avatar */
|
||||||
@Composable
|
@Composable
|
||||||
fun ProfilePhotoMenu(
|
fun ProfilePhotoMenu(
|
||||||
expanded: Boolean,
|
expanded: Boolean,
|
||||||
@@ -1141,13 +1222,15 @@ fun ProfilePhotoMenu(
|
|||||||
onDeletePhotoClick: (() -> Unit)? = null,
|
onDeletePhotoClick: (() -> Unit)? = null,
|
||||||
hasAvatar: Boolean = false
|
hasAvatar: Boolean = false
|
||||||
) {
|
) {
|
||||||
val dividerColor = if (isDarkTheme) Color.White.copy(alpha = 0.1f) else Color.Black.copy(alpha = 0.08f)
|
val dividerColor =
|
||||||
|
if (isDarkTheme) Color.White.copy(alpha = 0.1f) else Color.Black.copy(alpha = 0.08f)
|
||||||
|
|
||||||
DropdownMenu(
|
DropdownMenu(
|
||||||
expanded = expanded,
|
expanded = expanded,
|
||||||
onDismissRequest = onDismiss,
|
onDismissRequest = onDismiss,
|
||||||
modifier = Modifier.width(220.dp),
|
modifier = Modifier.width(220.dp),
|
||||||
properties = PopupProperties(
|
properties =
|
||||||
|
PopupProperties(
|
||||||
focusable = true,
|
focusable = true,
|
||||||
dismissOnBackPress = true,
|
dismissOnBackPress = true,
|
||||||
dismissOnClickOutside = true
|
dismissOnClickOutside = true
|
||||||
@@ -1164,8 +1247,8 @@ fun ProfilePhotoMenu(
|
|||||||
// Показываем Delete только если есть аватар
|
// Показываем Delete только если есть аватар
|
||||||
if (hasAvatar && onDeletePhotoClick != null) {
|
if (hasAvatar && onDeletePhotoClick != null) {
|
||||||
Box(
|
Box(
|
||||||
modifier = Modifier
|
modifier =
|
||||||
.fillMaxWidth()
|
Modifier.fillMaxWidth()
|
||||||
.padding(horizontal = 16.dp, vertical = 4.dp)
|
.padding(horizontal = 16.dp, vertical = 4.dp)
|
||||||
.height(0.5.dp)
|
.height(0.5.dp)
|
||||||
.background(dividerColor)
|
.background(dividerColor)
|
||||||
@@ -1192,13 +1275,15 @@ fun OtherProfileMenu(
|
|||||||
onBlockClick: () -> Unit,
|
onBlockClick: () -> Unit,
|
||||||
onClearChatClick: () -> Unit
|
onClearChatClick: () -> Unit
|
||||||
) {
|
) {
|
||||||
val dividerColor = if (isDarkTheme) Color.White.copy(alpha = 0.1f) else Color.Black.copy(alpha = 0.08f)
|
val dividerColor =
|
||||||
|
if (isDarkTheme) Color.White.copy(alpha = 0.1f) else Color.Black.copy(alpha = 0.08f)
|
||||||
|
|
||||||
DropdownMenu(
|
DropdownMenu(
|
||||||
expanded = expanded,
|
expanded = expanded,
|
||||||
onDismissRequest = onDismiss,
|
onDismissRequest = onDismiss,
|
||||||
modifier = Modifier.width(220.dp),
|
modifier = Modifier.width(220.dp),
|
||||||
properties = PopupProperties(
|
properties =
|
||||||
|
PopupProperties(
|
||||||
focusable = true,
|
focusable = true,
|
||||||
dismissOnBackPress = true,
|
dismissOnBackPress = true,
|
||||||
dismissOnClickOutside = true
|
dismissOnClickOutside = true
|
||||||
@@ -1213,8 +1298,8 @@ fun OtherProfileMenu(
|
|||||||
)
|
)
|
||||||
|
|
||||||
Box(
|
Box(
|
||||||
modifier = Modifier
|
modifier =
|
||||||
.fillMaxWidth()
|
Modifier.fillMaxWidth()
|
||||||
.padding(horizontal = 16.dp, vertical = 4.dp)
|
.padding(horizontal = 16.dp, vertical = 4.dp)
|
||||||
.height(0.5.dp)
|
.height(0.5.dp)
|
||||||
.background(dividerColor)
|
.background(dividerColor)
|
||||||
|
|||||||
Reference in New Issue
Block a user