fix: implement optimistic UI for image sending and enhance attachment handling
This commit is contained in:
@@ -265,7 +265,8 @@ data class MessageAttachment(
|
||||
val type: AttachmentType,
|
||||
val preview: String = "", // Метаданные: "UUID::metadata" или "filesize::filename"
|
||||
val width: Int = 0,
|
||||
val height: Int = 0
|
||||
val height: Int = 0,
|
||||
val localUri: String = "" // 🚀 Локальный URI для мгновенного отображения (optimistic UI)
|
||||
)
|
||||
|
||||
/**
|
||||
|
||||
@@ -2151,27 +2151,12 @@ fun ChatDetailScreen(
|
||||
pendingCameraPhotoUri = null
|
||||
},
|
||||
onSave = { editedUri ->
|
||||
// Fallback если onSaveWithCaption не сработал
|
||||
pendingCameraPhotoUri = null
|
||||
scope.launch {
|
||||
val base64 = MediaUtils.uriToBase64Image(context, editedUri)
|
||||
val blurhash = MediaUtils.generateBlurhash(context, editedUri)
|
||||
val (width, height) = MediaUtils.getImageDimensions(context, editedUri)
|
||||
if (base64 != null) {
|
||||
viewModel.sendImageMessage(base64, blurhash, "", width, height)
|
||||
}
|
||||
}
|
||||
// 🚀 Мгновенный optimistic UI - фото появляется СРАЗУ
|
||||
viewModel.sendImageFromUri(editedUri, "")
|
||||
},
|
||||
onSaveWithCaption = { editedUri, caption ->
|
||||
pendingCameraPhotoUri = null
|
||||
scope.launch {
|
||||
val base64 = MediaUtils.uriToBase64Image(context, editedUri)
|
||||
val blurhash = MediaUtils.generateBlurhash(context, editedUri)
|
||||
val (width, height) = MediaUtils.getImageDimensions(context, editedUri)
|
||||
if (base64 != null) {
|
||||
viewModel.sendImageMessage(base64, blurhash, caption, width, height)
|
||||
}
|
||||
}
|
||||
// 🚀 Мгновенный optimistic UI - фото появляется СРАЗУ
|
||||
viewModel.sendImageFromUri(editedUri, caption)
|
||||
},
|
||||
isDarkTheme = isDarkTheme,
|
||||
showCaptionInput = true,
|
||||
@@ -2187,33 +2172,9 @@ fun ChatDetailScreen(
|
||||
pendingGalleryImages = emptyList()
|
||||
},
|
||||
onSendAll = { imagesWithCaptions ->
|
||||
pendingGalleryImages = emptyList()
|
||||
scope.launch {
|
||||
// Собираем все изображения с их caption
|
||||
val imageDataList = mutableListOf<ChatViewModel.ImageData>()
|
||||
|
||||
for (imageWithCaption in imagesWithCaptions) {
|
||||
val base64 = MediaUtils.uriToBase64Image(context, imageWithCaption.uri)
|
||||
val blurhash = MediaUtils.generateBlurhash(context, imageWithCaption.uri)
|
||||
val (width, height) = MediaUtils.getImageDimensions(context, imageWithCaption.uri)
|
||||
if (base64 != null) {
|
||||
imageDataList.add(ChatViewModel.ImageData(base64, blurhash, width, height))
|
||||
}
|
||||
}
|
||||
|
||||
// Если одно фото с caption - отправляем как обычное сообщение
|
||||
if (imageDataList.size == 1 && imagesWithCaptions[0].caption.isNotBlank()) {
|
||||
viewModel.sendImageMessage(
|
||||
imageDataList[0].base64,
|
||||
imageDataList[0].blurhash,
|
||||
imagesWithCaptions[0].caption,
|
||||
imageDataList[0].width,
|
||||
imageDataList[0].height
|
||||
)
|
||||
} else if (imageDataList.isNotEmpty()) {
|
||||
// Отправляем группой (коллаж)
|
||||
viewModel.sendImageGroup(imageDataList)
|
||||
}
|
||||
// 🚀 Мгновенный optimistic UI для каждого фото
|
||||
for (imageWithCaption in imagesWithCaptions) {
|
||||
viewModel.sendImageFromUri(imageWithCaption.uri, imageWithCaption.caption)
|
||||
}
|
||||
},
|
||||
isDarkTheme = isDarkTheme,
|
||||
|
||||
@@ -1467,6 +1467,235 @@ val newList = messages + optimisticMessages
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 📸🚀 Отправка изображения по URI с МГНОВЕННЫМ optimistic UI
|
||||
* Фото появляется в чате СРАЗУ, конвертация и отправка происходят в фоне
|
||||
*
|
||||
* @param imageUri URI изображения
|
||||
* @param caption Подпись к изображению
|
||||
*/
|
||||
fun sendImageFromUri(imageUri: android.net.Uri, caption: String = "") {
|
||||
val recipient = opponentKey
|
||||
val sender = myPublicKey
|
||||
val privateKey = myPrivateKey
|
||||
val context = getApplication<Application>()
|
||||
|
||||
if (recipient == null || sender == null || privateKey == null) {
|
||||
return
|
||||
}
|
||||
|
||||
val messageId = UUID.randomUUID().toString().replace("-", "").take(32)
|
||||
val timestamp = System.currentTimeMillis()
|
||||
val text = caption.trim()
|
||||
|
||||
// 1. 🚀 МГНОВЕННО показываем optimistic сообщение с localUri
|
||||
// Используем URI напрямую для отображения (без конвертации в base64)
|
||||
val optimisticMessage = ChatMessage(
|
||||
id = messageId,
|
||||
text = text,
|
||||
isOutgoing = true,
|
||||
timestamp = Date(timestamp),
|
||||
status = MessageStatus.SENDING,
|
||||
attachments = listOf(
|
||||
MessageAttachment(
|
||||
id = "img_$timestamp",
|
||||
blob = "", // Пока пустой, обновим после конвертации
|
||||
type = AttachmentType.IMAGE,
|
||||
preview = "", // Пока пустой, обновим после генерации
|
||||
width = 0,
|
||||
height = 0,
|
||||
localUri = imageUri.toString() // 🔥 Используем localUri для мгновенного показа
|
||||
)
|
||||
)
|
||||
)
|
||||
addMessageSafely(optimisticMessage)
|
||||
_inputText.value = ""
|
||||
|
||||
// 2. 🔄 В ФОНЕ: конвертируем, генерируем blurhash и отправляем
|
||||
viewModelScope.launch(Dispatchers.IO) {
|
||||
try {
|
||||
// Получаем размеры изображения
|
||||
val (width, height) = com.rosetta.messenger.utils.MediaUtils.getImageDimensions(context, imageUri)
|
||||
|
||||
// Конвертируем в base64
|
||||
val imageBase64 = com.rosetta.messenger.utils.MediaUtils.uriToBase64Image(context, imageUri)
|
||||
if (imageBase64 == null) {
|
||||
withContext(Dispatchers.Main) {
|
||||
updateMessageStatus(messageId, MessageStatus.ERROR)
|
||||
}
|
||||
return@launch
|
||||
}
|
||||
|
||||
// Генерируем blurhash
|
||||
val blurhash = com.rosetta.messenger.utils.MediaUtils.generateBlurhash(context, imageUri)
|
||||
|
||||
// 3. 🔄 Обновляем optimistic сообщение с реальными данными
|
||||
withContext(Dispatchers.Main) {
|
||||
updateOptimisticImageMessage(messageId, imageBase64, blurhash, width, height)
|
||||
}
|
||||
|
||||
// 4. 📤 Отправляем (шифрование + загрузка на сервер)
|
||||
sendImageMessageInternal(
|
||||
messageId = messageId,
|
||||
imageBase64 = imageBase64,
|
||||
blurhash = blurhash,
|
||||
caption = text,
|
||||
width = width,
|
||||
height = height,
|
||||
timestamp = timestamp,
|
||||
recipient = recipient,
|
||||
sender = sender,
|
||||
privateKey = privateKey
|
||||
)
|
||||
|
||||
} catch (e: Exception) {
|
||||
withContext(Dispatchers.Main) {
|
||||
updateMessageStatus(messageId, MessageStatus.ERROR)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 🔄 Обновляет optimistic сообщение с реальными данными изображения
|
||||
*/
|
||||
private fun updateOptimisticImageMessage(messageId: String, base64: String, blurhash: String, width: Int, height: Int) {
|
||||
val currentMessages = _messages.value.toMutableList()
|
||||
val index = currentMessages.indexOfFirst { it.id == messageId }
|
||||
if (index != -1) {
|
||||
val message = currentMessages[index]
|
||||
val updatedAttachments = message.attachments.map { att ->
|
||||
if (att.type == AttachmentType.IMAGE) {
|
||||
att.copy(
|
||||
preview = blurhash,
|
||||
blob = base64,
|
||||
width = width,
|
||||
height = height
|
||||
)
|
||||
} else att
|
||||
}
|
||||
currentMessages[index] = message.copy(attachments = updatedAttachments)
|
||||
_messages.value = currentMessages
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 📤 Внутренняя функция отправки изображения (уже с готовым base64)
|
||||
*/
|
||||
private suspend fun sendImageMessageInternal(
|
||||
messageId: String,
|
||||
imageBase64: String,
|
||||
blurhash: String,
|
||||
caption: String,
|
||||
width: Int,
|
||||
height: Int,
|
||||
timestamp: Long,
|
||||
recipient: String,
|
||||
sender: String,
|
||||
privateKey: String
|
||||
) {
|
||||
try {
|
||||
val context = getApplication<Application>()
|
||||
|
||||
// Шифрование текста
|
||||
val encryptResult = MessageCrypto.encryptForSending(caption, recipient)
|
||||
val encryptedContent = encryptResult.ciphertext
|
||||
val encryptedKey = encryptResult.encryptedKey
|
||||
val plainKeyAndNonce = encryptResult.plainKeyAndNonce
|
||||
|
||||
val privateKeyHash = CryptoManager.generatePrivateKeyHash(privateKey)
|
||||
|
||||
// 🚀 Шифруем изображение с ChaCha ключом для Transport Server
|
||||
val encryptedImageBlob = MessageCrypto.encryptReplyBlob(imageBase64, plainKeyAndNonce)
|
||||
|
||||
val attachmentId = "img_$timestamp"
|
||||
|
||||
// 📤 Загружаем на Transport Server
|
||||
val isSavedMessages = (sender == recipient)
|
||||
var uploadTag = ""
|
||||
|
||||
if (!isSavedMessages) {
|
||||
uploadTag = TransportManager.uploadFile(attachmentId, encryptedImageBlob)
|
||||
}
|
||||
|
||||
// Preview содержит tag::blurhash
|
||||
val previewWithTag = if (uploadTag.isNotEmpty()) "$uploadTag::$blurhash" else blurhash
|
||||
|
||||
val imageAttachment = MessageAttachment(
|
||||
id = attachmentId,
|
||||
blob = "",
|
||||
type = AttachmentType.IMAGE,
|
||||
preview = previewWithTag,
|
||||
width = width,
|
||||
height = height
|
||||
)
|
||||
|
||||
val packet = PacketMessage().apply {
|
||||
fromPublicKey = sender
|
||||
toPublicKey = recipient
|
||||
content = encryptedContent
|
||||
chachaKey = encryptedKey
|
||||
this.timestamp = timestamp
|
||||
this.privateKey = privateKeyHash
|
||||
this.messageId = messageId
|
||||
attachments = listOf(imageAttachment)
|
||||
}
|
||||
|
||||
// Отправляем пакет
|
||||
if (!isSavedMessages) {
|
||||
ProtocolManager.send(packet)
|
||||
}
|
||||
|
||||
// 💾 Сохраняем изображение в файл локально
|
||||
AttachmentFileManager.saveAttachment(
|
||||
context = context,
|
||||
blob = imageBase64,
|
||||
attachmentId = attachmentId,
|
||||
publicKey = sender,
|
||||
privateKey = privateKey
|
||||
)
|
||||
|
||||
// Сохраняем в БД
|
||||
val attachmentsJson = JSONArray().apply {
|
||||
put(JSONObject().apply {
|
||||
put("id", attachmentId)
|
||||
put("type", AttachmentType.IMAGE.value)
|
||||
put("preview", previewWithTag)
|
||||
put("blob", "")
|
||||
put("width", width)
|
||||
put("height", height)
|
||||
})
|
||||
}.toString()
|
||||
|
||||
saveMessageToDatabase(
|
||||
messageId = messageId,
|
||||
text = caption,
|
||||
encryptedContent = encryptedContent,
|
||||
encryptedKey = encryptedKey,
|
||||
timestamp = timestamp,
|
||||
isFromMe = true,
|
||||
delivered = if (isSavedMessages) 2 else 0,
|
||||
attachmentsJson = attachmentsJson
|
||||
)
|
||||
|
||||
// Обновляем статус на SENT
|
||||
if (!isSavedMessages) {
|
||||
updateMessageStatusInDb(messageId, 2)
|
||||
}
|
||||
|
||||
withContext(Dispatchers.Main) {
|
||||
updateMessageStatus(messageId, MessageStatus.SENT)
|
||||
}
|
||||
|
||||
saveDialog(if (caption.isNotEmpty()) caption else "photo", timestamp)
|
||||
|
||||
} catch (e: Exception) {
|
||||
withContext(Dispatchers.Main) {
|
||||
updateMessageStatus(messageId, MessageStatus.ERROR)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 📸 Отправка сообщения с изображением
|
||||
* @param imageBase64 Base64 изображения
|
||||
|
||||
@@ -2,6 +2,7 @@ package com.rosetta.messenger.ui.chats.components
|
||||
|
||||
import android.graphics.Bitmap
|
||||
import android.graphics.BitmapFactory
|
||||
import android.graphics.Matrix
|
||||
import android.util.Base64
|
||||
import androidx.compose.animation.core.Animatable
|
||||
import androidx.compose.animation.core.animateFloatAsState
|
||||
@@ -33,6 +34,7 @@ import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import androidx.exifinterface.media.ExifInterface
|
||||
import com.rosetta.messenger.crypto.MessageCrypto
|
||||
import com.rosetta.messenger.network.AttachmentType
|
||||
import com.rosetta.messenger.network.MessageAttachment
|
||||
@@ -538,11 +540,15 @@ fun ImageAttachment(
|
||||
)
|
||||
|
||||
// Определяем начальный статус и декодируем blurhash (как в Desktop calcDownloadStatus)
|
||||
LaunchedEffect(attachment.id) {
|
||||
LaunchedEffect(attachment.id, attachment.localUri) {
|
||||
// Определяем статус (логика из Desktop useAttachment.ts)
|
||||
withContext(Dispatchers.IO) {
|
||||
downloadStatus =
|
||||
when {
|
||||
// 🚀 0. Если есть localUri - это optimistic UI, показываем сразу
|
||||
attachment.localUri.isNotEmpty() -> {
|
||||
DownloadStatus.DOWNLOADED
|
||||
}
|
||||
// 1. Если blob уже есть в памяти → DOWNLOADED
|
||||
attachment.blob.isNotEmpty() -> {
|
||||
DownloadStatus.DOWNLOADED
|
||||
@@ -587,11 +593,67 @@ fun ImageAttachment(
|
||||
// Загружаем изображение если статус DOWNLOADED
|
||||
if (downloadStatus == DownloadStatus.DOWNLOADED) {
|
||||
withContext(Dispatchers.IO) {
|
||||
// 1. Сначала пробуем blob из памяти
|
||||
if (attachment.blob.isNotEmpty()) {
|
||||
// 🚀 0. Если есть localUri - загружаем напрямую из URI (optimistic UI)
|
||||
if (attachment.localUri.isNotEmpty()) {
|
||||
try {
|
||||
val uri = android.net.Uri.parse(attachment.localUri)
|
||||
|
||||
// 📐 Читаем EXIF ориентацию
|
||||
val orientation = 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
|
||||
}
|
||||
|
||||
// Загружаем bitmap
|
||||
val inputStream = context.contentResolver.openInputStream(uri)
|
||||
var bitmap = BitmapFactory.decodeStream(inputStream)
|
||||
inputStream?.close()
|
||||
|
||||
// 🔄 Применяем EXIF ориентацию
|
||||
if (bitmap != null && orientation != ExifInterface.ORIENTATION_NORMAL) {
|
||||
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)
|
||||
}
|
||||
}
|
||||
val rotated = Bitmap.createBitmap(bitmap, 0, 0, bitmap.width, bitmap.height, matrix, true)
|
||||
if (rotated != bitmap) {
|
||||
bitmap.recycle()
|
||||
bitmap = rotated
|
||||
}
|
||||
}
|
||||
|
||||
imageBitmap = bitmap
|
||||
} catch (e: Exception) {
|
||||
// Fallback to other methods
|
||||
}
|
||||
}
|
||||
|
||||
// 1. Если нет bitmap, пробуем blob из памяти
|
||||
if (imageBitmap == null && attachment.blob.isNotEmpty()) {
|
||||
imageBitmap = base64ToBitmap(attachment.blob)
|
||||
} else {
|
||||
// 2. Читаем из файловой системы (как в Desktop getBlob)
|
||||
}
|
||||
|
||||
// 2. Если всё ещё нет, читаем из файловой системы
|
||||
if (imageBitmap == null) {
|
||||
val localBlob =
|
||||
AttachmentFileManager.readAttachment(
|
||||
context,
|
||||
@@ -601,7 +663,8 @@ fun ImageAttachment(
|
||||
)
|
||||
if (localBlob != null) {
|
||||
imageBitmap = base64ToBitmap(localBlob)
|
||||
} else {
|
||||
} else if (attachment.localUri.isEmpty()) {
|
||||
// Только если нет localUri - помечаем как NOT_DOWNLOADED
|
||||
downloadStatus = DownloadStatus.NOT_DOWNLOADED
|
||||
}
|
||||
}
|
||||
@@ -902,6 +965,30 @@ fun ImageAttachment(
|
||||
}
|
||||
}
|
||||
|
||||
// ✈️ Telegram-style: Loader при отправке фото (самолётик/кружок)
|
||||
// Показываем когда сообщение отправляется И это исходящее сообщение
|
||||
if (isOutgoing && messageStatus == MessageStatus.SENDING && downloadStatus == DownloadStatus.DOWNLOADED) {
|
||||
Box(
|
||||
modifier = Modifier.fillMaxSize().background(Color.Black.copy(alpha = 0.35f)),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
// Круглый индикатор отправки
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.size(48.dp)
|
||||
.clip(CircleShape)
|
||||
.background(Color.Black.copy(alpha = 0.5f)),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
CircularProgressIndicator(
|
||||
modifier = Modifier.size(28.dp),
|
||||
color = Color.White,
|
||||
strokeWidth = 2.5.dp
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Оверлей для статуса скачивания
|
||||
if (downloadStatus != DownloadStatus.DOWNLOADED) {
|
||||
Box(
|
||||
|
||||
@@ -603,19 +603,27 @@ fun ImageEditorScreen(
|
||||
onToggleEmojiPicker = { toggleEmojiPicker() },
|
||||
onEditTextViewCreated = { editTextView = it },
|
||||
onSend = {
|
||||
// 🚀 Сохраняем caption для использования после анимации
|
||||
val captionToSend = caption
|
||||
val uriToSend = currentImageUri
|
||||
|
||||
// ✈️ Сразу запускаем fade-out анимацию (как в Telegram)
|
||||
// Фото появится в чате через optimistic UI
|
||||
scope.launch {
|
||||
isSaving = true
|
||||
saveEditedImage(context, photoEditor, photoEditorView, currentImageUri) { savedUri ->
|
||||
isSaving = false
|
||||
saveEditedImage(context, photoEditor, photoEditorView, uriToSend) { savedUri ->
|
||||
if (savedUri != null) {
|
||||
// Вызываем callback (он запустит sendImageMessage с optimistic UI)
|
||||
if (onSaveWithCaption != null) {
|
||||
onSaveWithCaption(savedUri, caption)
|
||||
onSaveWithCaption(savedUri, captionToSend)
|
||||
} else {
|
||||
onSave(savedUri)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 🎬 Плавно закрываем экран (fade-out)
|
||||
animatedDismiss()
|
||||
}
|
||||
)
|
||||
|
||||
@@ -1478,6 +1486,39 @@ fun MultiImageEditorScreen(
|
||||
val scope = rememberCoroutineScope()
|
||||
val view = LocalView.current
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════
|
||||
// 🎬 TELEGRAM-STYLE FADE ANIMATION
|
||||
// ═══════════════════════════════════════════════════════════════
|
||||
var isClosing by remember { mutableStateOf(false) }
|
||||
val animationProgress = remember { Animatable(0f) }
|
||||
|
||||
// Запуск enter анимации
|
||||
LaunchedEffect(Unit) {
|
||||
animationProgress.animateTo(
|
||||
targetValue = 1f,
|
||||
animationSpec = tween(
|
||||
durationMillis = 200,
|
||||
easing = FastOutSlowInEasing
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
// Функция для плавного закрытия
|
||||
fun animatedDismiss() {
|
||||
if (isClosing) return
|
||||
isClosing = true
|
||||
scope.launch {
|
||||
animationProgress.animateTo(
|
||||
targetValue = 0f,
|
||||
animationSpec = tween(
|
||||
durationMillis = 200,
|
||||
easing = FastOutSlowInEasing
|
||||
)
|
||||
)
|
||||
onDismiss()
|
||||
}
|
||||
}
|
||||
|
||||
// 🎨 Черный статус бар и навигационный бар для редактора
|
||||
val activity = context as? Activity
|
||||
val window = activity?.window
|
||||
@@ -1554,12 +1595,20 @@ fun MultiImageEditorScreen(
|
||||
}
|
||||
}
|
||||
|
||||
BackHandler { onDismiss() }
|
||||
BackHandler { animatedDismiss() }
|
||||
|
||||
// 🎬 Анимированный контейнер с fade эффектом
|
||||
val progress = animationProgress.value
|
||||
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.background(Color.Black)
|
||||
.background(Color.Black.copy(alpha = progress))
|
||||
.graphicsLayer {
|
||||
alpha = progress
|
||||
scaleX = 0.95f + 0.05f * progress
|
||||
scaleY = 0.95f + 0.05f * progress
|
||||
}
|
||||
) {
|
||||
// Pager
|
||||
HorizontalPager(
|
||||
@@ -1646,7 +1695,7 @@ fun MultiImageEditorScreen(
|
||||
.padding(horizontal = 4.dp, vertical = 8.dp)
|
||||
) {
|
||||
IconButton(
|
||||
onClick = onDismiss,
|
||||
onClick = { animatedDismiss() },
|
||||
modifier = Modifier.align(Alignment.CenterStart)
|
||||
) {
|
||||
Icon(
|
||||
@@ -1829,15 +1878,18 @@ fun MultiImageEditorScreen(
|
||||
.size(48.dp)
|
||||
.clip(CircleShape)
|
||||
.background(PrimaryBlue)
|
||||
.clickable(enabled = !isSaving) {
|
||||
isSaving = true
|
||||
.clickable(enabled = !isSaving && !isClosing) {
|
||||
// 🚀 Сохраняем копию данных перед анимацией
|
||||
val imagesToSend = imagesWithCaptions.toList()
|
||||
|
||||
// 🎬 Запускаем fade-out анимацию сразу
|
||||
scope.launch {
|
||||
val savedImages = mutableListOf<ImageWithCaption>()
|
||||
|
||||
for (i in imagesWithCaptions.indices) {
|
||||
for (i in imagesToSend.indices) {
|
||||
val editor = photoEditors[i]
|
||||
val editorView = photoEditorViews[i]
|
||||
val originalImage = imagesWithCaptions[i]
|
||||
val originalImage = imagesToSend[i]
|
||||
|
||||
if (editor != null) {
|
||||
val savedUri = saveEditedImageSync(context, editor, editorView, originalImage.uri)
|
||||
@@ -1851,27 +1903,23 @@ fun MultiImageEditorScreen(
|
||||
}
|
||||
}
|
||||
|
||||
// Вызываем callback (он запустит sendImageGroup с optimistic UI)
|
||||
onSendAll(savedImages)
|
||||
}
|
||||
|
||||
// ✈️ Плавно закрываем экран (fade-out)
|
||||
animatedDismiss()
|
||||
},
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
if (isSaving) {
|
||||
CircularProgressIndicator(
|
||||
modifier = Modifier.size(22.dp),
|
||||
color = Color.White,
|
||||
strokeWidth = 2.dp
|
||||
)
|
||||
} else {
|
||||
Icon(
|
||||
TablerIcons.Send,
|
||||
contentDescription = "Send",
|
||||
tint = Color.White,
|
||||
modifier = Modifier
|
||||
.size(22.dp)
|
||||
.offset(x = 1.dp)
|
||||
)
|
||||
}
|
||||
Icon(
|
||||
TablerIcons.Send,
|
||||
contentDescription = "Send",
|
||||
tint = Color.White,
|
||||
modifier = Modifier
|
||||
.size(22.dp)
|
||||
.offset(x = 1.dp)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user