fix: adjust image dimensions and bubble width for better layout in chat components

This commit is contained in:
k1ngsterr1
2026-02-02 00:28:23 +05:00
parent 0ba33419dd
commit f78bd0edeb
2 changed files with 183 additions and 66 deletions

View File

@@ -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 {
// Fallback если размеры не указаны - квадрат средний
240.dp to 240.dp
// Вертикальное фото - уменьшаем ширину
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 {
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) {

View File

@@ -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 = {