fix: adjust image dimensions and bubble width for better layout in chat components
This commit is contained in:
@@ -48,9 +48,51 @@ import compose.icons.tablericons.*
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import androidx.compose.ui.platform.LocalConfiguration
|
||||
import kotlin.math.min
|
||||
|
||||
private const val TAG = "AttachmentComponents"
|
||||
|
||||
/**
|
||||
* 📐 Telegram Bubble Specification
|
||||
* Все константы взяты из ChatMessageCell.java и Theme.java
|
||||
*/
|
||||
object TelegramBubbleSpec {
|
||||
// === BUBBLE RADIUS ===
|
||||
val bubbleRadius = 17.dp // chat_msgTextPaintOneEmoji
|
||||
val nearRadius = 6.dp // smallRad для хвостика
|
||||
val minRadius = 6.dp // Минимальный радиус
|
||||
val photoRadius = 15.dp // bubbleRadius - 2 для медиа
|
||||
|
||||
// === PHOTO SIZING ===
|
||||
// maxWidth = min(AndroidUtilities.displaySize.x, AndroidUtilities.displaySize.y) * 0.5f
|
||||
// Telegram использует ~50-65% ширины экрана
|
||||
fun maxPhotoWidth(screenWidth: Int): Int = (screenWidth * 0.65f).toInt()
|
||||
val maxPhotoHeight = 360 // dp, примерно maxWidth + 100
|
||||
val minPhotoWidth = 150 // dp
|
||||
val minPhotoHeight = 120 // dp
|
||||
|
||||
// === CAPTION PADDING ===
|
||||
val captionPaddingTop = 9.dp // Отступ от фото до caption
|
||||
val captionPaddingBottom = 6.dp
|
||||
val captionPaddingStartOutgoing = 11.dp // Исходящие
|
||||
val captionPaddingStartIncoming = 13.dp // Входящие
|
||||
val captionPaddingEnd = 10.dp
|
||||
|
||||
// === TIME OVERLAY (на медиа) ===
|
||||
val timeOverlayPadding = 6.dp
|
||||
val timeOverlayRadius = 10.dp
|
||||
val timeOverlayAlpha = 0.5f
|
||||
val timeTextSize = 11.sp
|
||||
|
||||
// === COLLAGE ===
|
||||
val collageSpacing = 2.dp // Отступ между фото в коллаже
|
||||
|
||||
// === TEXT ===
|
||||
val messageTextSize = 16.sp
|
||||
val captionTextSize = 16.sp
|
||||
}
|
||||
|
||||
/** Статус скачивания attachment (как в desktop) */
|
||||
enum class DownloadStatus {
|
||||
DOWNLOADED,
|
||||
@@ -78,6 +120,7 @@ fun MessageAttachments(
|
||||
avatarRepository: AvatarRepository? = null,
|
||||
currentUserPublicKey: String = "",
|
||||
hasCaption: Boolean = false, // Если есть caption - время показывается под фото, не на фото
|
||||
showTail: Boolean = true, // Показывать хвостик пузырька
|
||||
onImageClick: (attachmentId: String, bounds: ImageSourceBounds?) -> Unit = { _, _ -> },
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
@@ -103,6 +146,7 @@ fun MessageAttachments(
|
||||
timestamp = timestamp,
|
||||
messageStatus = messageStatus,
|
||||
hasCaption = hasCaption,
|
||||
showTail = showTail,
|
||||
onImageClick = onImageClick
|
||||
)
|
||||
}
|
||||
@@ -162,32 +206,35 @@ fun ImageCollage(
|
||||
timestamp: java.util.Date,
|
||||
messageStatus: MessageStatus = MessageStatus.READ,
|
||||
hasCaption: Boolean = false, // Если есть caption - время показывается под фото
|
||||
showTail: Boolean = true, // Показывать хвостик пузырька
|
||||
onImageClick: (attachmentId: String, bounds: ImageSourceBounds?) -> Unit = { _, _ -> },
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
val count = attachments.size
|
||||
val spacing = 2.dp
|
||||
val spacing = TelegramBubbleSpec.collageSpacing
|
||||
|
||||
// Показываем время и статус только если нет caption
|
||||
val showOverlayOnLast = !hasCaption
|
||||
|
||||
// Закругление: если есть caption - только сверху, снизу прямые углы
|
||||
// Telegram: bubbleRadius=17dp, photoRadius=15dp (bubbleRadius-2), minRadius=6dp
|
||||
val collageShape =
|
||||
if (hasCaption) {
|
||||
// Фото над caption: верх=15dp, низ=0dp (плавный переход к caption)
|
||||
RoundedCornerShape(
|
||||
topStart = 16.dp,
|
||||
topEnd = 16.dp,
|
||||
topStart = TelegramBubbleSpec.photoRadius,
|
||||
topEnd = TelegramBubbleSpec.photoRadius,
|
||||
bottomStart = 0.dp,
|
||||
bottomEnd = 0.dp
|
||||
)
|
||||
} else {
|
||||
RoundedCornerShape(16.dp)
|
||||
// Фото без caption - простое закругление
|
||||
RoundedCornerShape(TelegramBubbleSpec.photoRadius)
|
||||
}
|
||||
|
||||
Box(modifier = modifier.clip(collageShape)) {
|
||||
when (count) {
|
||||
1 -> {
|
||||
// Одно фото - полная ширина
|
||||
// Одно фото - размер определяется самим фото (Telegram style)
|
||||
ImageAttachment(
|
||||
attachment = attachments[0],
|
||||
chachaKey = chachaKey,
|
||||
@@ -198,6 +245,7 @@ fun ImageCollage(
|
||||
timestamp = timestamp,
|
||||
messageStatus = messageStatus,
|
||||
showTimeOverlay = showOverlayOnLast,
|
||||
hasCaption = hasCaption,
|
||||
onImageClick = onImageClick
|
||||
)
|
||||
}
|
||||
@@ -464,6 +512,7 @@ fun ImageAttachment(
|
||||
showTimeOverlay: Boolean = true,
|
||||
aspectRatio: Float? = null,
|
||||
fillMaxSize: Boolean = false,
|
||||
hasCaption: Boolean = false,
|
||||
onImageClick: (attachmentId: String, bounds: ImageSourceBounds?) -> Unit = { _, _ -> }
|
||||
) {
|
||||
val context = LocalContext.current
|
||||
@@ -627,60 +676,63 @@ fun ImageAttachment(
|
||||
if (isDarkTheme) Color.White.copy(alpha = 0.1f) else Color.Black.copy(alpha = 0.08f)
|
||||
}
|
||||
|
||||
// 📐 Вычисляем размер пузырька на основе соотношения сторон изображения (как в Telegram)
|
||||
// Используем размеры из attachment или из загруженного bitmap
|
||||
// 📐 TELEGRAM-STYLE sizing (из ChatMessageCell.java)
|
||||
// maxWidth = min(displaySize.x, displaySize.y) * 0.5f (~65% в портрете)
|
||||
// Сохраняем aspect ratio, ограничиваем max/min размеры
|
||||
val configuration = LocalConfiguration.current
|
||||
val screenWidthDp = configuration.screenWidthDp
|
||||
|
||||
val actualWidth = if (attachment.width > 0) attachment.width else imageBitmap?.width ?: 0
|
||||
val actualHeight = if (attachment.height > 0) attachment.height else imageBitmap?.height ?: 0
|
||||
|
||||
val (imageWidth, imageHeight) =
|
||||
remember(actualWidth, actualHeight, fillMaxSize, aspectRatio) {
|
||||
// Если fillMaxSize - используем 100% контейнера
|
||||
remember(actualWidth, actualHeight, fillMaxSize, aspectRatio, screenWidthDp) {
|
||||
if (fillMaxSize) {
|
||||
null to null
|
||||
} else {
|
||||
// Telegram-style размеры - больше и адаптивнее
|
||||
val maxWidth = 280.dp
|
||||
val maxHeight = 400.dp // Увеличено для вертикальных фото
|
||||
val minWidth = 160.dp
|
||||
val minHeight = 120.dp
|
||||
// Telegram: maxWidth = screenWidth * 0.65
|
||||
val maxPhotoWidthPx = TelegramBubbleSpec.maxPhotoWidth(screenWidthDp)
|
||||
val maxPhotoWidth = maxPhotoWidthPx.dp
|
||||
val maxPhotoHeight = TelegramBubbleSpec.maxPhotoHeight.dp
|
||||
val minHeight = TelegramBubbleSpec.minPhotoHeight.dp
|
||||
val minWidth = TelegramBubbleSpec.minPhotoWidth.dp
|
||||
|
||||
if (actualWidth > 0 && actualHeight > 0) {
|
||||
val ar = actualWidth.toFloat() / actualHeight.toFloat()
|
||||
|
||||
when {
|
||||
// Очень широкое изображение (panorama)
|
||||
ar > 2.0f -> {
|
||||
val width = maxWidth
|
||||
val height = (maxWidth.value / ar).dp.coerceIn(minHeight, maxHeight)
|
||||
width to height
|
||||
}
|
||||
// Широкое изображение (landscape)
|
||||
ar > 1.2f -> {
|
||||
val width = maxWidth
|
||||
val height = (maxWidth.value / ar).dp.coerceIn(minHeight, maxHeight)
|
||||
width to height
|
||||
}
|
||||
// Очень высокое изображение (story-like)
|
||||
ar < 0.5f -> {
|
||||
val height = maxHeight
|
||||
val width = (maxHeight.value * ar).dp.coerceIn(minWidth, maxWidth)
|
||||
width to height
|
||||
}
|
||||
// Высокое изображение (portrait)
|
||||
ar < 0.85f -> {
|
||||
val height = maxHeight
|
||||
val width = (maxHeight.value * ar).dp.coerceIn(minWidth, maxWidth)
|
||||
width to height
|
||||
}
|
||||
// Квадратное или близкое к квадрату
|
||||
else -> {
|
||||
val size = 240.dp
|
||||
size to size
|
||||
}
|
||||
// Telegram: scale = imageW / photoWidth, пропорционально масштабируем
|
||||
var w: Float
|
||||
var h: Float
|
||||
|
||||
if (ar >= 1f) {
|
||||
// Горизонтальное фото - по ширине
|
||||
w = maxPhotoWidthPx.toFloat()
|
||||
h = w / ar
|
||||
} else {
|
||||
// Вертикальное фото - уменьшаем ширину
|
||||
w = maxPhotoWidthPx * 0.75f
|
||||
h = w / ar
|
||||
}
|
||||
|
||||
// Ограничение максимальной высоты
|
||||
if (h > TelegramBubbleSpec.maxPhotoHeight) {
|
||||
h = TelegramBubbleSpec.maxPhotoHeight.toFloat()
|
||||
w = h * ar
|
||||
}
|
||||
|
||||
// Минимальная высота
|
||||
if (h < TelegramBubbleSpec.minPhotoHeight) {
|
||||
h = TelegramBubbleSpec.minPhotoHeight.toFloat()
|
||||
w = h * ar
|
||||
}
|
||||
|
||||
// Ограничения по ширине
|
||||
w = w.coerceIn(TelegramBubbleSpec.minPhotoWidth.toFloat(), maxPhotoWidthPx.toFloat())
|
||||
h = h.coerceIn(TelegramBubbleSpec.minPhotoHeight.toFloat(), TelegramBubbleSpec.maxPhotoHeight.toFloat())
|
||||
|
||||
w.dp to h.dp
|
||||
} else {
|
||||
// Fallback если размеры не указаны - квадрат средний
|
||||
240.dp to 240.dp
|
||||
200.dp to 200.dp
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -692,15 +744,28 @@ fun ImageAttachment(
|
||||
aspectRatio != null -> Modifier.fillMaxWidth().aspectRatio(aspectRatio)
|
||||
imageWidth != null && imageHeight != null ->
|
||||
Modifier.width(imageWidth).height(imageHeight)
|
||||
else -> Modifier.size(220.dp)
|
||||
else -> Modifier.size(200.dp)
|
||||
}
|
||||
|
||||
val cornerRadius = if (fillMaxSize) 0f else 12f
|
||||
// Telegram: photoRadius = 15dp (bubbleRadius - 2)
|
||||
// Если есть caption - нижние углы прямые
|
||||
val photoRadius = TelegramBubbleSpec.photoRadius
|
||||
val imageShape = when {
|
||||
fillMaxSize -> RoundedCornerShape(0.dp)
|
||||
hasCaption -> RoundedCornerShape(
|
||||
topStart = photoRadius,
|
||||
topEnd = photoRadius,
|
||||
bottomStart = 0.dp,
|
||||
bottomEnd = 0.dp
|
||||
)
|
||||
else -> RoundedCornerShape(photoRadius)
|
||||
}
|
||||
val cornerRadius = if (fillMaxSize || hasCaption) 0f else 15f
|
||||
|
||||
Box(
|
||||
modifier =
|
||||
sizeModifier
|
||||
.clip(RoundedCornerShape(cornerRadius.dp))
|
||||
.clip(imageShape)
|
||||
.background(Color.Transparent)
|
||||
.onGloballyPositioned { coordinates ->
|
||||
// Capture bounds for shared element transition
|
||||
@@ -778,10 +843,10 @@ fun ImageAttachment(
|
||||
Box(
|
||||
modifier =
|
||||
Modifier.align(Alignment.BottomEnd)
|
||||
.padding(8.dp)
|
||||
.padding(TelegramBubbleSpec.timeOverlayPadding)
|
||||
.background(
|
||||
Color.Black.copy(alpha = 0.5f),
|
||||
shape = RoundedCornerShape(10.dp)
|
||||
Color.Black.copy(alpha = TelegramBubbleSpec.timeOverlayAlpha),
|
||||
shape = RoundedCornerShape(TelegramBubbleSpec.timeOverlayRadius)
|
||||
)
|
||||
.padding(horizontal = 6.dp, vertical = 3.dp)
|
||||
) {
|
||||
@@ -792,7 +857,7 @@ fun ImageAttachment(
|
||||
Text(
|
||||
text = timeFormat.format(timestamp),
|
||||
color = Color.White,
|
||||
fontSize = 11.sp,
|
||||
fontSize = TelegramBubbleSpec.timeTextSize,
|
||||
fontWeight = FontWeight.Medium
|
||||
)
|
||||
if (isOutgoing) {
|
||||
|
||||
@@ -32,6 +32,7 @@ import androidx.compose.ui.layout.Layout
|
||||
import androidx.compose.ui.layout.Measurable
|
||||
import androidx.compose.ui.layout.Placeable
|
||||
import androidx.compose.ui.platform.LocalDensity
|
||||
import androidx.compose.ui.platform.LocalConfiguration
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.IntOffset
|
||||
@@ -283,15 +284,18 @@ fun MessageBubble(
|
||||
else if (isDarkTheme) Color(0xFF8E8E93) else Color(0xFF666666)
|
||||
}
|
||||
|
||||
// Telegram: bubbleRadius = 17dp, smallRad (хвостик) = 6dp
|
||||
val bubbleShape =
|
||||
remember(message.isOutgoing, showTail) {
|
||||
RoundedCornerShape(
|
||||
topStart = 18.dp,
|
||||
topEnd = 18.dp,
|
||||
topStart = TelegramBubbleSpec.bubbleRadius,
|
||||
topEnd = TelegramBubbleSpec.bubbleRadius,
|
||||
bottomStart =
|
||||
if (message.isOutgoing) 18.dp else (if (showTail) 4.dp else 18.dp),
|
||||
if (message.isOutgoing) TelegramBubbleSpec.bubbleRadius
|
||||
else (if (showTail) TelegramBubbleSpec.nearRadius else TelegramBubbleSpec.bubbleRadius),
|
||||
bottomEnd =
|
||||
if (message.isOutgoing) (if (showTail) 4.dp else 18.dp) else 18.dp
|
||||
if (message.isOutgoing) (if (showTail) TelegramBubbleSpec.nearRadius else TelegramBubbleSpec.bubbleRadius)
|
||||
else TelegramBubbleSpec.bubbleRadius
|
||||
)
|
||||
}
|
||||
|
||||
@@ -447,11 +451,49 @@ fun MessageBubble(
|
||||
}
|
||||
val bubbleBorderWidth = if (hasOnlyMedia) 1.dp else 0.dp
|
||||
|
||||
// Telegram-style: ширина пузырька = ширина фото
|
||||
// Caption переносится на новые строки, не расширяя пузырёк
|
||||
val configuration = LocalConfiguration.current
|
||||
val screenWidthDp = configuration.screenWidthDp
|
||||
val maxPhotoWidth = TelegramBubbleSpec.maxPhotoWidth(screenWidthDp).dp
|
||||
|
||||
// Вычисляем ширину фото для ограничения пузырька
|
||||
val photoWidth = if (hasImageWithCaption || hasOnlyMedia) {
|
||||
val firstImage = message.attachments.firstOrNull {
|
||||
it.type == com.rosetta.messenger.network.AttachmentType.IMAGE
|
||||
}
|
||||
if (firstImage != null && firstImage.width > 0 && firstImage.height > 0) {
|
||||
val ar = firstImage.width.toFloat() / firstImage.height.toFloat()
|
||||
val maxW = TelegramBubbleSpec.maxPhotoWidth(screenWidthDp).toFloat()
|
||||
var w = if (ar >= 1f) maxW else maxW * 0.75f
|
||||
var h = w / ar
|
||||
|
||||
if (h > TelegramBubbleSpec.maxPhotoHeight) {
|
||||
h = TelegramBubbleSpec.maxPhotoHeight.toFloat()
|
||||
w = h * ar
|
||||
}
|
||||
if (h < TelegramBubbleSpec.minPhotoHeight) {
|
||||
h = TelegramBubbleSpec.minPhotoHeight.toFloat()
|
||||
w = h * ar
|
||||
}
|
||||
w.coerceIn(TelegramBubbleSpec.minPhotoWidth.toFloat(), maxW).dp
|
||||
} else {
|
||||
maxPhotoWidth
|
||||
}
|
||||
} else {
|
||||
280.dp
|
||||
}
|
||||
|
||||
val bubbleWidthModifier = if (hasImageWithCaption || hasOnlyMedia) {
|
||||
Modifier.widthIn(max = photoWidth) // Жёстко ограничиваем ширину размером фото
|
||||
} else {
|
||||
Modifier.widthIn(min = 60.dp, max = 280.dp)
|
||||
}
|
||||
|
||||
Box(
|
||||
modifier =
|
||||
Modifier.padding(end = 12.dp)
|
||||
.widthIn(min = 60.dp, max = 280.dp)
|
||||
.wrapContentWidth(unbounded = false)
|
||||
.then(bubbleWidthModifier)
|
||||
.graphicsLayer {
|
||||
this.alpha = selectionAlpha
|
||||
this.scaleX = selectionScale
|
||||
@@ -516,19 +558,29 @@ fun MessageBubble(
|
||||
messageStatus = attachmentDisplayStatus,
|
||||
avatarRepository = avatarRepository,
|
||||
currentUserPublicKey = currentUserPublicKey,
|
||||
hasCaption = hasImageWithCaption, // Если есть caption - время на
|
||||
// пузырьке, не на фото
|
||||
hasCaption = hasImageWithCaption,
|
||||
showTail = showTail, // Передаём для формы пузырька
|
||||
onImageClick = onImageClick
|
||||
)
|
||||
}
|
||||
|
||||
// 🖼️ Caption под фото (как в Telegram) - Telegram-style layout
|
||||
// 🖼️ Caption под фото (Telegram-style)
|
||||
// Отступ от фото = 9dp, слева = 11dp (исходящие) / 13dp (входящие)
|
||||
if (hasImageWithCaption) {
|
||||
val captionPaddingStart = if (message.isOutgoing)
|
||||
TelegramBubbleSpec.captionPaddingStartOutgoing
|
||||
else
|
||||
TelegramBubbleSpec.captionPaddingStartIncoming
|
||||
Box(
|
||||
modifier =
|
||||
Modifier.fillMaxWidth()
|
||||
Modifier
|
||||
.background(bubbleColor)
|
||||
.padding(horizontal = 10.dp, vertical = 6.dp)
|
||||
.padding(
|
||||
start = captionPaddingStart,
|
||||
end = TelegramBubbleSpec.captionPaddingEnd,
|
||||
top = TelegramBubbleSpec.captionPaddingTop,
|
||||
bottom = TelegramBubbleSpec.captionPaddingBottom
|
||||
)
|
||||
) {
|
||||
TelegramStyleMessageContent(
|
||||
textContent = {
|
||||
|
||||
Reference in New Issue
Block a user