Refactor code structure for improved readability and maintainability

This commit is contained in:
k1ngsterr1
2026-02-08 05:47:24 +05:00
parent 0eddd448c7
commit 0d0e1e2c22
2 changed files with 1527 additions and 1261 deletions

View File

@@ -4,10 +4,8 @@ import android.content.Context
import androidx.compose.animation.*
import androidx.compose.animation.core.*
import androidx.compose.foundation.*
import androidx.compose.foundation.gestures.detectHorizontalDragGestures
import androidx.compose.foundation.gestures.awaitEachGesture
import androidx.compose.foundation.gestures.awaitFirstDown
import androidx.compose.foundation.gestures.horizontalDrag
import androidx.activity.compose.BackHandler
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.LazyColumn
@@ -28,6 +26,7 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.draw.clipToBounds
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.input.pointer.changedToUpIgnoreConsumed
import androidx.compose.ui.input.pointer.pointerInput
import androidx.compose.ui.input.pointer.positionChange
import androidx.compose.ui.input.pointer.util.VelocityTracker
@@ -1694,6 +1693,8 @@ fun SwipeableDialogItem(
.background(backgroundColor)
.pointerInput(Unit) {
val velocityTracker = VelocityTracker()
val touchSlop = viewConfiguration.touchSlop
awaitEachGesture {
val down = awaitFirstDown(requireUnconsumed = false)
@@ -1701,22 +1702,58 @@ fun SwipeableDialogItem(
if (isDrawerOpen) return@awaitEachGesture
velocityTracker.resetTracking()
var started = false
try {
horizontalDrag(down.id) { change ->
val dragAmount = change.positionChange().x
var totalDragX = 0f
var totalDragY = 0f
var passedSlop = false
var claimed = false
// First movement determines direction
if (!started) {
// Swipe right with actions closed — let drawer handle it
if (dragAmount > 0 && offsetX == 0f) {
return@horizontalDrag
}
started = true
while (true) {
val event = awaitPointerEvent()
val change = event.changes.firstOrNull { it.id == down.id }
?: break
if (change.changedToUpIgnoreConsumed()) break
val delta = change.positionChange()
totalDragX += delta.x
totalDragY += delta.y
if (!passedSlop) {
val dist = kotlin.math.sqrt(
totalDragX * totalDragX + totalDragY * totalDragY
)
if (dist < touchSlop) continue
val dominated = kotlin.math.abs(totalDragX) >
kotlin.math.abs(totalDragY) * 2.0f
when {
// Horizontal left swipe — reveal action buttons
dominated && totalDragX < 0 -> {
passedSlop = true
claimed = true
onSwipeStarted()
change.consume()
}
val newOffset = offsetX + dragAmount
// Horizontal right swipe with buttons open — close them
dominated && totalDragX > 0 && offsetX != 0f -> {
passedSlop = true
claimed = true
change.consume()
}
// Right swipe with buttons closed — let drawer handle
totalDragX > 0 && offsetX == 0f -> break
// Vertical/diagonal — close buttons if open, let LazyColumn scroll
else -> {
if (offsetX != 0f) {
offsetX = 0f
onSwipeClosed()
}
break
}
}
} else {
// Gesture is ours — update offset
val newOffset = offsetX + delta.x
offsetX = newOffset.coerceIn(-swipeWidthPx, 0f)
velocityTracker.addPosition(
change.uptimeMillis,
@@ -1724,21 +1761,31 @@ fun SwipeableDialogItem(
)
change.consume()
}
} catch (_: Exception) {
offsetX = 0f
}
if (started) {
// Snap animation
if (claimed) {
val velocity = velocityTracker.calculateVelocity().x
// Telegram-like: fling left (-velocity) OR dragged past 1/3
val shouldOpen = velocity < -300f ||
kotlin.math.abs(offsetX) > swipeWidthPx / 3
if (shouldOpen) {
offsetX = -swipeWidthPx
} else {
when {
// Rightward fling — always close
velocity > 150f -> {
offsetX = 0f
onSwipeClosed()
}
// Strong leftward fling — always open
velocity < -300f -> {
offsetX = -swipeWidthPx
}
// Past halfway — stay open
kotlin.math.abs(offsetX) > swipeWidthPx / 2 -> {
offsetX = -swipeWidthPx
}
// Less than halfway — close
else -> {
offsetX = 0f
onSwipeClosed()
}
}
}
}
}

View File

@@ -11,10 +11,7 @@ import androidx.compose.foundation.background
import androidx.compose.foundation.border
import androidx.compose.foundation.clickable
import androidx.compose.foundation.combinedClickable
import androidx.compose.foundation.gestures.awaitEachGesture
import androidx.compose.foundation.gestures.awaitFirstDown
import androidx.compose.foundation.gestures.detectHorizontalDragGestures
import kotlin.math.abs
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.CircleShape
@@ -30,13 +27,10 @@ import androidx.compose.ui.graphics.asImageBitmap
import androidx.compose.ui.graphics.graphicsLayer
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.input.pointer.pointerInput
import androidx.compose.ui.input.pointer.positionChange
import androidx.compose.ui.layout.ContentScale
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.platform.LocalDensity
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.IntOffset
@@ -56,6 +50,7 @@ import compose.icons.TablerIcons
import compose.icons.tablericons.*
import java.text.SimpleDateFormat
import java.util.*
import kotlin.math.abs
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
@@ -65,10 +60,9 @@ import kotlinx.coroutines.withContext
*/
/**
* Telegram-style layout для текста сообщения с временем.
* Если текст + время помещаются в одну строку - располагает их рядом.
* Если текст длинный и переносится - время встаёт в правый нижний угол,
* под последней строкой текста (как в Telegram).
* Telegram-style layout для текста сообщения с временем. Если текст + время помещаются в одну
* строку - располагает их рядом. Если текст длинный и переносится - время встаёт в правый нижний
* угол, под последней строкой текста (как в Telegram).
*
* @param textContent Composable с текстом сообщения
* @param timeContent Composable с временем и статусом
@@ -94,13 +88,16 @@ fun TelegramStyleMessageContent(
},
modifier = modifier
) { measurables, constraints ->
require(measurables.size == 2) { "TelegramStyleMessageContent requires exactly 2 children" }
require(measurables.size == 2) {
"TelegramStyleMessageContent requires exactly 2 children"
}
val textMeasurable = measurables[0]
val timeMeasurable = measurables[1]
// Измеряем время с минимальными constraints
val timePlaceable = timeMeasurable.measure(constraints.copy(minWidth = 0, minHeight = 0))
val timePlaceable =
timeMeasurable.measure(constraints.copy(minWidth = 0, minHeight = 0))
// Измеряем текст с полной доступной шириной
val textConstraints = constraints.copy(minWidth = 0, minHeight = 0)
@@ -120,13 +117,15 @@ fun TelegramStyleMessageContent(
// 🔥 Для caption - занимаем всю доступную ширину
val w = constraints.maxWidth
// 🔥 ИСПРАВЛЕНИЕ: Проверяем помещается ли текст + время в одну строку
// 🔥 ИСПРАВЛЕНИЕ: Проверяем помещается ли текст + время в одну
// строку
val availableForTime = w - textWidth - spacingPx
if (availableForTime >= timeWidth) {
// Текст и время помещаются - время справа на одной линии
val h = maxOf(textPlaceable.height, timePlaceable.height)
val tX = w - timeWidth // Время справа
val tY = h - timePlaceable.height // Время внизу строки (выровнено по baseline)
val tY = h - timePlaceable.height // Время внизу строки
// (выровнено по baseline)
LayoutResult(w, h, tX, tY)
} else {
// Текст длинный - время на новой строке справа внизу
@@ -323,9 +322,13 @@ fun MessageBubble(
topEnd = TelegramBubbleSpec.bubbleRadius,
bottomStart =
if (message.isOutgoing) TelegramBubbleSpec.bubbleRadius
else (if (showTail) TelegramBubbleSpec.nearRadius else TelegramBubbleSpec.bubbleRadius),
else
(if (showTail) TelegramBubbleSpec.nearRadius
else TelegramBubbleSpec.bubbleRadius),
bottomEnd =
if (message.isOutgoing) (if (showTail) TelegramBubbleSpec.nearRadius else TelegramBubbleSpec.bubbleRadius)
if (message.isOutgoing)
(if (showTail) TelegramBubbleSpec.nearRadius
else TelegramBubbleSpec.bubbleRadius)
else TelegramBubbleSpec.bubbleRadius
)
}
@@ -336,7 +339,8 @@ fun MessageBubble(
modifier =
Modifier.fillMaxWidth().pointerInput(Unit) {
// 🔥 Простой горизонтальный свайп для reply
// Используем detectHorizontalDragGestures который лучше работает со скроллом
// Используем detectHorizontalDragGestures который лучше работает со
// скроллом
detectHorizontalDragGestures(
onDragStart = {},
onDragEnd = {
@@ -346,15 +350,14 @@ fun MessageBubble(
}
swipeOffset = 0f
},
onDragCancel = {
swipeOffset = 0f
},
onDragCancel = { swipeOffset = 0f },
onHorizontalDrag = { change, dragAmount ->
// Только свайп влево (отрицательное значение)
if (dragAmount < 0 || swipeOffset < 0) {
change.consume()
val newOffset = swipeOffset + dragAmount
swipeOffset = newOffset.coerceIn(-maxSwipe, 0f)
swipeOffset =
newOffset.coerceIn(-maxSwipe, 0f)
}
}
)
@@ -363,7 +366,9 @@ fun MessageBubble(
// 🔥 Reply icon - справа, появляется при свайпе влево
Box(
modifier =
Modifier.align(Alignment.CenterEnd).padding(end = 16.dp).graphicsLayer {
Modifier.align(Alignment.CenterEnd)
.padding(end = 16.dp)
.graphicsLayer {
alpha = swipeProgress
scaleX = swipeProgress
scaleY = swipeProgress
@@ -466,7 +471,9 @@ fun MessageBubble(
message.text.isEmpty() &&
message.replyData == null &&
message.attachments.all {
it.type == com.rosetta.messenger.network.AttachmentType.IMAGE
it.type ==
com.rosetta.messenger.network.AttachmentType
.IMAGE
}
// Фото + caption (как в Telegram)
@@ -475,7 +482,9 @@ fun MessageBubble(
message.text.isNotEmpty() &&
message.replyData == null &&
message.attachments.all {
it.type == com.rosetta.messenger.network.AttachmentType.IMAGE
it.type ==
com.rosetta.messenger.network.AttachmentType
.IMAGE
}
// Для сообщений только с фото - минимальный padding и тонкий border
@@ -495,25 +504,47 @@ fun MessageBubble(
val maxPhotoWidth = TelegramBubbleSpec.maxPhotoWidth(screenWidthDp).dp
// Вычисляем ширину фото для ограничения пузырька
val photoWidth = if (hasImageWithCaption || hasOnlyMedia) {
val firstImage = message.attachments.firstOrNull {
it.type == com.rosetta.messenger.network.AttachmentType.IMAGE
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()
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()
h =
TelegramBubbleSpec.maxPhotoHeight
.toFloat()
w = h * ar
}
if (h < TelegramBubbleSpec.minPhotoHeight) {
h = TelegramBubbleSpec.minPhotoHeight.toFloat()
h =
TelegramBubbleSpec.minPhotoHeight
.toFloat()
w = h * ar
}
w.coerceIn(TelegramBubbleSpec.minPhotoWidth.toFloat(), maxW).dp
w.coerceIn(
TelegramBubbleSpec.minPhotoWidth
.toFloat(),
maxW
)
.dp
} else {
maxPhotoWidth
}
@@ -521,8 +552,12 @@ fun MessageBubble(
280.dp
}
val bubbleWidthModifier = if (hasImageWithCaption || hasOnlyMedia) {
Modifier.width(photoWidth) // 🔥 Фиксированная ширина = размер фото (убирает лишний отступ)
val bubbleWidthModifier =
if (hasImageWithCaption || hasOnlyMedia) {
Modifier.width(
photoWidth
) // 🔥 Фиксированная ширина = размер фото (убирает лишний
// отступ)
} else {
Modifier.widthIn(min = 60.dp, max = 280.dp)
}
@@ -539,7 +574,9 @@ fun MessageBubble(
.combinedClickable(
indication = null,
interactionSource =
remember { MutableInteractionSource() },
remember {
MutableInteractionSource()
},
onClick = onClick,
onLongClick = onLongClick
)
@@ -549,16 +586,26 @@ fun MessageBubble(
Modifier.border(
width = bubbleBorderWidth,
color =
if (message.isOutgoing) {
Color.White.copy(alpha = 0.15f)
if (message.isOutgoing
) {
Color.White
.copy(
alpha =
0.15f
)
} else {
if (isDarkTheme)
Color.White.copy(
alpha = 0.1f
if (isDarkTheme
)
Color.White
.copy(
alpha =
0.1f
)
else
Color.Black.copy(
alpha = 0.08f
Color.Black
.copy(
alpha =
0.08f
)
},
shape = bubbleShape
@@ -583,7 +630,8 @@ fun MessageBubble(
// 📎 Attachments (IMAGE, FILE, AVATAR)
if (message.attachments.isNotEmpty()) {
val attachmentDisplayStatus =
if (isSavedMessages) MessageStatus.READ else message.status
if (isSavedMessages) MessageStatus.READ
else message.status
MessageAttachments(
attachments = message.attachments,
chachaKey = message.chachaKey,
@@ -596,28 +644,42 @@ fun MessageBubble(
avatarRepository = avatarRepository,
currentUserPublicKey = currentUserPublicKey,
hasCaption = hasImageWithCaption,
showTail = showTail, // Передаём для формы пузырька
showTail = showTail, // Передаём для формы
// пузырька
onImageClick = onImageClick
)
}
// 🖼️ Caption под фото (Telegram-style)
// Отступ от фото = 9dp, слева = 11dp (исходящие) / 13dp (входящие)
// Отступ от фото = 9dp, слева = 11dp (исходящие) / 13dp
// (входящие)
if (hasImageWithCaption) {
val captionPaddingStart = if (message.isOutgoing)
TelegramBubbleSpec.captionPaddingStartOutgoing
val captionPaddingStart =
if (message.isOutgoing)
TelegramBubbleSpec
.captionPaddingStartOutgoing
else
TelegramBubbleSpec.captionPaddingStartIncoming
TelegramBubbleSpec
.captionPaddingStartIncoming
Box(
modifier =
Modifier
.fillMaxWidth() // 🔥 Растягиваем на ширину фото для правильного позиционирования времени
Modifier.fillMaxWidth() // 🔥
// Растягиваем на ширину
// фото для правильного
// позиционирования времени
.background(bubbleColor)
.padding(
start = captionPaddingStart,
end = TelegramBubbleSpec.captionPaddingEnd,
top = TelegramBubbleSpec.captionPaddingTop,
bottom = TelegramBubbleSpec.captionPaddingBottom
start =
captionPaddingStart,
end =
TelegramBubbleSpec
.captionPaddingEnd,
top =
TelegramBubbleSpec
.captionPaddingTop,
bottom =
TelegramBubbleSpec
.captionPaddingBottom
)
) {
TelegramStyleMessageContent(
@@ -626,48 +688,82 @@ fun MessageBubble(
text = message.text,
color = textColor,
fontSize = 16.sp,
linkColor = linkColor,
onLongClick = onLongClick // 🔥 Long press для selection
linkColor =
linkColor,
onLongClick =
onLongClick // 🔥 Long press для selection
)
},
timeContent = {
Row(
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(2.dp)
verticalAlignment =
Alignment
.CenterVertically,
horizontalArrangement =
Arrangement
.spacedBy(
2.dp
)
) {
Text(
text = timeFormat.format(message.timestamp),
color = timeColor,
fontSize = 11.sp,
text =
timeFormat
.format(
message.timestamp
),
color =
timeColor,
fontSize =
11.sp,
fontStyle =
androidx.compose.ui.text.font.FontStyle
androidx.compose
.ui
.text
.font
.FontStyle
.Italic
)
if (message.isOutgoing) {
if (message.isOutgoing
) {
val displayStatus =
if (isSavedMessages) MessageStatus.READ
else message.status
if (isSavedMessages
)
MessageStatus
.READ
else
message.status
AnimatedMessageStatus(
status = displayStatus,
timeColor = timeColor,
timestamp = message.timestamp.time,
onRetry = onRetry,
onDelete = onDelete
status =
displayStatus,
timeColor =
timeColor,
timestamp =
message.timestamp
.time,
onRetry =
onRetry,
onDelete =
onDelete
)
}
}
},
spacing = 8,
fillWidth = true // 🔥 Время справа на всю ширину фото
fillWidth =
true // 🔥 Время справа на
// всю ширину фото
)
}
} else if (message.attachments.isNotEmpty() && message.text.isNotEmpty()) {
} else if (message.attachments.isNotEmpty() &&
message.text.isNotEmpty()
) {
// Обычное фото + текст (не только изображения)
Spacer(modifier = Modifier.height(8.dp))
}
// Если есть reply - Telegram-style layout
if (message.replyData != null && message.text.isNotEmpty()) {
if (message.replyData != null && message.text.isNotEmpty()
) {
TelegramStyleMessageContent(
textContent = {
AppleEmojiText(
@@ -675,39 +771,74 @@ fun MessageBubble(
color = textColor,
fontSize = 17.sp,
linkColor = linkColor,
onLongClick = onLongClick // 🔥 Long press для selection
onLongClick =
onLongClick // 🔥
// Long
// press
// для
// selection
)
},
timeContent = {
Row(
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(2.dp)
verticalAlignment =
Alignment
.CenterVertically,
horizontalArrangement =
Arrangement
.spacedBy(
2.dp
)
) {
Text(
text = timeFormat.format(message.timestamp),
text =
timeFormat
.format(
message.timestamp
),
color = timeColor,
fontSize = 11.sp,
fontStyle =
androidx.compose.ui.text.font.FontStyle.Italic
androidx.compose
.ui
.text
.font
.FontStyle
.Italic
)
if (message.isOutgoing) {
val displayStatus =
if (isSavedMessages) MessageStatus.READ
else message.status
if (isSavedMessages
)
MessageStatus
.READ
else
message.status
AnimatedMessageStatus(
status = displayStatus,
timeColor = timeColor,
timestamp = message.timestamp.time,
onRetry = onRetry,
onDelete = onDelete
status =
displayStatus,
timeColor =
timeColor,
timestamp =
message.timestamp
.time,
onRetry =
onRetry,
onDelete =
onDelete
)
}
}
},
fillWidth = true // 🔥 Время справа с учётом ширины ReplyBubble
fillWidth = true // 🔥 Время справа с учётом
// ширины ReplyBubble
)
} else if (!hasOnlyMedia && !hasImageWithCaption && message.text.isNotEmpty()) {
// Telegram-style: текст + время с автоматическим переносом
} else if (!hasOnlyMedia &&
!hasImageWithCaption &&
message.text.isNotEmpty()
) {
// Telegram-style: текст + время с автоматическим
// переносом
TelegramStyleMessageContent(
textContent = {
AppleEmojiText(
@@ -715,31 +846,61 @@ fun MessageBubble(
color = textColor,
fontSize = 17.sp,
linkColor = linkColor,
onLongClick = onLongClick // 🔥 Long press для selection
onLongClick =
onLongClick // 🔥
// Long
// press
// для
// selection
)
},
timeContent = {
Row(
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(2.dp)
verticalAlignment =
Alignment
.CenterVertically,
horizontalArrangement =
Arrangement
.spacedBy(
2.dp
)
) {
Text(
text = timeFormat.format(message.timestamp),
text =
timeFormat
.format(
message.timestamp
),
color = timeColor,
fontSize = 11.sp,
fontStyle =
androidx.compose.ui.text.font.FontStyle.Italic
androidx.compose
.ui
.text
.font
.FontStyle
.Italic
)
if (message.isOutgoing) {
val displayStatus =
if (isSavedMessages) MessageStatus.READ
else message.status
if (isSavedMessages
)
MessageStatus
.READ
else
message.status
AnimatedMessageStatus(
status = displayStatus,
timeColor = timeColor,
timestamp = message.timestamp.time,
onRetry = onRetry,
onDelete = onDelete
status =
displayStatus,
timeColor =
timeColor,
timestamp =
message.timestamp
.time,
onRetry =
onRetry,
onDelete =
onDelete
)
}
}
@@ -762,7 +923,9 @@ fun AnimatedMessageStatus(
onDelete: () -> Unit = {}
) {
val isTimedOut =
status == MessageStatus.SENDING && timestamp > 0 && !isMessageDeliveredByTime(timestamp)
status == MessageStatus.SENDING &&
timestamp > 0 &&
!isMessageDeliveredByTime(timestamp)
val effectiveStatus = if (isTimedOut) MessageStatus.ERROR else status
val targetColor =
@@ -827,13 +990,18 @@ fun AnimatedMessageStatus(
.scale(scale)
.then(
if (currentStatus == MessageStatus.ERROR) {
Modifier.clickable { showErrorMenu = true }
Modifier.clickable {
showErrorMenu = true
}
} else Modifier
)
)
}
DropdownMenu(expanded = showErrorMenu, onDismissRequest = { showErrorMenu = false }) {
DropdownMenu(
expanded = showErrorMenu,
onDismissRequest = { showErrorMenu = false }
) {
DropdownMenuItem(
text = { Text("Retry") },
onClick = {
@@ -906,19 +1074,22 @@ fun ReplyBubble(
if (imageAttachment != null && imageAttachment.preview.isNotEmpty()) {
withContext(Dispatchers.IO) {
try {
// Получаем blurhash из preview (может быть в формате "tag::blurhash")
// Получаем blurhash из preview (может быть в формате
// "tag::blurhash")
val blurhash =
if (imageAttachment.preview.contains("::")) {
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
} else {
""
}
if (blurhash.isNotEmpty()) {
blurPreviewBitmap = BlurHash.decode(blurhash, 36, 36)
blurPreviewBitmap =
BlurHash.decode(blurhash, 36, 36)
}
} catch (e: Exception) {
// Ignore blurhash decode errors
@@ -937,12 +1108,23 @@ fun ReplyBubble(
val decoded =
try {
val cleanBase64 =
if (imageAttachment.blob.contains(",")) {
imageAttachment.blob.substringAfter(",")
if (imageAttachment.blob
.contains(
","
)
) {
imageAttachment.blob
.substringAfter(
","
)
} else {
imageAttachment.blob
}
val decodedBytes = Base64.decode(cleanBase64, Base64.DEFAULT)
val decodedBytes =
Base64.decode(
cleanBase64,
Base64.DEFAULT
)
BitmapFactory.decodeByteArray(
decodedBytes,
0,
@@ -970,12 +1152,20 @@ fun ReplyBubble(
val decoded =
try {
val cleanBase64 =
if (localBlob.contains(",")) {
localBlob.substringAfter(",")
if (localBlob.contains(",")
) {
localBlob
.substringAfter(
","
)
} else {
localBlob
}
val decodedBytes = Base64.decode(cleanBase64, Base64.DEFAULT)
val decodedBytes =
Base64.decode(
cleanBase64,
Base64.DEFAULT
)
BitmapFactory.decodeByteArray(
decodedBytes,
0,
@@ -986,8 +1176,7 @@ fun ReplyBubble(
}
imageBitmap = decoded
}
} catch (e: Exception) {
}
} catch (e: Exception) {}
}
}
}
@@ -1033,7 +1222,9 @@ fun ReplyBubble(
when {
replyData.text.isNotEmpty() -> replyData.text
hasImage -> "Photo"
replyData.attachments.any { it.type == AttachmentType.FILE } -> "File"
replyData.attachments.any {
it.type == AttachmentType.FILE
} -> "File"
else -> "..."
}
@@ -1066,7 +1257,8 @@ fun ReplyBubble(
} else if (blurPreviewBitmap != null) {
// Blurhash preview если картинка не загружена
Image(
bitmap = blurPreviewBitmap!!.asImageBitmap(),
bitmap =
blurPreviewBitmap!!.asImageBitmap(),
contentDescription = "Photo preview",
modifier = Modifier.fillMaxSize(),
contentScale = ContentScale.Crop
@@ -1080,7 +1272,10 @@ fun ReplyBubble(
Icon(
TablerIcons.Photo,
contentDescription = null,
tint = Color.White.copy(alpha = 0.7f),
tint =
Color.White.copy(
alpha = 0.7f
),
modifier = Modifier.size(20.dp)
)
}
@@ -1147,7 +1342,8 @@ private fun SkeletonBubble(
RoundedCornerShape(
topStart = 18.dp,
topEnd = 18.dp,
bottomStart = if (isOutgoing) 18.dp else 6.dp,
bottomStart =
if (isOutgoing) 18.dp else 6.dp,
bottomEnd = if (isOutgoing) 6.dp else 18.dp
)
)
@@ -1284,10 +1480,12 @@ fun ReplyImagePreview(
if (attachment.preview.isNotEmpty()) {
withContext(Dispatchers.IO) {
try {
// Получаем blurhash из preview (может быть в формате "tag::blurhash")
// Получаем blurhash из preview (может быть в формате
// "tag::blurhash")
val blurhash =
if (attachment.preview.contains("::")) {
attachment.preview.split("::").lastOrNull() ?: ""
attachment.preview.split("::").lastOrNull()
?: ""
} else {
attachment.preview
}
@@ -1311,12 +1509,23 @@ fun ReplyImagePreview(
val decoded =
try {
val cleanBase64 =
if (attachment.blob.contains(",")) {
attachment.blob.substringAfter(",")
if (attachment.blob
.contains(
","
)
) {
attachment.blob
.substringAfter(
","
)
} else {
attachment.blob
}
val decodedBytes = Base64.decode(cleanBase64, Base64.DEFAULT)
val decodedBytes =
Base64.decode(
cleanBase64,
Base64.DEFAULT
)
BitmapFactory.decodeByteArray(
decodedBytes,
0,
@@ -1344,12 +1553,20 @@ fun ReplyImagePreview(
val decoded =
try {
val cleanBase64 =
if (localBlob.contains(",")) {
localBlob.substringAfter(",")
if (localBlob.contains(",")
) {
localBlob
.substringAfter(
","
)
} else {
localBlob
}
val decodedBytes = Base64.decode(cleanBase64, Base64.DEFAULT)
val decodedBytes =
Base64.decode(
cleanBase64,
Base64.DEFAULT
)
BitmapFactory.decodeByteArray(
decodedBytes,
0,
@@ -1360,8 +1577,7 @@ fun ReplyImagePreview(
}
fullImageBitmap = decoded
}
} catch (e: Exception) {
}
} catch (e: Exception) {}
}
}
}
@@ -1389,7 +1605,10 @@ fun ReplyImagePreview(
)
} else {
// Placeholder с иконкой только если нет ничего
Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
Box(
modifier = Modifier.fillMaxSize(),
contentAlignment = Alignment.Center
) {
Icon(
TablerIcons.Photo,
contentDescription = null,