fix: update swipe direction for reply functionality in MessageBubble
This commit is contained in:
@@ -321,8 +321,8 @@ fun MessageBubble(
|
|||||||
detectHorizontalDragGestures(
|
detectHorizontalDragGestures(
|
||||||
onDragStart = { },
|
onDragStart = { },
|
||||||
onDragEnd = {
|
onDragEnd = {
|
||||||
// Если свайпнули достаточно вправо - reply
|
// Если свайпнули достаточно влево - reply
|
||||||
if (swipeOffset >= swipeThreshold) {
|
if (swipeOffset <= -swipeThreshold) {
|
||||||
onSwipeToReply()
|
onSwipeToReply()
|
||||||
}
|
}
|
||||||
swipeOffset = 0f
|
swipeOffset = 0f
|
||||||
@@ -331,20 +331,20 @@ fun MessageBubble(
|
|||||||
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(0f, maxSwipe)
|
swipeOffset = newOffset.coerceIn(-maxSwipe, 0f)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
) {
|
) {
|
||||||
// 🔥 Reply icon - слева, появляется при свайпе вправо
|
// 🔥 Reply icon - справа, появляется при свайпе влево
|
||||||
Box(
|
Box(
|
||||||
modifier =
|
modifier =
|
||||||
Modifier.align(Alignment.CenterStart).padding(start = 16.dp).graphicsLayer {
|
Modifier.align(Alignment.CenterEnd).padding(end = 16.dp).graphicsLayer {
|
||||||
alpha = swipeProgress
|
alpha = swipeProgress
|
||||||
scaleX = swipeProgress
|
scaleX = swipeProgress
|
||||||
scaleY = swipeProgress
|
scaleY = swipeProgress
|
||||||
|
|||||||
@@ -1158,13 +1158,27 @@ private fun TelegramCaptionBar(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Save edited image and return the URI - returns original if no edits, otherwise crops black bars */
|
/** Save edited image and return the URI - returns ORIGINAL without processing */
|
||||||
private suspend fun saveEditedImage(
|
private suspend fun saveEditedImage(
|
||||||
context: Context,
|
context: Context,
|
||||||
photoEditor: PhotoEditor?,
|
photoEditor: PhotoEditor?,
|
||||||
photoEditorView: PhotoEditorView?,
|
photoEditorView: PhotoEditorView?,
|
||||||
imageUri: Uri,
|
imageUri: Uri,
|
||||||
onResult: (Uri?) -> Unit
|
onResult: (Uri?) -> Unit
|
||||||
|
) {
|
||||||
|
// Просто возвращаем оригинальный URI без обработки через PhotoEditor
|
||||||
|
// Это устраняет проблему с обрезкой изображений
|
||||||
|
onResult(imageUri)
|
||||||
|
}
|
||||||
|
|
||||||
|
/** OLD VERSION - Save edited image with crop logic (disabled due to cropping issues) */
|
||||||
|
@Suppress("unused")
|
||||||
|
private suspend fun saveEditedImageOld(
|
||||||
|
context: Context,
|
||||||
|
photoEditor: PhotoEditor?,
|
||||||
|
photoEditorView: PhotoEditorView?,
|
||||||
|
imageUri: Uri,
|
||||||
|
onResult: (Uri?) -> Unit
|
||||||
) {
|
) {
|
||||||
if (photoEditor == null || photoEditorView == null) {
|
if (photoEditor == null || photoEditorView == null) {
|
||||||
// Нет редактора - возвращаем оригинал
|
// Нет редактора - возвращаем оригинал
|
||||||
@@ -1282,12 +1296,25 @@ private suspend fun saveEditedImage(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Save edited image synchronously - returns original if no edits, otherwise crops black bars */
|
/** Save edited image synchronously - returns ORIGINAL URI without any processing */
|
||||||
private suspend fun saveEditedImageSync(
|
private suspend fun saveEditedImageSync(
|
||||||
context: Context,
|
context: Context,
|
||||||
photoEditor: PhotoEditor?,
|
photoEditor: PhotoEditor?,
|
||||||
photoEditorView: PhotoEditorView?,
|
photoEditorView: PhotoEditorView?,
|
||||||
imageUri: Uri
|
imageUri: Uri
|
||||||
|
): Uri? {
|
||||||
|
// Просто возвращаем оригинальный URI без обработки
|
||||||
|
// PhotoEditor вызывает проблемы с обрезкой - пользователь получает оригинал
|
||||||
|
return imageUri
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Save edited image synchronously - OLD VERSION with crop logic (disabled) */
|
||||||
|
@Suppress("unused")
|
||||||
|
private suspend fun saveEditedImageSyncOld(
|
||||||
|
context: Context,
|
||||||
|
photoEditor: PhotoEditor?,
|
||||||
|
photoEditorView: PhotoEditorView?,
|
||||||
|
imageUri: Uri
|
||||||
): Uri? {
|
): Uri? {
|
||||||
if (photoEditor == null || photoEditorView == null) {
|
if (photoEditor == null || photoEditorView == null) {
|
||||||
// Нет редактора - возвращаем оригинал
|
// Нет редактора - возвращаем оригинал
|
||||||
|
|||||||
@@ -3,8 +3,10 @@ package com.rosetta.messenger.utils
|
|||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.graphics.Bitmap
|
import android.graphics.Bitmap
|
||||||
import android.graphics.BitmapFactory
|
import android.graphics.BitmapFactory
|
||||||
|
import android.graphics.Matrix
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import android.util.Base64
|
import android.util.Base64
|
||||||
|
import androidx.exifinterface.media.ExifInterface
|
||||||
import com.vanniktech.blurhash.BlurHash
|
import com.vanniktech.blurhash.BlurHash
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
@@ -28,28 +30,33 @@ object MediaUtils {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Конвертировать изображение из Uri в Base64 PNG
|
* Конвертировать изображение из Uri в Base64 PNG
|
||||||
* Автоматически сжимает большие изображения
|
* Автоматически сжимает большие изображения и учитывает EXIF ориентацию
|
||||||
*/
|
*/
|
||||||
suspend fun uriToBase64Image(context: Context, uri: Uri): String? = withContext(Dispatchers.IO) {
|
suspend fun uriToBase64Image(context: Context, uri: Uri): String? = withContext(Dispatchers.IO) {
|
||||||
try {
|
try {
|
||||||
|
|
||||||
|
// Читаем EXIF ориентацию
|
||||||
|
val orientation = getExifOrientation(context, uri)
|
||||||
|
|
||||||
// Открываем InputStream
|
// Открываем InputStream
|
||||||
val inputStream: InputStream = context.contentResolver.openInputStream(uri)
|
val inputStream: InputStream = context.contentResolver.openInputStream(uri)
|
||||||
?: return@withContext null
|
?: return@withContext null
|
||||||
|
|
||||||
// Декодируем изображение
|
// Декодируем изображение
|
||||||
val originalBitmap = BitmapFactory.decodeStream(inputStream)
|
var bitmap = BitmapFactory.decodeStream(inputStream)
|
||||||
inputStream.close()
|
inputStream.close()
|
||||||
|
|
||||||
if (originalBitmap == null) {
|
if (bitmap == null) {
|
||||||
return@withContext null
|
return@withContext null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Применяем EXIF ориентацию (поворот/отражение)
|
||||||
|
bitmap = applyExifOrientation(bitmap, orientation)
|
||||||
|
|
||||||
// Масштабируем если слишком большое
|
// Масштабируем если слишком большое
|
||||||
val scaledBitmap = scaleDownBitmap(originalBitmap, MAX_IMAGE_SIZE)
|
val scaledBitmap = scaleDownBitmap(bitmap, MAX_IMAGE_SIZE)
|
||||||
if (scaledBitmap != originalBitmap) {
|
if (scaledBitmap != bitmap) {
|
||||||
originalBitmap.recycle()
|
bitmap.recycle()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -68,6 +75,59 @@ object MediaUtils {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Читает EXIF ориентацию из изображения
|
||||||
|
*/
|
||||||
|
private fun getExifOrientation(context: Context, uri: Uri): Int {
|
||||||
|
return try {
|
||||||
|
context.contentResolver.openInputStream(uri)?.use { inputStream ->
|
||||||
|
val exif = ExifInterface(inputStream)
|
||||||
|
exif.getAttributeInt(
|
||||||
|
ExifInterface.TAG_ORIENTATION,
|
||||||
|
ExifInterface.ORIENTATION_NORMAL
|
||||||
|
)
|
||||||
|
} ?: ExifInterface.ORIENTATION_NORMAL
|
||||||
|
} catch (e: Exception) {
|
||||||
|
ExifInterface.ORIENTATION_NORMAL
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Применяет EXIF ориентацию к Bitmap (поворот/отражение)
|
||||||
|
*/
|
||||||
|
private fun applyExifOrientation(bitmap: Bitmap, orientation: Int): Bitmap {
|
||||||
|
val matrix = Matrix()
|
||||||
|
|
||||||
|
when (orientation) {
|
||||||
|
ExifInterface.ORIENTATION_ROTATE_90 -> matrix.postRotate(90f)
|
||||||
|
ExifInterface.ORIENTATION_ROTATE_180 -> matrix.postRotate(180f)
|
||||||
|
ExifInterface.ORIENTATION_ROTATE_270 -> matrix.postRotate(270f)
|
||||||
|
ExifInterface.ORIENTATION_FLIP_HORIZONTAL -> matrix.preScale(-1f, 1f)
|
||||||
|
ExifInterface.ORIENTATION_FLIP_VERTICAL -> matrix.preScale(1f, -1f)
|
||||||
|
ExifInterface.ORIENTATION_TRANSPOSE -> {
|
||||||
|
matrix.postRotate(90f)
|
||||||
|
matrix.preScale(-1f, 1f)
|
||||||
|
}
|
||||||
|
ExifInterface.ORIENTATION_TRANSVERSE -> {
|
||||||
|
matrix.postRotate(270f)
|
||||||
|
matrix.preScale(-1f, 1f)
|
||||||
|
}
|
||||||
|
else -> return bitmap // ORIENTATION_NORMAL или неизвестный
|
||||||
|
}
|
||||||
|
|
||||||
|
return try {
|
||||||
|
val rotatedBitmap = Bitmap.createBitmap(
|
||||||
|
bitmap, 0, 0, bitmap.width, bitmap.height, matrix, true
|
||||||
|
)
|
||||||
|
if (rotatedBitmap != bitmap) {
|
||||||
|
bitmap.recycle()
|
||||||
|
}
|
||||||
|
rotatedBitmap
|
||||||
|
} catch (e: Exception) {
|
||||||
|
bitmap
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Генерировать Blurhash для изображения
|
* Генерировать Blurhash для изображения
|
||||||
*/
|
*/
|
||||||
@@ -203,6 +263,7 @@ object MediaUtils {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Получить размеры изображения из Uri без полной загрузки в память
|
* Получить размеры изображения из Uri без полной загрузки в память
|
||||||
|
* Учитывает EXIF ориентацию для правильных width/height
|
||||||
*/
|
*/
|
||||||
fun getImageDimensions(context: Context, uri: Uri): Pair<Int, Int> {
|
fun getImageDimensions(context: Context, uri: Uri): Pair<Int, Int> {
|
||||||
return try {
|
return try {
|
||||||
@@ -216,6 +277,17 @@ object MediaUtils {
|
|||||||
var width = options.outWidth
|
var width = options.outWidth
|
||||||
var height = options.outHeight
|
var height = options.outHeight
|
||||||
|
|
||||||
|
// Учитываем EXIF ориентацию - если поворот 90 или 270, меняем местами width/height
|
||||||
|
val orientation = getExifOrientation(context, uri)
|
||||||
|
if (orientation == ExifInterface.ORIENTATION_ROTATE_90 ||
|
||||||
|
orientation == ExifInterface.ORIENTATION_ROTATE_270 ||
|
||||||
|
orientation == ExifInterface.ORIENTATION_TRANSPOSE ||
|
||||||
|
orientation == ExifInterface.ORIENTATION_TRANSVERSE) {
|
||||||
|
val temp = width
|
||||||
|
width = height
|
||||||
|
height = temp
|
||||||
|
}
|
||||||
|
|
||||||
// Учитываем масштабирование (как в uriToBase64Image)
|
// Учитываем масштабирование (как в uriToBase64Image)
|
||||||
if (width > MAX_IMAGE_SIZE || height > MAX_IMAGE_SIZE) {
|
if (width > MAX_IMAGE_SIZE || height > MAX_IMAGE_SIZE) {
|
||||||
val ratio = width.toFloat() / height.toFloat()
|
val ratio = width.toFloat() / height.toFloat()
|
||||||
|
|||||||
Reference in New Issue
Block a user