From f78bd0edebaad8f74e97673689426c6d1c620f6c Mon Sep 17 00:00:00 2001 From: k1ngsterr1 Date: Mon, 2 Feb 2026 00:28:23 +0500 Subject: [PATCH] fix: adjust image dimensions and bubble width for better layout in chat components --- .../chats/components/AttachmentComponents.kt | 175 ++++++++++++------ .../chats/components/ChatDetailComponents.kt | 74 ++++++-- 2 files changed, 183 insertions(+), 66 deletions(-) diff --git a/app/src/main/java/com/rosetta/messenger/ui/chats/components/AttachmentComponents.kt b/app/src/main/java/com/rosetta/messenger/ui/chats/components/AttachmentComponents.kt index 4764e6f..33e646b 100644 --- a/app/src/main/java/com/rosetta/messenger/ui/chats/components/AttachmentComponents.kt +++ b/app/src/main/java/com/rosetta/messenger/ui/chats/components/AttachmentComponents.kt @@ -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) { diff --git a/app/src/main/java/com/rosetta/messenger/ui/chats/components/ChatDetailComponents.kt b/app/src/main/java/com/rosetta/messenger/ui/chats/components/ChatDetailComponents.kt index 1ef917b..963760c 100644 --- a/app/src/main/java/com/rosetta/messenger/ui/chats/components/ChatDetailComponents.kt +++ b/app/src/main/java/com/rosetta/messenger/ui/chats/components/ChatDetailComponents.kt @@ -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 = {