Refactor code structure for improved readability and maintainability
This commit is contained in:
@@ -4,10 +4,8 @@ import android.content.Context
|
|||||||
import androidx.compose.animation.*
|
import androidx.compose.animation.*
|
||||||
import androidx.compose.animation.core.*
|
import androidx.compose.animation.core.*
|
||||||
import androidx.compose.foundation.*
|
import androidx.compose.foundation.*
|
||||||
import androidx.compose.foundation.gestures.detectHorizontalDragGestures
|
|
||||||
import androidx.compose.foundation.gestures.awaitEachGesture
|
import androidx.compose.foundation.gestures.awaitEachGesture
|
||||||
import androidx.compose.foundation.gestures.awaitFirstDown
|
import androidx.compose.foundation.gestures.awaitFirstDown
|
||||||
import androidx.compose.foundation.gestures.horizontalDrag
|
|
||||||
import androidx.activity.compose.BackHandler
|
import androidx.activity.compose.BackHandler
|
||||||
import androidx.compose.foundation.layout.*
|
import androidx.compose.foundation.layout.*
|
||||||
import androidx.compose.foundation.lazy.LazyColumn
|
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.clip
|
||||||
import androidx.compose.ui.draw.clipToBounds
|
import androidx.compose.ui.draw.clipToBounds
|
||||||
import androidx.compose.ui.graphics.Color
|
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.pointerInput
|
||||||
import androidx.compose.ui.input.pointer.positionChange
|
import androidx.compose.ui.input.pointer.positionChange
|
||||||
import androidx.compose.ui.input.pointer.util.VelocityTracker
|
import androidx.compose.ui.input.pointer.util.VelocityTracker
|
||||||
@@ -1694,6 +1693,8 @@ fun SwipeableDialogItem(
|
|||||||
.background(backgroundColor)
|
.background(backgroundColor)
|
||||||
.pointerInput(Unit) {
|
.pointerInput(Unit) {
|
||||||
val velocityTracker = VelocityTracker()
|
val velocityTracker = VelocityTracker()
|
||||||
|
val touchSlop = viewConfiguration.touchSlop
|
||||||
|
|
||||||
awaitEachGesture {
|
awaitEachGesture {
|
||||||
val down = awaitFirstDown(requireUnconsumed = false)
|
val down = awaitFirstDown(requireUnconsumed = false)
|
||||||
|
|
||||||
@@ -1701,22 +1702,58 @@ fun SwipeableDialogItem(
|
|||||||
if (isDrawerOpen) return@awaitEachGesture
|
if (isDrawerOpen) return@awaitEachGesture
|
||||||
|
|
||||||
velocityTracker.resetTracking()
|
velocityTracker.resetTracking()
|
||||||
var started = false
|
var totalDragX = 0f
|
||||||
try {
|
var totalDragY = 0f
|
||||||
horizontalDrag(down.id) { change ->
|
var passedSlop = false
|
||||||
val dragAmount = change.positionChange().x
|
var claimed = false
|
||||||
|
|
||||||
// First movement determines direction
|
while (true) {
|
||||||
if (!started) {
|
val event = awaitPointerEvent()
|
||||||
// Swipe right with actions closed — let drawer handle it
|
val change = event.changes.firstOrNull { it.id == down.id }
|
||||||
if (dragAmount > 0 && offsetX == 0f) {
|
?: break
|
||||||
return@horizontalDrag
|
if (change.changedToUpIgnoreConsumed()) break
|
||||||
}
|
|
||||||
started = true
|
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()
|
onSwipeStarted()
|
||||||
|
change.consume()
|
||||||
}
|
}
|
||||||
|
// Horizontal right swipe with buttons open — close them
|
||||||
val newOffset = offsetX + dragAmount
|
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)
|
offsetX = newOffset.coerceIn(-swipeWidthPx, 0f)
|
||||||
velocityTracker.addPosition(
|
velocityTracker.addPosition(
|
||||||
change.uptimeMillis,
|
change.uptimeMillis,
|
||||||
@@ -1724,21 +1761,31 @@ fun SwipeableDialogItem(
|
|||||||
)
|
)
|
||||||
change.consume()
|
change.consume()
|
||||||
}
|
}
|
||||||
} catch (_: Exception) {
|
|
||||||
offsetX = 0f
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (started) {
|
// Snap animation
|
||||||
|
if (claimed) {
|
||||||
val velocity = velocityTracker.calculateVelocity().x
|
val velocity = velocityTracker.calculateVelocity().x
|
||||||
// Telegram-like: fling left (-velocity) OR dragged past 1/3
|
when {
|
||||||
val shouldOpen = velocity < -300f ||
|
// Rightward fling — always close
|
||||||
kotlin.math.abs(offsetX) > swipeWidthPx / 3
|
velocity > 150f -> {
|
||||||
if (shouldOpen) {
|
|
||||||
offsetX = -swipeWidthPx
|
|
||||||
} else {
|
|
||||||
offsetX = 0f
|
offsetX = 0f
|
||||||
onSwipeClosed()
|
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()
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,10 +11,7 @@ import androidx.compose.foundation.background
|
|||||||
import androidx.compose.foundation.border
|
import androidx.compose.foundation.border
|
||||||
import androidx.compose.foundation.clickable
|
import androidx.compose.foundation.clickable
|
||||||
import androidx.compose.foundation.combinedClickable
|
import androidx.compose.foundation.combinedClickable
|
||||||
import androidx.compose.foundation.gestures.awaitEachGesture
|
|
||||||
import androidx.compose.foundation.gestures.awaitFirstDown
|
|
||||||
import androidx.compose.foundation.gestures.detectHorizontalDragGestures
|
import androidx.compose.foundation.gestures.detectHorizontalDragGestures
|
||||||
import kotlin.math.abs
|
|
||||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||||
import androidx.compose.foundation.layout.*
|
import androidx.compose.foundation.layout.*
|
||||||
import androidx.compose.foundation.shape.CircleShape
|
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.graphicsLayer
|
||||||
import androidx.compose.ui.graphics.vector.ImageVector
|
import androidx.compose.ui.graphics.vector.ImageVector
|
||||||
import androidx.compose.ui.input.pointer.pointerInput
|
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.ContentScale
|
||||||
import androidx.compose.ui.layout.Layout
|
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.LocalConfiguration
|
||||||
|
import androidx.compose.ui.platform.LocalDensity
|
||||||
import androidx.compose.ui.text.font.FontWeight
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
import androidx.compose.ui.text.style.TextOverflow
|
import androidx.compose.ui.text.style.TextOverflow
|
||||||
import androidx.compose.ui.unit.IntOffset
|
import androidx.compose.ui.unit.IntOffset
|
||||||
@@ -56,6 +50,7 @@ import compose.icons.TablerIcons
|
|||||||
import compose.icons.tablericons.*
|
import compose.icons.tablericons.*
|
||||||
import java.text.SimpleDateFormat
|
import java.text.SimpleDateFormat
|
||||||
import java.util.*
|
import java.util.*
|
||||||
|
import kotlin.math.abs
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
|
|
||||||
@@ -65,10 +60,9 @@ import kotlinx.coroutines.withContext
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Telegram-style layout для текста сообщения с временем.
|
* Telegram-style layout для текста сообщения с временем. Если текст + время помещаются в одну
|
||||||
* Если текст + время помещаются в одну строку - располагает их рядом.
|
* строку - располагает их рядом. Если текст длинный и переносится - время встаёт в правый нижний
|
||||||
* Если текст длинный и переносится - время встаёт в правый нижний угол,
|
* угол, под последней строкой текста (как в Telegram).
|
||||||
* под последней строкой текста (как в Telegram).
|
|
||||||
*
|
*
|
||||||
* @param textContent Composable с текстом сообщения
|
* @param textContent Composable с текстом сообщения
|
||||||
* @param timeContent Composable с временем и статусом
|
* @param timeContent Composable с временем и статусом
|
||||||
@@ -94,13 +88,16 @@ fun TelegramStyleMessageContent(
|
|||||||
},
|
},
|
||||||
modifier = modifier
|
modifier = modifier
|
||||||
) { measurables, constraints ->
|
) { 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 textMeasurable = measurables[0]
|
||||||
val timeMeasurable = measurables[1]
|
val timeMeasurable = measurables[1]
|
||||||
|
|
||||||
// Измеряем время с минимальными constraints
|
// Измеряем время с минимальными 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)
|
val textConstraints = constraints.copy(minWidth = 0, minHeight = 0)
|
||||||
@@ -120,13 +117,15 @@ fun TelegramStyleMessageContent(
|
|||||||
// 🔥 Для caption - занимаем всю доступную ширину
|
// 🔥 Для caption - занимаем всю доступную ширину
|
||||||
val w = constraints.maxWidth
|
val w = constraints.maxWidth
|
||||||
|
|
||||||
// 🔥 ИСПРАВЛЕНИЕ: Проверяем помещается ли текст + время в одну строку
|
// 🔥 ИСПРАВЛЕНИЕ: Проверяем помещается ли текст + время в одну
|
||||||
|
// строку
|
||||||
val availableForTime = w - textWidth - spacingPx
|
val availableForTime = w - textWidth - spacingPx
|
||||||
if (availableForTime >= timeWidth) {
|
if (availableForTime >= timeWidth) {
|
||||||
// Текст и время помещаются - время справа на одной линии
|
// Текст и время помещаются - время справа на одной линии
|
||||||
val h = maxOf(textPlaceable.height, timePlaceable.height)
|
val h = maxOf(textPlaceable.height, timePlaceable.height)
|
||||||
val tX = w - timeWidth // Время справа
|
val tX = w - timeWidth // Время справа
|
||||||
val tY = h - timePlaceable.height // Время внизу строки (выровнено по baseline)
|
val tY = h - timePlaceable.height // Время внизу строки
|
||||||
|
// (выровнено по baseline)
|
||||||
LayoutResult(w, h, tX, tY)
|
LayoutResult(w, h, tX, tY)
|
||||||
} else {
|
} else {
|
||||||
// Текст длинный - время на новой строке справа внизу
|
// Текст длинный - время на новой строке справа внизу
|
||||||
@@ -323,9 +322,13 @@ fun MessageBubble(
|
|||||||
topEnd = TelegramBubbleSpec.bubbleRadius,
|
topEnd = TelegramBubbleSpec.bubbleRadius,
|
||||||
bottomStart =
|
bottomStart =
|
||||||
if (message.isOutgoing) TelegramBubbleSpec.bubbleRadius
|
if (message.isOutgoing) TelegramBubbleSpec.bubbleRadius
|
||||||
else (if (showTail) TelegramBubbleSpec.nearRadius else TelegramBubbleSpec.bubbleRadius),
|
else
|
||||||
|
(if (showTail) TelegramBubbleSpec.nearRadius
|
||||||
|
else TelegramBubbleSpec.bubbleRadius),
|
||||||
bottomEnd =
|
bottomEnd =
|
||||||
if (message.isOutgoing) (if (showTail) TelegramBubbleSpec.nearRadius else TelegramBubbleSpec.bubbleRadius)
|
if (message.isOutgoing)
|
||||||
|
(if (showTail) TelegramBubbleSpec.nearRadius
|
||||||
|
else TelegramBubbleSpec.bubbleRadius)
|
||||||
else TelegramBubbleSpec.bubbleRadius
|
else TelegramBubbleSpec.bubbleRadius
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -336,9 +339,10 @@ fun MessageBubble(
|
|||||||
modifier =
|
modifier =
|
||||||
Modifier.fillMaxWidth().pointerInput(Unit) {
|
Modifier.fillMaxWidth().pointerInput(Unit) {
|
||||||
// 🔥 Простой горизонтальный свайп для reply
|
// 🔥 Простой горизонтальный свайп для reply
|
||||||
// Используем detectHorizontalDragGestures который лучше работает со скроллом
|
// Используем detectHorizontalDragGestures который лучше работает со
|
||||||
|
// скроллом
|
||||||
detectHorizontalDragGestures(
|
detectHorizontalDragGestures(
|
||||||
onDragStart = { },
|
onDragStart = {},
|
||||||
onDragEnd = {
|
onDragEnd = {
|
||||||
// Если свайпнули достаточно влево - reply
|
// Если свайпнули достаточно влево - reply
|
||||||
if (swipeOffset <= -swipeThreshold) {
|
if (swipeOffset <= -swipeThreshold) {
|
||||||
@@ -346,15 +350,14 @@ fun MessageBubble(
|
|||||||
}
|
}
|
||||||
swipeOffset = 0f
|
swipeOffset = 0f
|
||||||
},
|
},
|
||||||
onDragCancel = {
|
onDragCancel = { swipeOffset = 0f },
|
||||||
swipeOffset = 0f
|
|
||||||
},
|
|
||||||
onHorizontalDrag = { change, dragAmount ->
|
onHorizontalDrag = { change, dragAmount ->
|
||||||
// Только свайп влево (отрицательное значение)
|
// Только свайп влево (отрицательное значение)
|
||||||
if (dragAmount < 0 || swipeOffset < 0) {
|
if (dragAmount < 0 || swipeOffset < 0) {
|
||||||
change.consume()
|
change.consume()
|
||||||
val newOffset = swipeOffset + dragAmount
|
val newOffset = swipeOffset + dragAmount
|
||||||
swipeOffset = newOffset.coerceIn(-maxSwipe, 0f)
|
swipeOffset =
|
||||||
|
newOffset.coerceIn(-maxSwipe, 0f)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
@@ -363,7 +366,9 @@ fun MessageBubble(
|
|||||||
// 🔥 Reply icon - справа, появляется при свайпе влево
|
// 🔥 Reply icon - справа, появляется при свайпе влево
|
||||||
Box(
|
Box(
|
||||||
modifier =
|
modifier =
|
||||||
Modifier.align(Alignment.CenterEnd).padding(end = 16.dp).graphicsLayer {
|
Modifier.align(Alignment.CenterEnd)
|
||||||
|
.padding(end = 16.dp)
|
||||||
|
.graphicsLayer {
|
||||||
alpha = swipeProgress
|
alpha = swipeProgress
|
||||||
scaleX = swipeProgress
|
scaleX = swipeProgress
|
||||||
scaleY = swipeProgress
|
scaleY = swipeProgress
|
||||||
@@ -466,7 +471,9 @@ fun MessageBubble(
|
|||||||
message.text.isEmpty() &&
|
message.text.isEmpty() &&
|
||||||
message.replyData == null &&
|
message.replyData == null &&
|
||||||
message.attachments.all {
|
message.attachments.all {
|
||||||
it.type == com.rosetta.messenger.network.AttachmentType.IMAGE
|
it.type ==
|
||||||
|
com.rosetta.messenger.network.AttachmentType
|
||||||
|
.IMAGE
|
||||||
}
|
}
|
||||||
|
|
||||||
// Фото + caption (как в Telegram)
|
// Фото + caption (как в Telegram)
|
||||||
@@ -475,7 +482,9 @@ fun MessageBubble(
|
|||||||
message.text.isNotEmpty() &&
|
message.text.isNotEmpty() &&
|
||||||
message.replyData == null &&
|
message.replyData == null &&
|
||||||
message.attachments.all {
|
message.attachments.all {
|
||||||
it.type == com.rosetta.messenger.network.AttachmentType.IMAGE
|
it.type ==
|
||||||
|
com.rosetta.messenger.network.AttachmentType
|
||||||
|
.IMAGE
|
||||||
}
|
}
|
||||||
|
|
||||||
// Для сообщений только с фото - минимальный padding и тонкий border
|
// Для сообщений только с фото - минимальный padding и тонкий border
|
||||||
@@ -495,25 +504,47 @@ fun MessageBubble(
|
|||||||
val maxPhotoWidth = TelegramBubbleSpec.maxPhotoWidth(screenWidthDp).dp
|
val maxPhotoWidth = TelegramBubbleSpec.maxPhotoWidth(screenWidthDp).dp
|
||||||
|
|
||||||
// Вычисляем ширину фото для ограничения пузырька
|
// Вычисляем ширину фото для ограничения пузырька
|
||||||
val photoWidth = if (hasImageWithCaption || hasOnlyMedia) {
|
val photoWidth =
|
||||||
val firstImage = message.attachments.firstOrNull {
|
if (hasImageWithCaption || hasOnlyMedia) {
|
||||||
it.type == com.rosetta.messenger.network.AttachmentType.IMAGE
|
val firstImage =
|
||||||
|
message.attachments.firstOrNull {
|
||||||
|
it.type ==
|
||||||
|
com.rosetta.messenger.network
|
||||||
|
.AttachmentType.IMAGE
|
||||||
}
|
}
|
||||||
if (firstImage != null && firstImage.width > 0 && firstImage.height > 0) {
|
if (firstImage != null &&
|
||||||
val ar = firstImage.width.toFloat() / firstImage.height.toFloat()
|
firstImage.width > 0 &&
|
||||||
val maxW = TelegramBubbleSpec.maxPhotoWidth(screenWidthDp).toFloat()
|
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 w = if (ar >= 1f) maxW else maxW * 0.75f
|
||||||
var h = w / ar
|
var h = w / ar
|
||||||
|
|
||||||
if (h > TelegramBubbleSpec.maxPhotoHeight) {
|
if (h > TelegramBubbleSpec.maxPhotoHeight) {
|
||||||
h = TelegramBubbleSpec.maxPhotoHeight.toFloat()
|
h =
|
||||||
|
TelegramBubbleSpec.maxPhotoHeight
|
||||||
|
.toFloat()
|
||||||
w = h * ar
|
w = h * ar
|
||||||
}
|
}
|
||||||
if (h < TelegramBubbleSpec.minPhotoHeight) {
|
if (h < TelegramBubbleSpec.minPhotoHeight) {
|
||||||
h = TelegramBubbleSpec.minPhotoHeight.toFloat()
|
h =
|
||||||
|
TelegramBubbleSpec.minPhotoHeight
|
||||||
|
.toFloat()
|
||||||
w = h * ar
|
w = h * ar
|
||||||
}
|
}
|
||||||
w.coerceIn(TelegramBubbleSpec.minPhotoWidth.toFloat(), maxW).dp
|
w.coerceIn(
|
||||||
|
TelegramBubbleSpec.minPhotoWidth
|
||||||
|
.toFloat(),
|
||||||
|
maxW
|
||||||
|
)
|
||||||
|
.dp
|
||||||
} else {
|
} else {
|
||||||
maxPhotoWidth
|
maxPhotoWidth
|
||||||
}
|
}
|
||||||
@@ -521,8 +552,12 @@ fun MessageBubble(
|
|||||||
280.dp
|
280.dp
|
||||||
}
|
}
|
||||||
|
|
||||||
val bubbleWidthModifier = if (hasImageWithCaption || hasOnlyMedia) {
|
val bubbleWidthModifier =
|
||||||
Modifier.width(photoWidth) // 🔥 Фиксированная ширина = размер фото (убирает лишний отступ)
|
if (hasImageWithCaption || hasOnlyMedia) {
|
||||||
|
Modifier.width(
|
||||||
|
photoWidth
|
||||||
|
) // 🔥 Фиксированная ширина = размер фото (убирает лишний
|
||||||
|
// отступ)
|
||||||
} else {
|
} else {
|
||||||
Modifier.widthIn(min = 60.dp, max = 280.dp)
|
Modifier.widthIn(min = 60.dp, max = 280.dp)
|
||||||
}
|
}
|
||||||
@@ -539,7 +574,9 @@ fun MessageBubble(
|
|||||||
.combinedClickable(
|
.combinedClickable(
|
||||||
indication = null,
|
indication = null,
|
||||||
interactionSource =
|
interactionSource =
|
||||||
remember { MutableInteractionSource() },
|
remember {
|
||||||
|
MutableInteractionSource()
|
||||||
|
},
|
||||||
onClick = onClick,
|
onClick = onClick,
|
||||||
onLongClick = onLongClick
|
onLongClick = onLongClick
|
||||||
)
|
)
|
||||||
@@ -549,16 +586,26 @@ fun MessageBubble(
|
|||||||
Modifier.border(
|
Modifier.border(
|
||||||
width = bubbleBorderWidth,
|
width = bubbleBorderWidth,
|
||||||
color =
|
color =
|
||||||
if (message.isOutgoing) {
|
if (message.isOutgoing
|
||||||
Color.White.copy(alpha = 0.15f)
|
) {
|
||||||
|
Color.White
|
||||||
|
.copy(
|
||||||
|
alpha =
|
||||||
|
0.15f
|
||||||
|
)
|
||||||
} else {
|
} else {
|
||||||
if (isDarkTheme)
|
if (isDarkTheme
|
||||||
Color.White.copy(
|
)
|
||||||
alpha = 0.1f
|
Color.White
|
||||||
|
.copy(
|
||||||
|
alpha =
|
||||||
|
0.1f
|
||||||
)
|
)
|
||||||
else
|
else
|
||||||
Color.Black.copy(
|
Color.Black
|
||||||
alpha = 0.08f
|
.copy(
|
||||||
|
alpha =
|
||||||
|
0.08f
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
shape = bubbleShape
|
shape = bubbleShape
|
||||||
@@ -583,7 +630,8 @@ fun MessageBubble(
|
|||||||
// 📎 Attachments (IMAGE, FILE, AVATAR)
|
// 📎 Attachments (IMAGE, FILE, AVATAR)
|
||||||
if (message.attachments.isNotEmpty()) {
|
if (message.attachments.isNotEmpty()) {
|
||||||
val attachmentDisplayStatus =
|
val attachmentDisplayStatus =
|
||||||
if (isSavedMessages) MessageStatus.READ else message.status
|
if (isSavedMessages) MessageStatus.READ
|
||||||
|
else message.status
|
||||||
MessageAttachments(
|
MessageAttachments(
|
||||||
attachments = message.attachments,
|
attachments = message.attachments,
|
||||||
chachaKey = message.chachaKey,
|
chachaKey = message.chachaKey,
|
||||||
@@ -596,28 +644,42 @@ fun MessageBubble(
|
|||||||
avatarRepository = avatarRepository,
|
avatarRepository = avatarRepository,
|
||||||
currentUserPublicKey = currentUserPublicKey,
|
currentUserPublicKey = currentUserPublicKey,
|
||||||
hasCaption = hasImageWithCaption,
|
hasCaption = hasImageWithCaption,
|
||||||
showTail = showTail, // Передаём для формы пузырька
|
showTail = showTail, // Передаём для формы
|
||||||
|
// пузырька
|
||||||
onImageClick = onImageClick
|
onImageClick = onImageClick
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 🖼️ Caption под фото (Telegram-style)
|
// 🖼️ Caption под фото (Telegram-style)
|
||||||
// Отступ от фото = 9dp, слева = 11dp (исходящие) / 13dp (входящие)
|
// Отступ от фото = 9dp, слева = 11dp (исходящие) / 13dp
|
||||||
|
// (входящие)
|
||||||
if (hasImageWithCaption) {
|
if (hasImageWithCaption) {
|
||||||
val captionPaddingStart = if (message.isOutgoing)
|
val captionPaddingStart =
|
||||||
TelegramBubbleSpec.captionPaddingStartOutgoing
|
if (message.isOutgoing)
|
||||||
|
TelegramBubbleSpec
|
||||||
|
.captionPaddingStartOutgoing
|
||||||
else
|
else
|
||||||
TelegramBubbleSpec.captionPaddingStartIncoming
|
TelegramBubbleSpec
|
||||||
|
.captionPaddingStartIncoming
|
||||||
Box(
|
Box(
|
||||||
modifier =
|
modifier =
|
||||||
Modifier
|
Modifier.fillMaxWidth() // 🔥
|
||||||
.fillMaxWidth() // 🔥 Растягиваем на ширину фото для правильного позиционирования времени
|
// Растягиваем на ширину
|
||||||
|
// фото для правильного
|
||||||
|
// позиционирования времени
|
||||||
.background(bubbleColor)
|
.background(bubbleColor)
|
||||||
.padding(
|
.padding(
|
||||||
start = captionPaddingStart,
|
start =
|
||||||
end = TelegramBubbleSpec.captionPaddingEnd,
|
captionPaddingStart,
|
||||||
top = TelegramBubbleSpec.captionPaddingTop,
|
end =
|
||||||
bottom = TelegramBubbleSpec.captionPaddingBottom
|
TelegramBubbleSpec
|
||||||
|
.captionPaddingEnd,
|
||||||
|
top =
|
||||||
|
TelegramBubbleSpec
|
||||||
|
.captionPaddingTop,
|
||||||
|
bottom =
|
||||||
|
TelegramBubbleSpec
|
||||||
|
.captionPaddingBottom
|
||||||
)
|
)
|
||||||
) {
|
) {
|
||||||
TelegramStyleMessageContent(
|
TelegramStyleMessageContent(
|
||||||
@@ -626,48 +688,82 @@ fun MessageBubble(
|
|||||||
text = message.text,
|
text = message.text,
|
||||||
color = textColor,
|
color = textColor,
|
||||||
fontSize = 16.sp,
|
fontSize = 16.sp,
|
||||||
linkColor = linkColor,
|
linkColor =
|
||||||
onLongClick = onLongClick // 🔥 Long press для selection
|
linkColor,
|
||||||
|
onLongClick =
|
||||||
|
onLongClick // 🔥 Long press для selection
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
timeContent = {
|
timeContent = {
|
||||||
Row(
|
Row(
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
verticalAlignment =
|
||||||
horizontalArrangement = Arrangement.spacedBy(2.dp)
|
Alignment
|
||||||
|
.CenterVertically,
|
||||||
|
horizontalArrangement =
|
||||||
|
Arrangement
|
||||||
|
.spacedBy(
|
||||||
|
2.dp
|
||||||
|
)
|
||||||
) {
|
) {
|
||||||
Text(
|
Text(
|
||||||
text = timeFormat.format(message.timestamp),
|
text =
|
||||||
color = timeColor,
|
timeFormat
|
||||||
fontSize = 11.sp,
|
.format(
|
||||||
|
message.timestamp
|
||||||
|
),
|
||||||
|
color =
|
||||||
|
timeColor,
|
||||||
|
fontSize =
|
||||||
|
11.sp,
|
||||||
fontStyle =
|
fontStyle =
|
||||||
androidx.compose.ui.text.font.FontStyle
|
androidx.compose
|
||||||
|
.ui
|
||||||
|
.text
|
||||||
|
.font
|
||||||
|
.FontStyle
|
||||||
.Italic
|
.Italic
|
||||||
)
|
)
|
||||||
if (message.isOutgoing) {
|
if (message.isOutgoing
|
||||||
|
) {
|
||||||
val displayStatus =
|
val displayStatus =
|
||||||
if (isSavedMessages) MessageStatus.READ
|
if (isSavedMessages
|
||||||
else message.status
|
)
|
||||||
|
MessageStatus
|
||||||
|
.READ
|
||||||
|
else
|
||||||
|
message.status
|
||||||
AnimatedMessageStatus(
|
AnimatedMessageStatus(
|
||||||
status = displayStatus,
|
status =
|
||||||
timeColor = timeColor,
|
displayStatus,
|
||||||
timestamp = message.timestamp.time,
|
timeColor =
|
||||||
onRetry = onRetry,
|
timeColor,
|
||||||
onDelete = onDelete
|
timestamp =
|
||||||
|
message.timestamp
|
||||||
|
.time,
|
||||||
|
onRetry =
|
||||||
|
onRetry,
|
||||||
|
onDelete =
|
||||||
|
onDelete
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
spacing = 8,
|
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))
|
Spacer(modifier = Modifier.height(8.dp))
|
||||||
}
|
}
|
||||||
|
|
||||||
// Если есть reply - Telegram-style layout
|
// Если есть reply - Telegram-style layout
|
||||||
if (message.replyData != null && message.text.isNotEmpty()) {
|
if (message.replyData != null && message.text.isNotEmpty()
|
||||||
|
) {
|
||||||
TelegramStyleMessageContent(
|
TelegramStyleMessageContent(
|
||||||
textContent = {
|
textContent = {
|
||||||
AppleEmojiText(
|
AppleEmojiText(
|
||||||
@@ -675,39 +771,74 @@ fun MessageBubble(
|
|||||||
color = textColor,
|
color = textColor,
|
||||||
fontSize = 17.sp,
|
fontSize = 17.sp,
|
||||||
linkColor = linkColor,
|
linkColor = linkColor,
|
||||||
onLongClick = onLongClick // 🔥 Long press для selection
|
onLongClick =
|
||||||
|
onLongClick // 🔥
|
||||||
|
// Long
|
||||||
|
// press
|
||||||
|
// для
|
||||||
|
// selection
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
timeContent = {
|
timeContent = {
|
||||||
Row(
|
Row(
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
verticalAlignment =
|
||||||
horizontalArrangement = Arrangement.spacedBy(2.dp)
|
Alignment
|
||||||
|
.CenterVertically,
|
||||||
|
horizontalArrangement =
|
||||||
|
Arrangement
|
||||||
|
.spacedBy(
|
||||||
|
2.dp
|
||||||
|
)
|
||||||
) {
|
) {
|
||||||
Text(
|
Text(
|
||||||
text = timeFormat.format(message.timestamp),
|
text =
|
||||||
|
timeFormat
|
||||||
|
.format(
|
||||||
|
message.timestamp
|
||||||
|
),
|
||||||
color = timeColor,
|
color = timeColor,
|
||||||
fontSize = 11.sp,
|
fontSize = 11.sp,
|
||||||
fontStyle =
|
fontStyle =
|
||||||
androidx.compose.ui.text.font.FontStyle.Italic
|
androidx.compose
|
||||||
|
.ui
|
||||||
|
.text
|
||||||
|
.font
|
||||||
|
.FontStyle
|
||||||
|
.Italic
|
||||||
)
|
)
|
||||||
if (message.isOutgoing) {
|
if (message.isOutgoing) {
|
||||||
val displayStatus =
|
val displayStatus =
|
||||||
if (isSavedMessages) MessageStatus.READ
|
if (isSavedMessages
|
||||||
else message.status
|
)
|
||||||
|
MessageStatus
|
||||||
|
.READ
|
||||||
|
else
|
||||||
|
message.status
|
||||||
AnimatedMessageStatus(
|
AnimatedMessageStatus(
|
||||||
status = displayStatus,
|
status =
|
||||||
timeColor = timeColor,
|
displayStatus,
|
||||||
timestamp = message.timestamp.time,
|
timeColor =
|
||||||
onRetry = onRetry,
|
timeColor,
|
||||||
onDelete = onDelete
|
timestamp =
|
||||||
|
message.timestamp
|
||||||
|
.time,
|
||||||
|
onRetry =
|
||||||
|
onRetry,
|
||||||
|
onDelete =
|
||||||
|
onDelete
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
fillWidth = true // 🔥 Время справа с учётом ширины ReplyBubble
|
fillWidth = true // 🔥 Время справа с учётом
|
||||||
|
// ширины ReplyBubble
|
||||||
)
|
)
|
||||||
} else if (!hasOnlyMedia && !hasImageWithCaption && message.text.isNotEmpty()) {
|
} else if (!hasOnlyMedia &&
|
||||||
// Telegram-style: текст + время с автоматическим переносом
|
!hasImageWithCaption &&
|
||||||
|
message.text.isNotEmpty()
|
||||||
|
) {
|
||||||
|
// Telegram-style: текст + время с автоматическим
|
||||||
|
// переносом
|
||||||
TelegramStyleMessageContent(
|
TelegramStyleMessageContent(
|
||||||
textContent = {
|
textContent = {
|
||||||
AppleEmojiText(
|
AppleEmojiText(
|
||||||
@@ -715,31 +846,61 @@ fun MessageBubble(
|
|||||||
color = textColor,
|
color = textColor,
|
||||||
fontSize = 17.sp,
|
fontSize = 17.sp,
|
||||||
linkColor = linkColor,
|
linkColor = linkColor,
|
||||||
onLongClick = onLongClick // 🔥 Long press для selection
|
onLongClick =
|
||||||
|
onLongClick // 🔥
|
||||||
|
// Long
|
||||||
|
// press
|
||||||
|
// для
|
||||||
|
// selection
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
timeContent = {
|
timeContent = {
|
||||||
Row(
|
Row(
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
verticalAlignment =
|
||||||
horizontalArrangement = Arrangement.spacedBy(2.dp)
|
Alignment
|
||||||
|
.CenterVertically,
|
||||||
|
horizontalArrangement =
|
||||||
|
Arrangement
|
||||||
|
.spacedBy(
|
||||||
|
2.dp
|
||||||
|
)
|
||||||
) {
|
) {
|
||||||
Text(
|
Text(
|
||||||
text = timeFormat.format(message.timestamp),
|
text =
|
||||||
|
timeFormat
|
||||||
|
.format(
|
||||||
|
message.timestamp
|
||||||
|
),
|
||||||
color = timeColor,
|
color = timeColor,
|
||||||
fontSize = 11.sp,
|
fontSize = 11.sp,
|
||||||
fontStyle =
|
fontStyle =
|
||||||
androidx.compose.ui.text.font.FontStyle.Italic
|
androidx.compose
|
||||||
|
.ui
|
||||||
|
.text
|
||||||
|
.font
|
||||||
|
.FontStyle
|
||||||
|
.Italic
|
||||||
)
|
)
|
||||||
if (message.isOutgoing) {
|
if (message.isOutgoing) {
|
||||||
val displayStatus =
|
val displayStatus =
|
||||||
if (isSavedMessages) MessageStatus.READ
|
if (isSavedMessages
|
||||||
else message.status
|
)
|
||||||
|
MessageStatus
|
||||||
|
.READ
|
||||||
|
else
|
||||||
|
message.status
|
||||||
AnimatedMessageStatus(
|
AnimatedMessageStatus(
|
||||||
status = displayStatus,
|
status =
|
||||||
timeColor = timeColor,
|
displayStatus,
|
||||||
timestamp = message.timestamp.time,
|
timeColor =
|
||||||
onRetry = onRetry,
|
timeColor,
|
||||||
onDelete = onDelete
|
timestamp =
|
||||||
|
message.timestamp
|
||||||
|
.time,
|
||||||
|
onRetry =
|
||||||
|
onRetry,
|
||||||
|
onDelete =
|
||||||
|
onDelete
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -762,7 +923,9 @@ fun AnimatedMessageStatus(
|
|||||||
onDelete: () -> Unit = {}
|
onDelete: () -> Unit = {}
|
||||||
) {
|
) {
|
||||||
val isTimedOut =
|
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 effectiveStatus = if (isTimedOut) MessageStatus.ERROR else status
|
||||||
|
|
||||||
val targetColor =
|
val targetColor =
|
||||||
@@ -827,13 +990,18 @@ fun AnimatedMessageStatus(
|
|||||||
.scale(scale)
|
.scale(scale)
|
||||||
.then(
|
.then(
|
||||||
if (currentStatus == MessageStatus.ERROR) {
|
if (currentStatus == MessageStatus.ERROR) {
|
||||||
Modifier.clickable { showErrorMenu = true }
|
Modifier.clickable {
|
||||||
|
showErrorMenu = true
|
||||||
|
}
|
||||||
} else Modifier
|
} else Modifier
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
DropdownMenu(expanded = showErrorMenu, onDismissRequest = { showErrorMenu = false }) {
|
DropdownMenu(
|
||||||
|
expanded = showErrorMenu,
|
||||||
|
onDismissRequest = { showErrorMenu = false }
|
||||||
|
) {
|
||||||
DropdownMenuItem(
|
DropdownMenuItem(
|
||||||
text = { Text("Retry") },
|
text = { Text("Retry") },
|
||||||
onClick = {
|
onClick = {
|
||||||
@@ -906,19 +1074,22 @@ fun ReplyBubble(
|
|||||||
if (imageAttachment != null && imageAttachment.preview.isNotEmpty()) {
|
if (imageAttachment != null && imageAttachment.preview.isNotEmpty()) {
|
||||||
withContext(Dispatchers.IO) {
|
withContext(Dispatchers.IO) {
|
||||||
try {
|
try {
|
||||||
// Получаем blurhash из preview (может быть в формате "tag::blurhash")
|
// Получаем blurhash из preview (может быть в формате
|
||||||
|
// "tag::blurhash")
|
||||||
val blurhash =
|
val blurhash =
|
||||||
if (imageAttachment.preview.contains("::")) {
|
if (imageAttachment.preview.contains("::")) {
|
||||||
imageAttachment.preview.substringAfter("::")
|
imageAttachment.preview.substringAfter("::")
|
||||||
} else if (!imageAttachment.preview.startsWith("http") &&
|
} else if (!imageAttachment.preview.startsWith(
|
||||||
imageAttachment.preview.length < 50
|
"http"
|
||||||
|
) && imageAttachment.preview.length < 50
|
||||||
) {
|
) {
|
||||||
imageAttachment.preview
|
imageAttachment.preview
|
||||||
} else {
|
} else {
|
||||||
""
|
""
|
||||||
}
|
}
|
||||||
if (blurhash.isNotEmpty()) {
|
if (blurhash.isNotEmpty()) {
|
||||||
blurPreviewBitmap = BlurHash.decode(blurhash, 36, 36)
|
blurPreviewBitmap =
|
||||||
|
BlurHash.decode(blurhash, 36, 36)
|
||||||
}
|
}
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
// Ignore blurhash decode errors
|
// Ignore blurhash decode errors
|
||||||
@@ -937,12 +1108,23 @@ fun ReplyBubble(
|
|||||||
val decoded =
|
val decoded =
|
||||||
try {
|
try {
|
||||||
val cleanBase64 =
|
val cleanBase64 =
|
||||||
if (imageAttachment.blob.contains(",")) {
|
if (imageAttachment.blob
|
||||||
imageAttachment.blob.substringAfter(",")
|
.contains(
|
||||||
|
","
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
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(
|
BitmapFactory.decodeByteArray(
|
||||||
decodedBytes,
|
decodedBytes,
|
||||||
0,
|
0,
|
||||||
@@ -970,12 +1152,20 @@ fun ReplyBubble(
|
|||||||
val decoded =
|
val decoded =
|
||||||
try {
|
try {
|
||||||
val cleanBase64 =
|
val cleanBase64 =
|
||||||
if (localBlob.contains(",")) {
|
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(
|
BitmapFactory.decodeByteArray(
|
||||||
decodedBytes,
|
decodedBytes,
|
||||||
0,
|
0,
|
||||||
@@ -986,8 +1176,7 @@ fun ReplyBubble(
|
|||||||
}
|
}
|
||||||
imageBitmap = decoded
|
imageBitmap = decoded
|
||||||
}
|
}
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1033,7 +1222,9 @@ fun ReplyBubble(
|
|||||||
when {
|
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"
|
||||||
else -> "..."
|
else -> "..."
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1066,7 +1257,8 @@ fun ReplyBubble(
|
|||||||
} else if (blurPreviewBitmap != null) {
|
} else if (blurPreviewBitmap != null) {
|
||||||
// Blurhash preview если картинка не загружена
|
// Blurhash preview если картинка не загружена
|
||||||
Image(
|
Image(
|
||||||
bitmap = blurPreviewBitmap!!.asImageBitmap(),
|
bitmap =
|
||||||
|
blurPreviewBitmap!!.asImageBitmap(),
|
||||||
contentDescription = "Photo preview",
|
contentDescription = "Photo preview",
|
||||||
modifier = Modifier.fillMaxSize(),
|
modifier = Modifier.fillMaxSize(),
|
||||||
contentScale = ContentScale.Crop
|
contentScale = ContentScale.Crop
|
||||||
@@ -1080,7 +1272,10 @@ fun ReplyBubble(
|
|||||||
Icon(
|
Icon(
|
||||||
TablerIcons.Photo,
|
TablerIcons.Photo,
|
||||||
contentDescription = null,
|
contentDescription = null,
|
||||||
tint = Color.White.copy(alpha = 0.7f),
|
tint =
|
||||||
|
Color.White.copy(
|
||||||
|
alpha = 0.7f
|
||||||
|
),
|
||||||
modifier = Modifier.size(20.dp)
|
modifier = Modifier.size(20.dp)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -1147,7 +1342,8 @@ private fun SkeletonBubble(
|
|||||||
RoundedCornerShape(
|
RoundedCornerShape(
|
||||||
topStart = 18.dp,
|
topStart = 18.dp,
|
||||||
topEnd = 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
|
bottomEnd = if (isOutgoing) 6.dp else 18.dp
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
@@ -1284,10 +1480,12 @@ fun ReplyImagePreview(
|
|||||||
if (attachment.preview.isNotEmpty()) {
|
if (attachment.preview.isNotEmpty()) {
|
||||||
withContext(Dispatchers.IO) {
|
withContext(Dispatchers.IO) {
|
||||||
try {
|
try {
|
||||||
// Получаем blurhash из preview (может быть в формате "tag::blurhash")
|
// Получаем blurhash из preview (может быть в формате
|
||||||
|
// "tag::blurhash")
|
||||||
val blurhash =
|
val blurhash =
|
||||||
if (attachment.preview.contains("::")) {
|
if (attachment.preview.contains("::")) {
|
||||||
attachment.preview.split("::").lastOrNull() ?: ""
|
attachment.preview.split("::").lastOrNull()
|
||||||
|
?: ""
|
||||||
} else {
|
} else {
|
||||||
attachment.preview
|
attachment.preview
|
||||||
}
|
}
|
||||||
@@ -1311,12 +1509,23 @@ fun ReplyImagePreview(
|
|||||||
val decoded =
|
val decoded =
|
||||||
try {
|
try {
|
||||||
val cleanBase64 =
|
val cleanBase64 =
|
||||||
if (attachment.blob.contains(",")) {
|
if (attachment.blob
|
||||||
attachment.blob.substringAfter(",")
|
.contains(
|
||||||
|
","
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
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(
|
BitmapFactory.decodeByteArray(
|
||||||
decodedBytes,
|
decodedBytes,
|
||||||
0,
|
0,
|
||||||
@@ -1344,12 +1553,20 @@ fun ReplyImagePreview(
|
|||||||
val decoded =
|
val decoded =
|
||||||
try {
|
try {
|
||||||
val cleanBase64 =
|
val cleanBase64 =
|
||||||
if (localBlob.contains(",")) {
|
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(
|
BitmapFactory.decodeByteArray(
|
||||||
decodedBytes,
|
decodedBytes,
|
||||||
0,
|
0,
|
||||||
@@ -1360,8 +1577,7 @@ fun ReplyImagePreview(
|
|||||||
}
|
}
|
||||||
fullImageBitmap = decoded
|
fullImageBitmap = decoded
|
||||||
}
|
}
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1389,7 +1605,10 @@ fun ReplyImagePreview(
|
|||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
// Placeholder с иконкой только если нет ничего
|
// Placeholder с иконкой только если нет ничего
|
||||||
Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
|
Box(
|
||||||
|
modifier = Modifier.fillMaxSize(),
|
||||||
|
contentAlignment = Alignment.Center
|
||||||
|
) {
|
||||||
Icon(
|
Icon(
|
||||||
TablerIcons.Photo,
|
TablerIcons.Photo,
|
||||||
contentDescription = null,
|
contentDescription = null,
|
||||||
|
|||||||
Reference in New Issue
Block a user