fix: update message logging format for consistency and clarity
This commit is contained in:
@@ -43,6 +43,7 @@ import com.rosetta.messenger.R
|
|||||||
import com.rosetta.messenger.data.RecentSearchesManager
|
import com.rosetta.messenger.data.RecentSearchesManager
|
||||||
import com.rosetta.messenger.network.ProtocolManager
|
import com.rosetta.messenger.network.ProtocolManager
|
||||||
import com.rosetta.messenger.network.ProtocolState
|
import com.rosetta.messenger.network.ProtocolState
|
||||||
|
import com.rosetta.messenger.ui.chats.components.AnimatedDotsText
|
||||||
import com.rosetta.messenger.ui.components.AppleEmojiText
|
import com.rosetta.messenger.ui.components.AppleEmojiText
|
||||||
import com.rosetta.messenger.ui.components.AvatarImage
|
import com.rosetta.messenger.ui.components.AvatarImage
|
||||||
import com.rosetta.messenger.ui.components.BlurredAvatarBackground
|
import com.rosetta.messenger.ui.components.BlurredAvatarBackground
|
||||||
@@ -351,27 +352,49 @@ android.util.Log.d("ChatsListScreen", "✅ Total LaunchedEffect: ${System.curren
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
Spacer(modifier = Modifier.width(12.dp))
|
Spacer(modifier = Modifier.width(12.dp))
|
||||||
Text(
|
|
||||||
text =
|
|
||||||
when (protocolState) {
|
when (protocolState) {
|
||||||
ProtocolState
|
ProtocolState.DISCONNECTED -> {
|
||||||
.DISCONNECTED ->
|
Text(
|
||||||
"Disconnected"
|
text = "Disconnected",
|
||||||
ProtocolState.CONNECTING ->
|
|
||||||
"Connecting..."
|
|
||||||
ProtocolState.CONNECTED ->
|
|
||||||
"Connected"
|
|
||||||
ProtocolState.HANDSHAKING ->
|
|
||||||
"Authenticating..."
|
|
||||||
ProtocolState
|
|
||||||
.AUTHENTICATED ->
|
|
||||||
"Authenticated"
|
|
||||||
},
|
|
||||||
fontSize = 16.sp,
|
fontSize = 16.sp,
|
||||||
fontWeight = FontWeight.Medium,
|
fontWeight = FontWeight.Medium,
|
||||||
color = textColor
|
color = textColor
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
ProtocolState.CONNECTING -> {
|
||||||
|
AnimatedDotsText(
|
||||||
|
baseText = "Connecting",
|
||||||
|
color = textColor,
|
||||||
|
fontSize = 16.sp,
|
||||||
|
fontWeight = FontWeight.Medium
|
||||||
|
)
|
||||||
|
}
|
||||||
|
ProtocolState.CONNECTED -> {
|
||||||
|
Text(
|
||||||
|
text = "Connected",
|
||||||
|
fontSize = 16.sp,
|
||||||
|
fontWeight = FontWeight.Medium,
|
||||||
|
color = textColor
|
||||||
|
)
|
||||||
|
}
|
||||||
|
ProtocolState.HANDSHAKING -> {
|
||||||
|
AnimatedDotsText(
|
||||||
|
baseText = "Authenticating",
|
||||||
|
color = textColor,
|
||||||
|
fontSize = 16.sp,
|
||||||
|
fontWeight = FontWeight.Medium
|
||||||
|
)
|
||||||
|
}
|
||||||
|
ProtocolState.AUTHENTICATED -> {
|
||||||
|
Text(
|
||||||
|
text = "Authenticated",
|
||||||
|
fontSize = 16.sp,
|
||||||
|
fontWeight = FontWeight.Medium,
|
||||||
|
color = textColor
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
confirmButton = {
|
confirmButton = {
|
||||||
|
|||||||
@@ -158,6 +158,8 @@ if (currentAccount == publicKey) {
|
|||||||
val actualRead = if (actualFromMe == 1) (lastMsgStatus?.read ?: 0) else 0
|
val actualRead = if (actualFromMe == 1) (lastMsgStatus?.read ?: 0) else 0
|
||||||
|
|
||||||
// 📎 Определяем тип attachment последнего сообщения
|
// 📎 Определяем тип attachment последнего сообщения
|
||||||
|
// 🔥 Reply vs Forward: если есть текст - это Reply (показываем текст),
|
||||||
|
// если текст пустой - это Forward (показываем "Forwarded message")
|
||||||
val attachmentType = try {
|
val attachmentType = try {
|
||||||
val attachmentsJson = messageDao.getLastMessageAttachments(publicKey, dialog.opponentKey)
|
val attachmentsJson = messageDao.getLastMessageAttachments(publicKey, dialog.opponentKey)
|
||||||
if (!attachmentsJson.isNullOrEmpty() && attachmentsJson != "[]") {
|
if (!attachmentsJson.isNullOrEmpty() && attachmentsJson != "[]") {
|
||||||
@@ -167,7 +169,12 @@ if (currentAccount == publicKey) {
|
|||||||
val type = firstAttachment.optInt("type", -1)
|
val type = firstAttachment.optInt("type", -1)
|
||||||
when (type) {
|
when (type) {
|
||||||
0 -> "Photo" // AttachmentType.IMAGE = 0
|
0 -> "Photo" // AttachmentType.IMAGE = 0
|
||||||
1 -> "Forwarded" // AttachmentType.MESSAGES = 1
|
1 -> {
|
||||||
|
// AttachmentType.MESSAGES = 1 (Reply или Forward)
|
||||||
|
// Reply: есть текст сообщения -> показываем текст (null)
|
||||||
|
// Forward: текст пустой -> показываем "Forwarded"
|
||||||
|
if (decryptedLastMessage.isNotEmpty()) null else "Forwarded"
|
||||||
|
}
|
||||||
2 -> "File" // AttachmentType.FILE = 2
|
2 -> "File" // AttachmentType.FILE = 2
|
||||||
3 -> "Avatar" // AttachmentType.AVATAR = 3
|
3 -> "Avatar" // AttachmentType.AVATAR = 3
|
||||||
else -> null
|
else -> null
|
||||||
@@ -244,6 +251,8 @@ if (currentAccount == publicKey) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 📎 Определяем тип attachment последнего сообщения
|
// 📎 Определяем тип attachment последнего сообщения
|
||||||
|
// 🔥 Reply vs Forward: если есть текст - это Reply (показываем текст),
|
||||||
|
// если текст пустой - это Forward (показываем "Forwarded message")
|
||||||
val attachmentType = try {
|
val attachmentType = try {
|
||||||
val attachmentsJson = messageDao.getLastMessageAttachments(publicKey, dialog.opponentKey)
|
val attachmentsJson = messageDao.getLastMessageAttachments(publicKey, dialog.opponentKey)
|
||||||
if (!attachmentsJson.isNullOrEmpty() && attachmentsJson != "[]") {
|
if (!attachmentsJson.isNullOrEmpty() && attachmentsJson != "[]") {
|
||||||
@@ -253,7 +262,12 @@ if (currentAccount == publicKey) {
|
|||||||
val type = firstAttachment.optInt("type", -1)
|
val type = firstAttachment.optInt("type", -1)
|
||||||
when (type) {
|
when (type) {
|
||||||
0 -> "Photo" // AttachmentType.IMAGE = 0
|
0 -> "Photo" // AttachmentType.IMAGE = 0
|
||||||
1 -> "Forwarded" // AttachmentType.MESSAGES = 1
|
1 -> {
|
||||||
|
// AttachmentType.MESSAGES = 1 (Reply или Forward)
|
||||||
|
// Reply: есть текст сообщения -> показываем текст (null)
|
||||||
|
// Forward: текст пустой -> показываем "Forwarded"
|
||||||
|
if (decryptedLastMessage.isNotEmpty()) null else "Forwarded"
|
||||||
|
}
|
||||||
2 -> "File" // AttachmentType.FILE = 2
|
2 -> "File" // AttachmentType.FILE = 2
|
||||||
3 -> "Avatar" // AttachmentType.AVATAR = 3
|
3 -> "Avatar" // AttachmentType.AVATAR = 3
|
||||||
else -> null
|
else -> null
|
||||||
|
|||||||
@@ -49,6 +49,7 @@ import com.vanniktech.blurhash.BlurHash
|
|||||||
import compose.icons.TablerIcons
|
import compose.icons.TablerIcons
|
||||||
import compose.icons.tablericons.*
|
import compose.icons.tablericons.*
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.delay
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
import androidx.compose.ui.platform.LocalConfiguration
|
import androidx.compose.ui.platform.LocalConfiguration
|
||||||
@@ -56,6 +57,48 @@ import kotlin.math.min
|
|||||||
|
|
||||||
private const val TAG = "AttachmentComponents"
|
private const val TAG = "AttachmentComponents"
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 🔄 Анимированный текст с точками (Downloading... → Downloading. → Downloading.. → Downloading...)
|
||||||
|
* Как в Telegram - точки плавно появляются и исчезают
|
||||||
|
*/
|
||||||
|
@Composable
|
||||||
|
fun AnimatedDotsText(
|
||||||
|
baseText: String,
|
||||||
|
color: Color,
|
||||||
|
fontSize: androidx.compose.ui.unit.TextUnit = 12.sp,
|
||||||
|
fontWeight: FontWeight = FontWeight.Normal
|
||||||
|
) {
|
||||||
|
var dotCount by remember { mutableIntStateOf(0) }
|
||||||
|
|
||||||
|
// Анимация точек: 0 → 1 → 2 → 3 → 0 → ...
|
||||||
|
LaunchedEffect(Unit) {
|
||||||
|
while (true) {
|
||||||
|
delay(400) // Интервал между изменениями
|
||||||
|
dotCount = (dotCount + 1) % 4
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val dots = ".".repeat(dotCount)
|
||||||
|
// Добавляем невидимые точки для фиксированной ширины текста
|
||||||
|
val invisibleDots = ".".repeat(3 - dotCount)
|
||||||
|
|
||||||
|
Row {
|
||||||
|
Text(
|
||||||
|
text = "$baseText$dots",
|
||||||
|
fontSize = fontSize,
|
||||||
|
fontWeight = fontWeight,
|
||||||
|
color = color
|
||||||
|
)
|
||||||
|
// Невидимые точки для сохранения ширины
|
||||||
|
Text(
|
||||||
|
text = invisibleDots,
|
||||||
|
fontSize = fontSize,
|
||||||
|
fontWeight = fontWeight,
|
||||||
|
color = Color.Transparent
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 🖼️ Глобальный LRU кэш для bitmap'ов изображений
|
* 🖼️ Глобальный LRU кэш для bitmap'ов изображений
|
||||||
* Это предотвращает мигание при перезаходе в диалог,
|
* Это предотвращает мигание при перезаходе в диалог,
|
||||||
@@ -1315,25 +1358,41 @@ fun FileAttachment(
|
|||||||
|
|
||||||
// Размер файла и тип
|
// Размер файла и тип
|
||||||
val fileExtension = fileName.substringAfterLast('.', "").uppercase()
|
val fileExtension = fileName.substringAfterLast('.', "").uppercase()
|
||||||
Text(
|
val statusColor = if (downloadStatus == DownloadStatus.ERROR) {
|
||||||
text =
|
|
||||||
when (downloadStatus) {
|
|
||||||
DownloadStatus.DOWNLOADING -> "Downloading..."
|
|
||||||
DownloadStatus.DECRYPTING -> "Decrypting..."
|
|
||||||
DownloadStatus.ERROR -> "File expired"
|
|
||||||
else -> "${formatFileSize(fileSize)} $fileExtension"
|
|
||||||
},
|
|
||||||
fontSize = 12.sp,
|
|
||||||
color =
|
|
||||||
if (downloadStatus == DownloadStatus.ERROR) {
|
|
||||||
Color(0xFFE53935)
|
Color(0xFFE53935)
|
||||||
} else if (isOutgoing) {
|
} else if (isOutgoing) {
|
||||||
Color.White.copy(alpha = 0.7f)
|
Color.White.copy(alpha = 0.7f)
|
||||||
} else {
|
} else {
|
||||||
PrimaryBlue
|
PrimaryBlue
|
||||||
}
|
}
|
||||||
|
|
||||||
|
when (downloadStatus) {
|
||||||
|
DownloadStatus.DOWNLOADING -> {
|
||||||
|
AnimatedDotsText(
|
||||||
|
baseText = "Downloading",
|
||||||
|
color = statusColor,
|
||||||
|
fontSize = 12.sp
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
DownloadStatus.DECRYPTING -> {
|
||||||
|
AnimatedDotsText(
|
||||||
|
baseText = "Decrypting",
|
||||||
|
color = statusColor,
|
||||||
|
fontSize = 12.sp
|
||||||
|
)
|
||||||
|
}
|
||||||
|
else -> {
|
||||||
|
Text(
|
||||||
|
text = when (downloadStatus) {
|
||||||
|
DownloadStatus.ERROR -> "File expired"
|
||||||
|
else -> "${formatFileSize(fileSize)} $fileExtension"
|
||||||
|
},
|
||||||
|
fontSize = 12.sp,
|
||||||
|
color = statusColor
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Time and checkmarks (bottom-right overlay) for outgoing files
|
// Time and checkmarks (bottom-right overlay) for outgoing files
|
||||||
@@ -1692,20 +1751,36 @@ fun AvatarAttachment(
|
|||||||
Spacer(modifier = Modifier.height(2.dp))
|
Spacer(modifier = Modifier.height(2.dp))
|
||||||
|
|
||||||
// Описание статуса
|
// Описание статуса
|
||||||
Text(
|
val avatarStatusColor = if (isOutgoing) Color.White.copy(alpha = 0.7f)
|
||||||
text =
|
else (if (isDarkTheme) Color.White.copy(alpha = 0.5f) else Color.Gray)
|
||||||
|
|
||||||
when (downloadStatus) {
|
when (downloadStatus) {
|
||||||
DownloadStatus.DOWNLOADING -> "Downloading..."
|
DownloadStatus.DOWNLOADING -> {
|
||||||
DownloadStatus.DECRYPTING -> "Decrypting..."
|
AnimatedDotsText(
|
||||||
|
baseText = "Downloading",
|
||||||
|
color = avatarStatusColor,
|
||||||
|
fontSize = 13.sp
|
||||||
|
)
|
||||||
|
}
|
||||||
|
DownloadStatus.DECRYPTING -> {
|
||||||
|
AnimatedDotsText(
|
||||||
|
baseText = "Decrypting",
|
||||||
|
color = avatarStatusColor,
|
||||||
|
fontSize = 13.sp
|
||||||
|
)
|
||||||
|
}
|
||||||
|
else -> {
|
||||||
|
Text(
|
||||||
|
text = when (downloadStatus) {
|
||||||
DownloadStatus.ERROR -> "Tap to retry"
|
DownloadStatus.ERROR -> "Tap to retry"
|
||||||
DownloadStatus.DOWNLOADED -> "Shared profile photo"
|
DownloadStatus.DOWNLOADED -> "Shared profile photo"
|
||||||
else -> "Tap to download"
|
else -> "Tap to download"
|
||||||
},
|
},
|
||||||
fontSize = 13.sp,
|
fontSize = 13.sp,
|
||||||
color =
|
color = avatarStatusColor
|
||||||
if (isOutgoing) Color.White.copy(alpha = 0.7f)
|
|
||||||
else (if (isDarkTheme) Color.White.copy(alpha = 0.5f) else Color.Gray)
|
|
||||||
)
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Spacer(modifier = Modifier.height(4.dp))
|
Spacer(modifier = Modifier.height(4.dp))
|
||||||
|
|
||||||
|
|||||||
@@ -626,7 +626,8 @@ fun MessageBubble(
|
|||||||
text = message.text,
|
text = message.text,
|
||||||
color = textColor,
|
color = textColor,
|
||||||
fontSize = 16.sp,
|
fontSize = 16.sp,
|
||||||
linkColor = linkColor
|
linkColor = linkColor,
|
||||||
|
onLongClick = onLongClick // 🔥 Long press для selection
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
timeContent = {
|
timeContent = {
|
||||||
@@ -673,7 +674,8 @@ fun MessageBubble(
|
|||||||
text = message.text,
|
text = message.text,
|
||||||
color = textColor,
|
color = textColor,
|
||||||
fontSize = 17.sp,
|
fontSize = 17.sp,
|
||||||
linkColor = linkColor
|
linkColor = linkColor,
|
||||||
|
onLongClick = onLongClick // 🔥 Long press для selection
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
timeContent = {
|
timeContent = {
|
||||||
@@ -701,7 +703,8 @@ fun MessageBubble(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
|
fillWidth = true // 🔥 Время справа с учётом ширины ReplyBubble
|
||||||
)
|
)
|
||||||
} else if (!hasOnlyMedia && !hasImageWithCaption && message.text.isNotEmpty()) {
|
} else if (!hasOnlyMedia && !hasImageWithCaption && message.text.isNotEmpty()) {
|
||||||
// Telegram-style: текст + время с автоматическим переносом
|
// Telegram-style: текст + время с автоматическим переносом
|
||||||
@@ -711,7 +714,8 @@ fun MessageBubble(
|
|||||||
text = message.text,
|
text = message.text,
|
||||||
color = textColor,
|
color = textColor,
|
||||||
fontSize = 17.sp,
|
fontSize = 17.sp,
|
||||||
linkColor = linkColor
|
linkColor = linkColor,
|
||||||
|
onLongClick = onLongClick // 🔥 Long press для selection
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
timeContent = {
|
timeContent = {
|
||||||
|
|||||||
@@ -36,6 +36,9 @@ import androidx.compose.runtime.*
|
|||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.draw.clip
|
import androidx.compose.ui.draw.clip
|
||||||
|
import androidx.compose.ui.input.pointer.pointerInput
|
||||||
|
import androidx.compose.foundation.gestures.awaitEachGesture
|
||||||
|
import androidx.compose.foundation.gestures.awaitFirstDown
|
||||||
import androidx.compose.ui.graphics.Brush
|
import androidx.compose.ui.graphics.Brush
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
import androidx.compose.ui.graphics.asImageBitmap
|
import androidx.compose.ui.graphics.asImageBitmap
|
||||||
@@ -387,6 +390,13 @@ fun ImageEditorScreen(
|
|||||||
Box(
|
Box(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxSize()
|
.fillMaxSize()
|
||||||
|
// 🔥 Блокируем свайпы от SwipeBackContainer - на ImageEditor свайпы не должны работать
|
||||||
|
.pointerInput(Unit) {
|
||||||
|
awaitEachGesture {
|
||||||
|
val down = awaitFirstDown(requireUnconsumed = false)
|
||||||
|
down.consume() // Поглощаем все touch события
|
||||||
|
}
|
||||||
|
}
|
||||||
.graphicsLayer { alpha = animationProgress.value } // ⚡ Всё плавно fade
|
.graphicsLayer { alpha = animationProgress.value } // ⚡ Всё плавно fade
|
||||||
.background(Color.Black)
|
.background(Color.Black)
|
||||||
) {
|
) {
|
||||||
@@ -1067,33 +1077,20 @@ private fun TelegramCaptionBar(
|
|||||||
horizontalArrangement = Arrangement.spacedBy(10.dp)
|
horizontalArrangement = Arrangement.spacedBy(10.dp)
|
||||||
) {
|
) {
|
||||||
// 🎬 Левая иконка: плавная анимация через alpha
|
// 🎬 Левая иконка: плавная анимация через alpha
|
||||||
// Камера (compact) ↔ Emoji/Keyboard (expanded)
|
// Emoji/Keyboard toggle - всегда виден
|
||||||
Box(
|
Box(
|
||||||
modifier = Modifier.size(32.dp),
|
modifier = Modifier.size(32.dp),
|
||||||
contentAlignment = Alignment.Center
|
contentAlignment = Alignment.Center
|
||||||
) {
|
) {
|
||||||
// Камера - видна когда progress близок к 0
|
// Emoji/Keyboard toggle
|
||||||
Icon(
|
|
||||||
TablerIcons.CameraPlus,
|
|
||||||
contentDescription = "Camera",
|
|
||||||
tint = Color.White.copy(alpha = 0.7f * (1f - progress)),
|
|
||||||
modifier = Modifier
|
|
||||||
.size(26.dp)
|
|
||||||
.graphicsLayer { alpha = 1f - progress }
|
|
||||||
)
|
|
||||||
|
|
||||||
// Emoji/Keyboard toggle - виден когда progress близок к 1
|
|
||||||
IconButton(
|
IconButton(
|
||||||
onClick = onToggleEmojiPicker,
|
onClick = onToggleEmojiPicker,
|
||||||
modifier = Modifier
|
modifier = Modifier.size(32.dp)
|
||||||
.size(32.dp)
|
|
||||||
.graphicsLayer { alpha = progress },
|
|
||||||
enabled = progress > 0.5f // Кликабелен только когда достаточно виден
|
|
||||||
) {
|
) {
|
||||||
Icon(
|
Icon(
|
||||||
if (showEmojiPicker) TablerIcons.Keyboard else TablerIcons.MoodSmile,
|
if (showEmojiPicker) TablerIcons.Keyboard else TablerIcons.MoodSmile,
|
||||||
contentDescription = if (showEmojiPicker) "Keyboard" else "Emoji",
|
contentDescription = if (showEmojiPicker) "Keyboard" else "Emoji",
|
||||||
tint = Color.White.copy(alpha = 0.7f * progress),
|
tint = Color.White.copy(alpha = 0.7f),
|
||||||
modifier = Modifier.size(26.dp)
|
modifier = Modifier.size(26.dp)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -69,6 +69,24 @@ private const val TAG = "ImageViewerScreen"
|
|||||||
*/
|
*/
|
||||||
private val TelegramEasing = CubicBezierEasing(0.25f, 0.1f, 0.25f, 1.0f)
|
private val TelegramEasing = CubicBezierEasing(0.25f, 0.1f, 0.25f, 1.0f)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Linear interpolation helper
|
||||||
|
*/
|
||||||
|
private fun lerp(start: Float, stop: Float, fraction: Float): Float {
|
||||||
|
return start + (stop - start) * fraction
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Transform data for shared element animation
|
||||||
|
*/
|
||||||
|
private data class ImageTransform(
|
||||||
|
val scaleX: Float,
|
||||||
|
val scaleY: Float,
|
||||||
|
val translationX: Float,
|
||||||
|
val translationY: Float,
|
||||||
|
val cornerRadius: Float
|
||||||
|
)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Данные об источнике изображения для shared element transition
|
* Данные об источнике изображения для shared element transition
|
||||||
*/
|
*/
|
||||||
@@ -196,8 +214,47 @@ fun ImageViewerScreen(
|
|||||||
var showControls by remember { mutableStateOf(true) }
|
var showControls by remember { mutableStateOf(true) }
|
||||||
|
|
||||||
// ═══════════════════════════════════════════════════════════
|
// ═══════════════════════════════════════════════════════════
|
||||||
|
// 🎬 SHARED ELEMENT TRANSFORM - вычисляем трансформацию
|
||||||
// ═══════════════════════════════════════════════════════════
|
// ═══════════════════════════════════════════════════════════
|
||||||
// 🎬 ПРОСТОЙ FADE-OUT при свайпе
|
val imageTransform by remember(sourceBounds, screenSize, animationProgress.value) {
|
||||||
|
derivedStateOf {
|
||||||
|
if (sourceBounds != null && screenSize != IntSize.Zero) {
|
||||||
|
val progress = animationProgress.value
|
||||||
|
|
||||||
|
// Вычисляем scale для перехода от источника к fullscreen
|
||||||
|
val sourceScaleX = sourceBounds.width / screenSize.width.toFloat()
|
||||||
|
val sourceScaleY = sourceBounds.height / screenSize.height.toFloat()
|
||||||
|
|
||||||
|
val currentScaleX = lerp(sourceScaleX, 1f, progress)
|
||||||
|
val currentScaleY = lerp(sourceScaleY, 1f, progress)
|
||||||
|
|
||||||
|
// Вычисляем translation для центрирования
|
||||||
|
val targetCenterX = screenSize.width / 2f
|
||||||
|
val targetCenterY = screenSize.height / 2f
|
||||||
|
val sourceCenterX = sourceBounds.left + sourceBounds.width / 2f
|
||||||
|
val sourceCenterY = sourceBounds.top + sourceBounds.height / 2f
|
||||||
|
|
||||||
|
val currentTranslationX = lerp(sourceCenterX - targetCenterX, 0f, progress)
|
||||||
|
val currentTranslationY = lerp(sourceCenterY - targetCenterY, 0f, progress)
|
||||||
|
|
||||||
|
// Скругление углов: от sourceBounds.cornerRadius до 0
|
||||||
|
val currentCornerRadius = lerp(sourceBounds.cornerRadius, 0f, progress)
|
||||||
|
|
||||||
|
ImageTransform(
|
||||||
|
scaleX = currentScaleX,
|
||||||
|
scaleY = currentScaleY,
|
||||||
|
translationX = currentTranslationX,
|
||||||
|
translationY = currentTranslationY,
|
||||||
|
cornerRadius = currentCornerRadius
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
ImageTransform(1f, 1f, 0f, 0f, 0f)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ═══════════════════════════════════════════════════════════
|
||||||
|
// 🎬 SWIPE TO DISMISS
|
||||||
// ═══════════════════════════════════════════════════════════
|
// ═══════════════════════════════════════════════════════════
|
||||||
var dragOffsetY by remember { mutableFloatStateOf(0f) }
|
var dragOffsetY by remember { mutableFloatStateOf(0f) }
|
||||||
var isDragging by remember { mutableStateOf(false) }
|
var isDragging by remember { mutableStateOf(false) }
|
||||||
@@ -216,21 +273,35 @@ fun ImageViewerScreen(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 🎬 Простой fade-out dismiss
|
// 🎬 Shared element dismiss - возврат к исходному положению
|
||||||
fun smoothDismiss() {
|
fun smoothDismiss() {
|
||||||
if (isClosing) return
|
if (isClosing) return
|
||||||
isClosing = true
|
isClosing = true
|
||||||
onClosingStart() // Сразу сбрасываем status bar
|
onClosingStart() // Сразу сбрасываем status bar
|
||||||
|
|
||||||
scope.launch {
|
scope.launch {
|
||||||
// Плавно исчезаем - более длинная анимация
|
// Сначала сбрасываем offset от drag
|
||||||
|
animatedOffsetY.snapTo(0f)
|
||||||
|
|
||||||
|
if (sourceBounds != null && pagerState.currentPage == initialIndex) {
|
||||||
|
// 🔥 Telegram-style: возвращаемся к исходному положению с shared element
|
||||||
|
animationProgress.animateTo(
|
||||||
|
targetValue = 0f,
|
||||||
|
animationSpec = tween(
|
||||||
|
durationMillis = 280,
|
||||||
|
easing = FastOutSlowInEasing
|
||||||
|
)
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
// Fallback: простой fade-out если пролистнули на другое фото
|
||||||
dismissAlpha.animateTo(
|
dismissAlpha.animateTo(
|
||||||
targetValue = 0f,
|
targetValue = 0f,
|
||||||
animationSpec = tween(
|
animationSpec = tween(
|
||||||
durationMillis = 350,
|
durationMillis = 250,
|
||||||
easing = LinearEasing
|
easing = LinearEasing
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
}
|
||||||
onDismiss()
|
onDismiss()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -269,6 +340,11 @@ fun ImageViewerScreen(
|
|||||||
// ═══════════════════════════════════════════════════════════
|
// ═══════════════════════════════════════════════════════════
|
||||||
// 📸 HORIZONTAL PAGER с Telegram-style эффектом наслаивания
|
// 📸 HORIZONTAL PAGER с Telegram-style эффектом наслаивания
|
||||||
// ═══════════════════════════════════════════════════════════
|
// ═══════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
// 🔥 Применяем shared element transform только для текущей страницы при анимации
|
||||||
|
val isAnimating = animationProgress.value < 1f
|
||||||
|
val shouldApplyTransform = isAnimating && pagerState.currentPage == initialIndex
|
||||||
|
|
||||||
HorizontalPager(
|
HorizontalPager(
|
||||||
state = pagerState,
|
state = pagerState,
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
@@ -276,9 +352,25 @@ fun ImageViewerScreen(
|
|||||||
.graphicsLayer {
|
.graphicsLayer {
|
||||||
alpha = dismissAlpha.value
|
alpha = dismissAlpha.value
|
||||||
translationY = animatedOffsetY.value
|
translationY = animatedOffsetY.value
|
||||||
},
|
|
||||||
|
// 🔥 Shared element transform при входе/выходе
|
||||||
|
if (shouldApplyTransform) {
|
||||||
|
scaleX = imageTransform.scaleX
|
||||||
|
scaleY = imageTransform.scaleY
|
||||||
|
translationX = imageTransform.translationX
|
||||||
|
translationY = imageTransform.translationY
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.then(
|
||||||
|
if (shouldApplyTransform && imageTransform.cornerRadius > 0f) {
|
||||||
|
Modifier.clip(RoundedCornerShape(imageTransform.cornerRadius.dp))
|
||||||
|
} else {
|
||||||
|
Modifier
|
||||||
|
}
|
||||||
|
),
|
||||||
pageSpacing = 30.dp, // Telegram: dp(30) между фото
|
pageSpacing = 30.dp, // Telegram: dp(30) между фото
|
||||||
key = { images[it].attachmentId }
|
key = { images[it].attachmentId },
|
||||||
|
userScrollEnabled = !isAnimating // Отключаем скролл во время анимации
|
||||||
) { page ->
|
) { page ->
|
||||||
val image = images[page]
|
val image = images[page]
|
||||||
|
|
||||||
|
|||||||
@@ -59,6 +59,7 @@ import androidx.camera.core.Preview
|
|||||||
import androidx.camera.lifecycle.ProcessCameraProvider
|
import androidx.camera.lifecycle.ProcessCameraProvider
|
||||||
import androidx.camera.view.PreviewView
|
import androidx.camera.view.PreviewView
|
||||||
import androidx.core.content.ContextCompat
|
import androidx.core.content.ContextCompat
|
||||||
|
import androidx.core.view.WindowCompat
|
||||||
import coil.compose.AsyncImage
|
import coil.compose.AsyncImage
|
||||||
import coil.request.ImageRequest
|
import coil.request.ImageRequest
|
||||||
import compose.icons.TablerIcons
|
import compose.icons.TablerIcons
|
||||||
@@ -364,23 +365,38 @@ fun MediaPickerBottomSheet(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 🎨 Затемнение статус бара когда галерея открыта
|
// 🎨 Затемнение статус бара и навигейшн бара когда галерея открыта
|
||||||
val view = LocalView.current
|
val view = LocalView.current
|
||||||
|
|
||||||
|
// 🔥 Контроллер для управления цветом иконок navigation bar
|
||||||
|
val insetsController = remember(view) {
|
||||||
|
val window = (view.context as? android.app.Activity)?.window
|
||||||
|
window?.let { WindowCompat.getInsetsController(it, view) }
|
||||||
|
}
|
||||||
|
|
||||||
DisposableEffect(shouldShow, scrimAlpha) {
|
DisposableEffect(shouldShow, scrimAlpha) {
|
||||||
if (shouldShow && !view.isInEditMode) {
|
if (shouldShow && !view.isInEditMode) {
|
||||||
val window = (view.context as? android.app.Activity)?.window
|
val window = (view.context as? android.app.Activity)?.window
|
||||||
val originalStatusBarColor = window?.statusBarColor ?: 0
|
val originalStatusBarColor = window?.statusBarColor ?: 0
|
||||||
|
val originalNavigationBarColor = window?.navigationBarColor ?: 0
|
||||||
|
val originalLightNavigationBars = insetsController?.isAppearanceLightNavigationBars ?: true
|
||||||
|
|
||||||
// Затемняем статус бар
|
// Затемняем статус бар и навигейшн бар
|
||||||
val scrimColor = android.graphics.Color.argb(
|
val scrimColor = android.graphics.Color.argb(
|
||||||
(scrimAlpha * 255).toInt().coerceIn(0, 255),
|
(scrimAlpha * 255).toInt().coerceIn(0, 255),
|
||||||
0, 0, 0
|
0, 0, 0
|
||||||
)
|
)
|
||||||
window?.statusBarColor = scrimColor
|
window?.statusBarColor = scrimColor
|
||||||
|
window?.navigationBarColor = scrimColor
|
||||||
|
|
||||||
|
// 🔥 Иконки навигейшн бара светлые (белые) когда scrim виден
|
||||||
|
insetsController?.isAppearanceLightNavigationBars = scrimAlpha < 0.15f && originalLightNavigationBars
|
||||||
|
|
||||||
onDispose {
|
onDispose {
|
||||||
// Восстанавливаем оригинальный цвет
|
// Восстанавливаем оригинальные цвета
|
||||||
window?.statusBarColor = originalStatusBarColor
|
window?.statusBarColor = originalStatusBarColor
|
||||||
|
window?.navigationBarColor = originalNavigationBarColor
|
||||||
|
insetsController?.isAppearanceLightNavigationBars = originalLightNavigationBars
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
onDispose { }
|
onDispose { }
|
||||||
@@ -413,6 +429,15 @@ fun MediaPickerBottomSheet(
|
|||||||
dismissOnClickOutside = false
|
dismissOnClickOutside = false
|
||||||
)
|
)
|
||||||
) {
|
) {
|
||||||
|
// 🔥 Получаем высоту navigation bar и IME (клавиатуры)
|
||||||
|
val navigationBarHeight = WindowInsets.navigationBars.asPaddingValues().calculateBottomPadding()
|
||||||
|
val imeHeight = WindowInsets.ime.asPaddingValues().calculateBottomPadding()
|
||||||
|
val navigationBarHeightPx = with(density) { navigationBarHeight.toPx().toInt() }
|
||||||
|
val imeHeightPx = with(density) { imeHeight.toPx().toInt() }
|
||||||
|
|
||||||
|
// 🔥 Если клавиатура открыта - не добавляем navigationBar offset (клавиатура его перекрывает)
|
||||||
|
val bottomOffset = if (imeHeightPx > 0) 0 else navigationBarHeightPx
|
||||||
|
|
||||||
// Полноэкранный контейнер с мягким затемнением
|
// Полноэкранный контейнер с мягким затемнением
|
||||||
Box(
|
Box(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
@@ -426,7 +451,8 @@ fun MediaPickerBottomSheet(
|
|||||||
) {
|
) {
|
||||||
// Sheet content
|
// Sheet content
|
||||||
val currentHeightDp = with(density) { sheetHeightPx.value.toDp() }
|
val currentHeightDp = with(density) { sheetHeightPx.value.toDp() }
|
||||||
val slideOffset = (sheetHeightPx.value * animatedOffset).toInt()
|
// 🔥 Добавляем смещение вниз только если клавиатура закрыта
|
||||||
|
val slideOffset = (sheetHeightPx.value * animatedOffset).toInt() + bottomOffset
|
||||||
|
|
||||||
// Отслеживаем velocity для плавного snap
|
// Отслеживаем velocity для плавного snap
|
||||||
var lastDragVelocity by remember { mutableFloatStateOf(0f) }
|
var lastDragVelocity by remember { mutableFloatStateOf(0f) }
|
||||||
|
|||||||
@@ -17,7 +17,9 @@ import android.text.style.ImageSpan
|
|||||||
import android.util.AttributeSet
|
import android.util.AttributeSet
|
||||||
import android.util.LruCache
|
import android.util.LruCache
|
||||||
import android.util.Patterns
|
import android.util.Patterns
|
||||||
|
import android.view.GestureDetector
|
||||||
import android.view.Gravity
|
import android.view.Gravity
|
||||||
|
import android.view.MotionEvent
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.view.inputmethod.EditorInfo
|
import android.view.inputmethod.EditorInfo
|
||||||
import android.widget.EditText
|
import android.widget.EditText
|
||||||
@@ -325,7 +327,8 @@ fun AppleEmojiText(
|
|||||||
maxLines: Int = Int.MAX_VALUE,
|
maxLines: Int = Int.MAX_VALUE,
|
||||||
overflow: android.text.TextUtils.TruncateAt? = null,
|
overflow: android.text.TextUtils.TruncateAt? = null,
|
||||||
linkColor: androidx.compose.ui.graphics.Color = androidx.compose.ui.graphics.Color(0xFF54A9EB), // Telegram-style blue
|
linkColor: androidx.compose.ui.graphics.Color = androidx.compose.ui.graphics.Color(0xFF54A9EB), // Telegram-style blue
|
||||||
enableLinks: Boolean = true // 🔥 Включить кликабельные ссылки
|
enableLinks: Boolean = true, // 🔥 Включить кликабельные ссылки
|
||||||
|
onLongClick: (() -> Unit)? = null // 🔥 Callback для long press (selection в MessageBubble)
|
||||||
) {
|
) {
|
||||||
val fontSizeValue = if (fontSize == androidx.compose.ui.unit.TextUnit.Unspecified) 15f
|
val fontSizeValue = if (fontSize == androidx.compose.ui.unit.TextUnit.Unspecified) 15f
|
||||||
else fontSize.value
|
else fontSize.value
|
||||||
@@ -355,10 +358,13 @@ fun AppleEmojiText(
|
|||||||
if (overflow != null) {
|
if (overflow != null) {
|
||||||
ellipsize = overflow
|
ellipsize = overflow
|
||||||
}
|
}
|
||||||
// 🔥 Включаем кликабельные ссылки
|
// 🔥 Включаем кликабельные ссылки с поддержкой long press
|
||||||
if (enableLinks) {
|
if (enableLinks) {
|
||||||
setLinkColor(linkColor.toArgb())
|
setLinkColor(linkColor.toArgb())
|
||||||
enableClickableLinks(true)
|
enableClickableLinks(true, onLongClick)
|
||||||
|
} else {
|
||||||
|
// 🔥 Даже без ссылок поддерживаем long press
|
||||||
|
onLongClickCallback = onLongClick
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -371,10 +377,13 @@ fun AppleEmojiText(
|
|||||||
if (overflow != null) {
|
if (overflow != null) {
|
||||||
view.ellipsize = overflow
|
view.ellipsize = overflow
|
||||||
}
|
}
|
||||||
// 🔥 Обновляем настройки ссылок
|
// 🔥 Обновляем настройки ссылок с поддержкой long press
|
||||||
if (enableLinks) {
|
if (enableLinks) {
|
||||||
view.setLinkColor(linkColor.toArgb())
|
view.setLinkColor(linkColor.toArgb())
|
||||||
view.enableClickableLinks(true)
|
view.enableClickableLinks(true, onLongClick)
|
||||||
|
} else {
|
||||||
|
// 🔥 Даже без ссылок поддерживаем long press
|
||||||
|
view.onLongClickCallback = onLongClick
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
modifier = modifier
|
modifier = modifier
|
||||||
@@ -415,11 +424,32 @@ class AppleEmojiTextView @JvmOverloads constructor(
|
|||||||
private var linkColorValue: Int = 0xFF54A9EB.toInt() // Default Telegram blue
|
private var linkColorValue: Int = 0xFF54A9EB.toInt() // Default Telegram blue
|
||||||
private var linksEnabled: Boolean = false
|
private var linksEnabled: Boolean = false
|
||||||
|
|
||||||
|
// 🔥 Long press callback для selection в MessageBubble
|
||||||
|
var onLongClickCallback: (() -> Unit)? = null
|
||||||
|
|
||||||
|
// 🔥 GestureDetector для обработки long press поверх LinkMovementMethod
|
||||||
|
private val gestureDetector = GestureDetector(context, object : GestureDetector.SimpleOnGestureListener() {
|
||||||
|
override fun onLongPress(e: MotionEvent) {
|
||||||
|
onLongClickCallback?.invoke()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
init {
|
init {
|
||||||
// Отключаем лишние отступы шрифта для корректного отображения emoji
|
// Отключаем лишние отступы шрифта для корректного отображения emoji
|
||||||
includeFontPadding = false
|
includeFontPadding = false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 🔥 Перехватываем touch события для обработки long press
|
||||||
|
* GestureDetector обрабатывает long press, затем передаем событие parent для ссылок
|
||||||
|
*/
|
||||||
|
override fun dispatchTouchEvent(event: MotionEvent): Boolean {
|
||||||
|
// Позволяем GestureDetector обработать событие (для long press)
|
||||||
|
gestureDetector.onTouchEvent(event)
|
||||||
|
// Передаем событие дальше для обработки ссылок
|
||||||
|
return super.dispatchTouchEvent(event)
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 🔥 Установить цвет для ссылок
|
* 🔥 Установить цвет для ссылок
|
||||||
*/
|
*/
|
||||||
@@ -429,9 +459,12 @@ class AppleEmojiTextView @JvmOverloads constructor(
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* 🔥 Включить/выключить кликабельные ссылки
|
* 🔥 Включить/выключить кликабельные ссылки
|
||||||
|
* @param enable - включить ссылки
|
||||||
|
* @param onLongClick - callback для long press (для selection в MessageBubble)
|
||||||
*/
|
*/
|
||||||
fun enableClickableLinks(enable: Boolean) {
|
fun enableClickableLinks(enable: Boolean, onLongClick: (() -> Unit)? = null) {
|
||||||
linksEnabled = enable
|
linksEnabled = enable
|
||||||
|
onLongClickCallback = onLongClick
|
||||||
if (enable) {
|
if (enable) {
|
||||||
movementMethod = LinkMovementMethod.getInstance()
|
movementMethod = LinkMovementMethod.getInstance()
|
||||||
// Убираем highlight при клике
|
// Убираем highlight при клике
|
||||||
|
|||||||
@@ -76,8 +76,8 @@ fun AvatarImage(
|
|||||||
var bitmap by remember { mutableStateOf<Bitmap?>(null) }
|
var bitmap by remember { mutableStateOf<Bitmap?>(null) }
|
||||||
|
|
||||||
// 🔥 FIX: Декодируем только когда avatarKey (timestamp) реально изменился
|
// 🔥 FIX: Декодируем только когда avatarKey (timestamp) реально изменился
|
||||||
// НЕ сбрасываем bitmap в null - показываем старый пока грузится новый
|
// Сбрасываем bitmap в null ТОЛЬКО когда аватар удалён (avatars пустой)
|
||||||
LaunchedEffect(avatarKey) {
|
LaunchedEffect(avatarKey, avatars.isEmpty()) {
|
||||||
val currentAvatars = avatars
|
val currentAvatars = avatars
|
||||||
if (currentAvatars.isNotEmpty()) {
|
if (currentAvatars.isNotEmpty()) {
|
||||||
val newBitmap = withContext(Dispatchers.IO) {
|
val newBitmap = withContext(Dispatchers.IO) {
|
||||||
@@ -87,9 +87,10 @@ fun AvatarImage(
|
|||||||
if (newBitmap != null) {
|
if (newBitmap != null) {
|
||||||
bitmap = newBitmap
|
bitmap = newBitmap
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
// 🔥 FIX: Если аватары удалены - сбрасываем bitmap чтобы показался placeholder
|
||||||
|
bitmap = null
|
||||||
}
|
}
|
||||||
// Если avatars пустой - НЕ сбрасываем bitmap в null
|
|
||||||
// Placeholder покажется через условие bitmap == null ниже
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Box(
|
Box(
|
||||||
|
|||||||
@@ -38,7 +38,8 @@ object MessageLogger {
|
|||||||
if (!isEnabled) return
|
if (!isEnabled) return
|
||||||
val shortId = messageId.take(8)
|
val shortId = messageId.take(8)
|
||||||
val shortKey = toPublicKey.take(12)
|
val shortKey = toPublicKey.take(12)
|
||||||
val msg = "📤 SEND | id:\$shortId to:\$shortKey len:\$textLength att:\$attachmentsCount saved:\$isSavedMessages reply:\${replyToMessageId?.take(8) ?: \"-\"}"
|
val replyStr = replyToMessageId?.take(8) ?: "-"
|
||||||
|
val msg = "📤 SEND | id:$shortId to:$shortKey len:$textLength att:$attachmentsCount saved:$isSavedMessages reply:$replyStr"
|
||||||
Log.d(TAG, msg)
|
Log.d(TAG, msg)
|
||||||
addToUI(msg)
|
addToUI(msg)
|
||||||
}
|
}
|
||||||
@@ -53,7 +54,7 @@ object MessageLogger {
|
|||||||
) {
|
) {
|
||||||
if (!isEnabled) return
|
if (!isEnabled) return
|
||||||
val shortId = messageId.take(8)
|
val shortId = messageId.take(8)
|
||||||
val msg = "🔐 ENCRYPT | id:\$shortId content:\${encryptedContentLength}b key:\${encryptedKeyLength}b"
|
val msg = "🔐 ENCRYPT | id:$shortId content:${encryptedContentLength}b key:${encryptedKeyLength}b"
|
||||||
Log.d(TAG, msg)
|
Log.d(TAG, msg)
|
||||||
addToUI(msg)
|
addToUI(msg)
|
||||||
}
|
}
|
||||||
@@ -69,7 +70,7 @@ object MessageLogger {
|
|||||||
if (!isEnabled) return
|
if (!isEnabled) return
|
||||||
val shortId = messageId.take(8)
|
val shortId = messageId.take(8)
|
||||||
val shortDialog = dialogKey.take(12)
|
val shortDialog = dialogKey.take(12)
|
||||||
val msg = "💾 DB | id:\$shortId dialog:\$shortDialog new:\$isNew"
|
val msg = "💾 DB | id:$shortId dialog:$shortDialog new:$isNew"
|
||||||
Log.d(TAG, msg)
|
Log.d(TAG, msg)
|
||||||
addToUI(msg)
|
addToUI(msg)
|
||||||
}
|
}
|
||||||
@@ -85,7 +86,7 @@ object MessageLogger {
|
|||||||
if (!isEnabled) return
|
if (!isEnabled) return
|
||||||
val shortId = messageId.take(8)
|
val shortId = messageId.take(8)
|
||||||
val shortKey = toPublicKey.take(12)
|
val shortKey = toPublicKey.take(12)
|
||||||
val msg = "📡 PACKET→ | id:\$shortId to:\$shortKey ts:\$timestamp"
|
val msg = "📡 PACKET→ | id:$shortId to:$shortKey ts:$timestamp"
|
||||||
Log.d(TAG, msg)
|
Log.d(TAG, msg)
|
||||||
addToUI(msg)
|
addToUI(msg)
|
||||||
}
|
}
|
||||||
@@ -96,7 +97,7 @@ object MessageLogger {
|
|||||||
fun logSendSuccess(messageId: String, duration: Long) {
|
fun logSendSuccess(messageId: String, duration: Long) {
|
||||||
if (!isEnabled) return
|
if (!isEnabled) return
|
||||||
val shortId = messageId.take(8)
|
val shortId = messageId.take(8)
|
||||||
val msg = "✅ SENT | id:\$shortId time:\${duration}ms"
|
val msg = "✅ SENT | id:$shortId time:${duration}ms"
|
||||||
Log.d(TAG, msg)
|
Log.d(TAG, msg)
|
||||||
addToUI(msg)
|
addToUI(msg)
|
||||||
}
|
}
|
||||||
@@ -107,7 +108,8 @@ object MessageLogger {
|
|||||||
fun logSendError(messageId: String, error: Throwable) {
|
fun logSendError(messageId: String, error: Throwable) {
|
||||||
if (!isEnabled) return
|
if (!isEnabled) return
|
||||||
val shortId = messageId.take(8)
|
val shortId = messageId.take(8)
|
||||||
val msg = "❌ SEND ERR | id:\$shortId err:\${error.message?.take(50)}"
|
val errMsg = error.message?.take(50) ?: "unknown"
|
||||||
|
val msg = "❌ SEND ERR | id:$shortId err:$errMsg"
|
||||||
Log.e(TAG, msg, error)
|
Log.e(TAG, msg, error)
|
||||||
addToUI(msg)
|
addToUI(msg)
|
||||||
}
|
}
|
||||||
@@ -127,7 +129,7 @@ object MessageLogger {
|
|||||||
val shortId = messageId.take(8)
|
val shortId = messageId.take(8)
|
||||||
val shortFrom = fromPublicKey.take(12)
|
val shortFrom = fromPublicKey.take(12)
|
||||||
val shortTo = toPublicKey.take(12)
|
val shortTo = toPublicKey.take(12)
|
||||||
val msg = "📥 RECV | id:\$shortId from:\$shortFrom to:\$shortTo len:\${contentLength}b att:\$attachmentsCount ts:\$timestamp"
|
val msg = "📥 RECV | id:$shortId from:$shortFrom to:$shortTo len:${contentLength}b att:$attachmentsCount ts:$timestamp"
|
||||||
Log.d(TAG, msg)
|
Log.d(TAG, msg)
|
||||||
addToUI(msg)
|
addToUI(msg)
|
||||||
}
|
}
|
||||||
@@ -139,7 +141,7 @@ object MessageLogger {
|
|||||||
if (!isEnabled) return
|
if (!isEnabled) return
|
||||||
val shortId = messageId.take(8)
|
val shortId = messageId.take(8)
|
||||||
val status = if (isDuplicate) "⚠️DUP" else "✓NEW"
|
val status = if (isDuplicate) "⚠️DUP" else "✓NEW"
|
||||||
val msg = "🔍 CHECK | id:\$shortId \$status"
|
val msg = "🔍 CHECK | id:$shortId $status"
|
||||||
Log.d(TAG, msg)
|
Log.d(TAG, msg)
|
||||||
addToUI(msg)
|
addToUI(msg)
|
||||||
}
|
}
|
||||||
@@ -150,7 +152,7 @@ object MessageLogger {
|
|||||||
fun logBlockedSender(fromPublicKey: String) {
|
fun logBlockedSender(fromPublicKey: String) {
|
||||||
if (!isEnabled) return
|
if (!isEnabled) return
|
||||||
val shortKey = fromPublicKey.take(12)
|
val shortKey = fromPublicKey.take(12)
|
||||||
val msg = "🚫 BLOCKED | from:\$shortKey"
|
val msg = "🚫 BLOCKED | from:$shortKey"
|
||||||
Log.d(TAG, msg)
|
Log.d(TAG, msg)
|
||||||
addToUI(msg)
|
addToUI(msg)
|
||||||
}
|
}
|
||||||
@@ -165,7 +167,7 @@ object MessageLogger {
|
|||||||
) {
|
) {
|
||||||
if (!isEnabled) return
|
if (!isEnabled) return
|
||||||
val shortId = messageId.take(8)
|
val shortId = messageId.take(8)
|
||||||
val msg = "🔓 DECRYPT | id:\$shortId text:\${plainTextLength}c att:\$attachmentsCount"
|
val msg = "🔓 DECRYPT | id:$shortId text:${plainTextLength}c att:$attachmentsCount"
|
||||||
Log.d(TAG, msg)
|
Log.d(TAG, msg)
|
||||||
addToUI(msg)
|
addToUI(msg)
|
||||||
}
|
}
|
||||||
@@ -176,7 +178,8 @@ object MessageLogger {
|
|||||||
fun logDecryptionError(messageId: String, error: Throwable) {
|
fun logDecryptionError(messageId: String, error: Throwable) {
|
||||||
if (!isEnabled) return
|
if (!isEnabled) return
|
||||||
val shortId = messageId.take(8)
|
val shortId = messageId.take(8)
|
||||||
val msg = "❌ DECRYPT ERR | id:\$shortId err:\${error.message?.take(50)}"
|
val errMsg = error.message?.take(50) ?: "unknown"
|
||||||
|
val msg = "❌ DECRYPT ERR | id:$shortId err:$errMsg"
|
||||||
Log.e(TAG, msg, error)
|
Log.e(TAG, msg, error)
|
||||||
addToUI(msg)
|
addToUI(msg)
|
||||||
}
|
}
|
||||||
@@ -187,7 +190,7 @@ object MessageLogger {
|
|||||||
fun logReceiveSuccess(messageId: String, duration: Long) {
|
fun logReceiveSuccess(messageId: String, duration: Long) {
|
||||||
if (!isEnabled) return
|
if (!isEnabled) return
|
||||||
val shortId = messageId.take(8)
|
val shortId = messageId.take(8)
|
||||||
val msg = "✅ RECEIVED | id:\$shortId time:\${duration}ms"
|
val msg = "✅ RECEIVED | id:$shortId time:${duration}ms"
|
||||||
Log.d(TAG, msg)
|
Log.d(TAG, msg)
|
||||||
addToUI(msg)
|
addToUI(msg)
|
||||||
}
|
}
|
||||||
@@ -203,7 +206,7 @@ object MessageLogger {
|
|||||||
if (!isEnabled) return
|
if (!isEnabled) return
|
||||||
val shortId = messageId.take(8)
|
val shortId = messageId.take(8)
|
||||||
val shortKey = toPublicKey.take(12)
|
val shortKey = toPublicKey.take(12)
|
||||||
val msg = "📬 DELIVERY | id:\$shortId to:\$shortKey status:\$status"
|
val msg = "📬 DELIVERY | id:$shortId to:$shortKey status:$status"
|
||||||
Log.d(TAG, msg)
|
Log.d(TAG, msg)
|
||||||
addToUI(msg)
|
addToUI(msg)
|
||||||
}
|
}
|
||||||
@@ -217,7 +220,7 @@ object MessageLogger {
|
|||||||
) {
|
) {
|
||||||
if (!isEnabled) return
|
if (!isEnabled) return
|
||||||
val shortKey = fromPublicKey.take(12)
|
val shortKey = fromPublicKey.take(12)
|
||||||
val msg = "👁 READ | from:\$shortKey count:\$messagesCount"
|
val msg = "👁 READ | from:$shortKey count:$messagesCount"
|
||||||
Log.d(TAG, msg)
|
Log.d(TAG, msg)
|
||||||
addToUI(msg)
|
addToUI(msg)
|
||||||
}
|
}
|
||||||
@@ -233,7 +236,7 @@ object MessageLogger {
|
|||||||
if (!isEnabled) return
|
if (!isEnabled) return
|
||||||
val shortDialog = dialogKey.take(12)
|
val shortDialog = dialogKey.take(12)
|
||||||
val shortMsg = lastMessage?.take(20) ?: "-"
|
val shortMsg = lastMessage?.take(20) ?: "-"
|
||||||
val msg = "📋 DIALOG | key:\$shortDialog last:\"\$shortMsg\" unread:\$unreadCount"
|
val msg = "📋 DIALOG | key:$shortDialog last:\"$shortMsg\" unread:$unreadCount"
|
||||||
Log.d(TAG, msg)
|
Log.d(TAG, msg)
|
||||||
addToUI(msg)
|
addToUI(msg)
|
||||||
}
|
}
|
||||||
@@ -244,7 +247,7 @@ object MessageLogger {
|
|||||||
fun logCacheUpdate(dialogKey: String, totalMessages: Int) {
|
fun logCacheUpdate(dialogKey: String, totalMessages: Int) {
|
||||||
if (!isEnabled) return
|
if (!isEnabled) return
|
||||||
val shortDialog = dialogKey.take(12)
|
val shortDialog = dialogKey.take(12)
|
||||||
val msg = "🗃 CACHE | dialog:\$shortDialog total:\$totalMessages"
|
val msg = "🗃 CACHE | dialog:$shortDialog total:$totalMessages"
|
||||||
Log.d(TAG, msg)
|
Log.d(TAG, msg)
|
||||||
addToUI(msg)
|
addToUI(msg)
|
||||||
}
|
}
|
||||||
@@ -268,6 +271,6 @@ object MessageLogger {
|
|||||||
} else {
|
} else {
|
||||||
Log.e(TAG, message)
|
Log.e(TAG, message)
|
||||||
}
|
}
|
||||||
addToUI("❌ \$message")
|
addToUI("❌ $message")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user