feat: Enhance background color animation on theme change in SwipeableDialogItem
This commit is contained in:
@@ -69,6 +69,7 @@ import com.rosetta.messenger.data.ForwardManager
|
||||
import com.rosetta.messenger.data.MessageRepository
|
||||
import com.rosetta.messenger.database.RosettaDatabase
|
||||
import com.rosetta.messenger.network.AttachmentType
|
||||
import com.rosetta.messenger.network.ProtocolManager
|
||||
import com.rosetta.messenger.network.SearchUser
|
||||
import com.rosetta.messenger.repository.AvatarRepository
|
||||
import com.rosetta.messenger.ui.chats.components.*
|
||||
@@ -355,11 +356,18 @@ fun ChatDetailScreen(
|
||||
}
|
||||
}
|
||||
|
||||
DisposableEffect(Unit) {
|
||||
ProtocolManager.enableUILogs(true)
|
||||
onDispose { ProtocolManager.enableUILogs(false) }
|
||||
}
|
||||
|
||||
// Состояние выпадающего меню
|
||||
var showMenu by remember { mutableStateOf(false) }
|
||||
var showDebugLogs by remember { mutableStateOf(false) }
|
||||
var showDeleteConfirm by remember { mutableStateOf(false) }
|
||||
var showBlockConfirm by remember { mutableStateOf(false) }
|
||||
var showUnblockConfirm by remember { mutableStateOf(false) }
|
||||
val debugLogs by ProtocolManager.debugLogs.collectAsState()
|
||||
// Наблюдаем за статусом блокировки в реальном времени через Flow
|
||||
val isBlocked by
|
||||
database.blacklistDao()
|
||||
@@ -1178,6 +1186,10 @@ fun ChatDetailScreen(
|
||||
isSystemAccount,
|
||||
isBlocked =
|
||||
isBlocked,
|
||||
onLogsClick = {
|
||||
showMenu = false
|
||||
showDebugLogs = true
|
||||
},
|
||||
onBlockClick = {
|
||||
showMenu =
|
||||
false
|
||||
@@ -2480,4 +2492,13 @@ fun ChatDetailScreen(
|
||||
recipientName = user.title
|
||||
)
|
||||
}
|
||||
|
||||
if (showDebugLogs) {
|
||||
DebugLogsBottomSheet(
|
||||
logs = debugLogs,
|
||||
isDarkTheme = isDarkTheme,
|
||||
onDismiss = { showDebugLogs = false },
|
||||
onClearLogs = { ProtocolManager.clearLogs() }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -446,6 +446,23 @@ class ChatViewModel(application: Application) : AndroidViewModel(application) {
|
||||
updateCacheFromCurrentMessages()
|
||||
}
|
||||
|
||||
private fun shortPhotoId(value: String, limit: Int = 8): String {
|
||||
val trimmed = value.trim()
|
||||
if (trimmed.isEmpty()) return "unknown"
|
||||
return if (trimmed.length <= limit) trimmed else trimmed.take(limit)
|
||||
}
|
||||
|
||||
private fun logPhotoPipeline(messageId: String, message: String) {
|
||||
ProtocolManager.addLog("📸 IMG ${shortPhotoId(messageId)} | $message")
|
||||
}
|
||||
|
||||
private fun logPhotoPipelineError(messageId: String, stage: String, throwable: Throwable) {
|
||||
val reason = throwable.message ?: "unknown"
|
||||
ProtocolManager.addLog(
|
||||
"❌ IMG ${shortPhotoId(messageId)} | $stage failed: ${throwable.javaClass.simpleName}: $reason"
|
||||
)
|
||||
}
|
||||
|
||||
/** 🔄 Очистить localUri в attachments сообщения (после успешной отправки) */
|
||||
private fun updateMessageAttachments(messageId: String, localUri: String?) {
|
||||
_messages.value =
|
||||
@@ -2240,6 +2257,9 @@ class ChatViewModel(application: Application) : AndroidViewModel(application) {
|
||||
val context = getApplication<Application>()
|
||||
|
||||
if (recipient == null || sender == null || privateKey == null) {
|
||||
ProtocolManager.addLog(
|
||||
"❌ IMG send aborted: missing keys (recipient=${recipient != null}, sender=${sender != null}, private=${privateKey != null})"
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -2248,10 +2268,16 @@ class ChatViewModel(application: Application) : AndroidViewModel(application) {
|
||||
val text = caption.trim()
|
||||
val attachmentId = "img_$timestamp"
|
||||
|
||||
logPhotoPipeline(
|
||||
messageId,
|
||||
"start: uri=${imageUri.lastPathSegment ?: "unknown"}, captionLen=${text.length}, attachment=${shortPhotoId(attachmentId, 12)}"
|
||||
)
|
||||
|
||||
// 🔥 КРИТИЧНО: Получаем размеры СРАЗУ (быстрая операция - только читает заголовок файла)
|
||||
// Это предотвращает "расширение" пузырька при первом показе
|
||||
val (imageWidth, imageHeight) =
|
||||
com.rosetta.messenger.utils.MediaUtils.getImageDimensions(context, imageUri)
|
||||
logPhotoPipeline(messageId, "dimensions: ${imageWidth}x$imageHeight")
|
||||
|
||||
// 1. 🚀 МГНОВЕННО показываем optimistic сообщение с localUri И РАЗМЕРАМИ
|
||||
// Используем URI напрямую для отображения (без конвертации в base64)
|
||||
@@ -2282,11 +2308,13 @@ class ChatViewModel(application: Application) : AndroidViewModel(application) {
|
||||
)
|
||||
addMessageSafely(optimisticMessage)
|
||||
_inputText.value = ""
|
||||
logPhotoPipeline(messageId, "optimistic UI added")
|
||||
|
||||
// 2. 🔄 В фоне, независимо от жизненного цикла экрана:
|
||||
// сохраняем optimistic в БД -> конвертируем -> загружаем -> отправляем пакет.
|
||||
backgroundUploadScope.launch {
|
||||
try {
|
||||
logPhotoPipeline(messageId, "persist optimistic message in DB")
|
||||
val optimisticAttachmentsJson =
|
||||
JSONArray()
|
||||
.apply {
|
||||
@@ -2323,16 +2351,20 @@ class ChatViewModel(application: Application) : AndroidViewModel(application) {
|
||||
timestamp = timestamp,
|
||||
opponentPublicKey = recipient
|
||||
)
|
||||
logPhotoPipeline(messageId, "optimistic dialog updated")
|
||||
} catch (_: Exception) {
|
||||
logPhotoPipeline(messageId, "optimistic DB save skipped (non-fatal)")
|
||||
}
|
||||
|
||||
try {
|
||||
val convertStartedAt = System.currentTimeMillis()
|
||||
val (width, height) =
|
||||
com.rosetta.messenger.utils.MediaUtils.getImageDimensions(context, imageUri)
|
||||
|
||||
val imageBase64 =
|
||||
com.rosetta.messenger.utils.MediaUtils.uriToBase64Image(context, imageUri)
|
||||
if (imageBase64 == null) {
|
||||
logPhotoPipeline(messageId, "base64 conversion returned null")
|
||||
if (!isCleared) {
|
||||
withContext(Dispatchers.Main) {
|
||||
updateMessageStatus(messageId, MessageStatus.ERROR)
|
||||
@@ -2340,14 +2372,20 @@ class ChatViewModel(application: Application) : AndroidViewModel(application) {
|
||||
}
|
||||
return@launch
|
||||
}
|
||||
logPhotoPipeline(
|
||||
messageId,
|
||||
"base64 ready: len=${imageBase64.length}, elapsed=${System.currentTimeMillis() - convertStartedAt}ms"
|
||||
)
|
||||
|
||||
val blurhash =
|
||||
com.rosetta.messenger.utils.MediaUtils.generateBlurhash(context, imageUri)
|
||||
logPhotoPipeline(messageId, "blurhash ready: len=${blurhash.length}")
|
||||
|
||||
if (!isCleared) {
|
||||
withContext(Dispatchers.Main) {
|
||||
updateOptimisticImageMessage(messageId, imageBase64, blurhash, width, height)
|
||||
}
|
||||
logPhotoPipeline(messageId, "optimistic payload updated in UI")
|
||||
}
|
||||
|
||||
sendImageMessageInternal(
|
||||
@@ -2362,7 +2400,9 @@ class ChatViewModel(application: Application) : AndroidViewModel(application) {
|
||||
sender = sender,
|
||||
privateKey = privateKey
|
||||
)
|
||||
} catch (_: Exception) {
|
||||
logPhotoPipeline(messageId, "pipeline completed")
|
||||
} catch (e: Exception) {
|
||||
logPhotoPipelineError(messageId, "prepare+convert", e)
|
||||
if (!isCleared) {
|
||||
withContext(Dispatchers.Main) {
|
||||
updateMessageStatus(messageId, MessageStatus.ERROR)
|
||||
@@ -2415,27 +2455,53 @@ class ChatViewModel(application: Application) : AndroidViewModel(application) {
|
||||
) {
|
||||
try {
|
||||
val context = getApplication<Application>()
|
||||
val pipelineStartedAt = System.currentTimeMillis()
|
||||
logPhotoPipeline(
|
||||
messageId,
|
||||
"internal send start: base64Len=${imageBase64.length}, blurhashLen=${blurhash.length}, captionLen=${caption.length}"
|
||||
)
|
||||
|
||||
// Шифрование текста
|
||||
val encryptStartedAt = System.currentTimeMillis()
|
||||
val encryptResult = MessageCrypto.encryptForSending(caption, recipient)
|
||||
val encryptedContent = encryptResult.ciphertext
|
||||
val encryptedKey = encryptResult.encryptedKey
|
||||
val plainKeyAndNonce = encryptResult.plainKeyAndNonce
|
||||
val aesChachaKey = encryptAesChachaKey(plainKeyAndNonce, privateKey)
|
||||
logPhotoPipeline(
|
||||
messageId,
|
||||
"text encrypted: contentLen=${encryptedContent.length}, keyLen=${encryptedKey.length}, elapsed=${System.currentTimeMillis() - encryptStartedAt}ms"
|
||||
)
|
||||
|
||||
val privateKeyHash = CryptoManager.generatePrivateKeyHash(privateKey)
|
||||
|
||||
// 🚀 Шифруем изображение с ChaCha ключом для Transport Server
|
||||
val blobEncryptStartedAt = System.currentTimeMillis()
|
||||
val encryptedImageBlob = MessageCrypto.encryptReplyBlob(imageBase64, plainKeyAndNonce)
|
||||
logPhotoPipeline(
|
||||
messageId,
|
||||
"blob encrypted: len=${encryptedImageBlob.length}, elapsed=${System.currentTimeMillis() - blobEncryptStartedAt}ms"
|
||||
)
|
||||
|
||||
val attachmentId = "img_$timestamp"
|
||||
logPhotoPipeline(
|
||||
messageId,
|
||||
"attachment prepared: id=${shortPhotoId(attachmentId, 12)}, size=${width}x$height"
|
||||
)
|
||||
|
||||
// 📤 Загружаем на Transport Server
|
||||
val isSavedMessages = (sender == recipient)
|
||||
var uploadTag = ""
|
||||
|
||||
if (!isSavedMessages) {
|
||||
logPhotoPipeline(messageId, "upload start: attachment=${shortPhotoId(attachmentId, 12)}")
|
||||
uploadTag = TransportManager.uploadFile(attachmentId, encryptedImageBlob)
|
||||
logPhotoPipeline(
|
||||
messageId,
|
||||
"upload done: tag=${shortPhotoId(uploadTag, 12)}"
|
||||
)
|
||||
} else {
|
||||
logPhotoPipeline(messageId, "saved-messages mode: upload skipped")
|
||||
}
|
||||
|
||||
// Preview содержит tag::blurhash
|
||||
@@ -2467,16 +2533,21 @@ class ChatViewModel(application: Application) : AndroidViewModel(application) {
|
||||
// Отправляем пакет
|
||||
if (!isSavedMessages) {
|
||||
ProtocolManager.send(packet)
|
||||
logPhotoPipeline(messageId, "packet sent to protocol")
|
||||
} else {
|
||||
logPhotoPipeline(messageId, "saved-messages mode: packet send skipped")
|
||||
}
|
||||
|
||||
// 💾 Сохраняем изображение в файл локально
|
||||
AttachmentFileManager.saveAttachment(
|
||||
val savedLocally =
|
||||
AttachmentFileManager.saveAttachment(
|
||||
context = context,
|
||||
blob = imageBase64,
|
||||
attachmentId = attachmentId,
|
||||
publicKey = sender,
|
||||
privateKey = privateKey
|
||||
)
|
||||
logPhotoPipeline(messageId, "local file cache saved=$savedLocally")
|
||||
|
||||
// Сохраняем в БД
|
||||
val attachmentsJson =
|
||||
@@ -2504,19 +2575,26 @@ class ChatViewModel(application: Application) : AndroidViewModel(application) {
|
||||
} else {
|
||||
updateMessageStatusAndAttachmentsInDb(messageId, 1, finalAttachmentsJson)
|
||||
}
|
||||
logPhotoPipeline(messageId, "db status+attachments updated")
|
||||
|
||||
withContext(Dispatchers.Main) {
|
||||
updateMessageStatus(messageId, MessageStatus.SENT)
|
||||
// Также очищаем localUri в UI
|
||||
updateMessageAttachments(messageId, null)
|
||||
}
|
||||
logPhotoPipeline(messageId, "ui status switched to SENT")
|
||||
|
||||
saveDialog(
|
||||
lastMessage = if (caption.isNotEmpty()) caption else "photo",
|
||||
timestamp = timestamp,
|
||||
opponentPublicKey = recipient
|
||||
)
|
||||
logPhotoPipeline(
|
||||
messageId,
|
||||
"dialog updated; totalElapsed=${System.currentTimeMillis() - pipelineStartedAt}ms"
|
||||
)
|
||||
} catch (e: Exception) {
|
||||
logPhotoPipelineError(messageId, "internal-send", e)
|
||||
withContext(Dispatchers.Main) { updateMessageStatus(messageId, MessageStatus.ERROR) }
|
||||
}
|
||||
}
|
||||
@@ -2687,7 +2765,7 @@ class ChatViewModel(application: Application) : AndroidViewModel(application) {
|
||||
opponentPublicKey = recipient
|
||||
)
|
||||
} catch (e: Exception) {
|
||||
withContext(Dispatchers.Main) { updateMessageStatus(messageId, MessageStatus.SENT) }
|
||||
withContext(Dispatchers.Main) { updateMessageStatus(messageId, MessageStatus.ERROR) }
|
||||
} finally {
|
||||
isSending = false
|
||||
}
|
||||
@@ -2719,10 +2797,14 @@ class ChatViewModel(application: Application) : AndroidViewModel(application) {
|
||||
}
|
||||
|
||||
val context = getApplication<Application>()
|
||||
val groupDebugId = UUID.randomUUID().toString().replace("-", "").take(8)
|
||||
ProtocolManager.addLog(
|
||||
"📸 IMG-GROUP $groupDebugId | prepare start: count=${imageUris.size}, captionLen=${caption.trim().length}"
|
||||
)
|
||||
|
||||
backgroundUploadScope.launch {
|
||||
val preparedImages =
|
||||
imageUris.mapNotNull { uri ->
|
||||
imageUris.mapIndexedNotNull { index, uri ->
|
||||
val (width, height) =
|
||||
com.rosetta.messenger.utils.MediaUtils.getImageDimensions(
|
||||
context,
|
||||
@@ -2733,12 +2815,20 @@ class ChatViewModel(application: Application) : AndroidViewModel(application) {
|
||||
context,
|
||||
uri
|
||||
)
|
||||
?: return@mapNotNull null
|
||||
?: run {
|
||||
ProtocolManager.addLog(
|
||||
"❌ IMG-GROUP $groupDebugId | item#$index base64 conversion failed"
|
||||
)
|
||||
return@mapIndexedNotNull null
|
||||
}
|
||||
val blurhash =
|
||||
com.rosetta.messenger.utils.MediaUtils.generateBlurhash(
|
||||
context,
|
||||
uri
|
||||
)
|
||||
ProtocolManager.addLog(
|
||||
"📸 IMG-GROUP $groupDebugId | item#$index prepared: ${width}x$height, base64Len=${imageBase64.length}, blurhashLen=${blurhash.length}"
|
||||
)
|
||||
ImageData(
|
||||
base64 = imageBase64,
|
||||
blurhash = blurhash,
|
||||
@@ -2747,7 +2837,15 @@ class ChatViewModel(application: Application) : AndroidViewModel(application) {
|
||||
)
|
||||
}
|
||||
|
||||
if (preparedImages.isEmpty()) return@launch
|
||||
if (preparedImages.isEmpty()) {
|
||||
ProtocolManager.addLog(
|
||||
"❌ IMG-GROUP $groupDebugId | no prepared images, send canceled"
|
||||
)
|
||||
return@launch
|
||||
}
|
||||
ProtocolManager.addLog(
|
||||
"📸 IMG-GROUP $groupDebugId | prepare done: ready=${preparedImages.size}"
|
||||
)
|
||||
|
||||
withContext(Dispatchers.Main) {
|
||||
sendImageGroup(preparedImages, caption)
|
||||
@@ -2786,6 +2884,10 @@ class ChatViewModel(application: Application) : AndroidViewModel(application) {
|
||||
val messageId = UUID.randomUUID().toString().replace("-", "").take(32)
|
||||
val timestamp = System.currentTimeMillis()
|
||||
val text = caption.trim()
|
||||
logPhotoPipeline(
|
||||
messageId,
|
||||
"group start: count=${images.size}, captionLen=${text.length}"
|
||||
)
|
||||
|
||||
// Создаём attachments для всех изображений
|
||||
val attachmentsList =
|
||||
@@ -2816,12 +2918,17 @@ class ChatViewModel(application: Application) : AndroidViewModel(application) {
|
||||
|
||||
backgroundUploadScope.launch {
|
||||
try {
|
||||
val groupStartedAt = System.currentTimeMillis()
|
||||
// Шифрование текста
|
||||
val encryptResult = MessageCrypto.encryptForSending(text, recipient)
|
||||
val encryptedContent = encryptResult.ciphertext
|
||||
val encryptedKey = encryptResult.encryptedKey
|
||||
val plainKeyAndNonce = encryptResult.plainKeyAndNonce
|
||||
val aesChachaKey = encryptAesChachaKey(plainKeyAndNonce, privateKey)
|
||||
logPhotoPipeline(
|
||||
messageId,
|
||||
"group text encrypted: contentLen=${encryptedContent.length}, keyLen=${encryptedKey.length}"
|
||||
)
|
||||
|
||||
val privateKeyHash = CryptoManager.generatePrivateKeyHash(privateKey)
|
||||
|
||||
@@ -2831,6 +2938,10 @@ class ChatViewModel(application: Application) : AndroidViewModel(application) {
|
||||
|
||||
for ((index, imageData) in images.withIndex()) {
|
||||
val attachmentId = "img_${timestamp}_$index"
|
||||
logPhotoPipeline(
|
||||
messageId,
|
||||
"group item#$index start: id=${shortPhotoId(attachmentId, 12)}, size=${imageData.width}x${imageData.height}"
|
||||
)
|
||||
|
||||
// Шифруем изображение с ChaCha ключом
|
||||
val encryptedImageBlob =
|
||||
@@ -2844,6 +2955,10 @@ class ChatViewModel(application: Application) : AndroidViewModel(application) {
|
||||
} else {
|
||||
imageData.blurhash
|
||||
}
|
||||
logPhotoPipeline(
|
||||
messageId,
|
||||
"group item#$index upload done: tag=${shortPhotoId(uploadTag, 12)}"
|
||||
)
|
||||
|
||||
// Сохраняем в файл локально
|
||||
AttachmentFileManager.saveAttachment(
|
||||
@@ -2897,6 +3012,7 @@ class ChatViewModel(application: Application) : AndroidViewModel(application) {
|
||||
val isSavedMessages = (sender == recipient)
|
||||
if (!isSavedMessages) {
|
||||
ProtocolManager.send(packet)
|
||||
logPhotoPipeline(messageId, "group packet sent")
|
||||
}
|
||||
|
||||
// Сохраняем в БД
|
||||
@@ -2925,8 +3041,13 @@ class ChatViewModel(application: Application) : AndroidViewModel(application) {
|
||||
timestamp = timestamp,
|
||||
opponentPublicKey = recipient
|
||||
)
|
||||
logPhotoPipeline(
|
||||
messageId,
|
||||
"group completed; totalElapsed=${System.currentTimeMillis() - groupStartedAt}ms"
|
||||
)
|
||||
} catch (e: Exception) {
|
||||
withContext(Dispatchers.Main) { updateMessageStatus(messageId, MessageStatus.SENT) }
|
||||
logPhotoPipelineError(messageId, "group-send", e)
|
||||
withContext(Dispatchers.Main) { updateMessageStatus(messageId, MessageStatus.ERROR) }
|
||||
} finally {
|
||||
isSending = false
|
||||
}
|
||||
|
||||
@@ -2544,10 +2544,15 @@ fun SwipeableDialogItem(
|
||||
} else {
|
||||
if (isDarkTheme) Color(0xFF1A1A1A) else Color(0xFFF2F2F7)
|
||||
}
|
||||
var previousTheme by remember { mutableStateOf(isDarkTheme) }
|
||||
val themeJustChanged = previousTheme != isDarkTheme
|
||||
SideEffect { previousTheme = isDarkTheme }
|
||||
val backgroundColor by
|
||||
animateColorAsState(
|
||||
targetValue = targetBackgroundColor,
|
||||
animationSpec = tween(durationMillis = 260, easing = FastOutSlowInEasing),
|
||||
animationSpec =
|
||||
if (themeJustChanged) snap()
|
||||
else tween(durationMillis = 260, easing = FastOutSlowInEasing),
|
||||
label = "pinnedBackground"
|
||||
)
|
||||
var offsetX by remember { mutableStateOf(0f) }
|
||||
|
||||
@@ -1863,6 +1863,7 @@ fun KebabMenu(
|
||||
isSavedMessages: Boolean,
|
||||
isSystemAccount: Boolean = false,
|
||||
isBlocked: Boolean,
|
||||
onLogsClick: () -> Unit,
|
||||
onBlockClick: () -> Unit,
|
||||
onUnblockClick: () -> Unit,
|
||||
onDeleteClick: () -> Unit
|
||||
@@ -1891,6 +1892,14 @@ fun KebabMenu(
|
||||
dismissOnClickOutside = true
|
||||
)
|
||||
) {
|
||||
KebabMenuItem(
|
||||
icon = TelegramIcons.Info,
|
||||
text = "Debug Logs",
|
||||
onClick = onLogsClick,
|
||||
tintColor = iconColor,
|
||||
textColor = textColor
|
||||
)
|
||||
|
||||
if (!isSavedMessages && !isSystemAccount) {
|
||||
KebabMenuItem(
|
||||
icon = if (isBlocked) TelegramIcons.Done else TelegramIcons.Block,
|
||||
|
||||
Reference in New Issue
Block a user