Compare commits
14 Commits
3bef589274
...
6124a52c84
| Author | SHA1 | Date | |
|---|---|---|---|
| 6124a52c84 | |||
| 3485cb458f | |||
| 0dd3255cfe | |||
| accf34f233 | |||
| 30327fade2 | |||
| e5ff42ce1d | |||
| 06f43b9d4e | |||
| 655cc10a3e | |||
| d02f03516c | |||
| d94b3ec37a | |||
| 73d3b2baf6 | |||
| 66cc21fc29 | |||
| b81b38f40d | |||
| ead84a8a53 |
@@ -23,8 +23,8 @@ val gitShortSha = safeGitOutput("rev-parse", "--short", "HEAD") ?: "unknown"
|
||||
// ═══════════════════════════════════════════════════════════
|
||||
// Rosetta versioning — bump here on each release
|
||||
// ═══════════════════════════════════════════════════════════
|
||||
val rosettaVersionName = "1.4.8"
|
||||
val rosettaVersionCode = 50 // Increment on each release
|
||||
val rosettaVersionName = "1.5.0"
|
||||
val rosettaVersionCode = 52 // Increment on each release
|
||||
val customWebRtcAar = file("libs/libwebrtc-custom.aar")
|
||||
|
||||
android {
|
||||
|
||||
@@ -141,6 +141,7 @@ class MainActivity : FragmentActivity() {
|
||||
// 🔥 Инициализируем ProtocolManager для обработки онлайн статусов
|
||||
ProtocolManager.initialize(this)
|
||||
CallManager.initialize(this)
|
||||
com.rosetta.messenger.ui.chats.components.AttachmentDownloadDebugLogger.init(this)
|
||||
|
||||
// 🔔 Инициализируем Firebase для push-уведомлений
|
||||
initializeFirebase()
|
||||
|
||||
@@ -17,19 +17,12 @@ object ReleaseNotes {
|
||||
val RELEASE_NOTICE = """
|
||||
Update v$VERSION_PLACEHOLDER
|
||||
|
||||
Синхронизация (как на Desktop)
|
||||
- Во время sync экран чатов показывает "Updating..." и скрывает шумящие промежуточные индикаторы
|
||||
- На период синхронизации скрываются badge'ы непрочитанного и requests, чтобы список не "прыгал"
|
||||
|
||||
Медиа и вложения
|
||||
- Исправлен кейс, когда фото уже отправлено, но локально оставалось в ERROR с красным индикатором
|
||||
- Для исходящих медиа стабилизирован переход статусов: после успешной отправки фиксируется SENT без ложного timeout->ERROR
|
||||
- Таймаут/ретрай WAITING из БД больше не портит медиа-вложения (применяется только к обычным текстовым ожиданиям)
|
||||
- Для legacy/неподдерживаемых attachment добавлен desktop-style fallback:
|
||||
"This attachment is no longer available because it was sent for a previous version of the app."
|
||||
|
||||
Группы и UI
|
||||
- Исправлена геометрия входящих фото в группах: пузырь больше не прилипает к аватарке
|
||||
- Исправлена расшифровка фото в группах (совместимость с Desktop v1.2.1)
|
||||
- Исправлен статус доставки: галочки больше не откатываются на часики
|
||||
- Исправлен просмотр фото из медиа-галереи профиля
|
||||
- Зашифрованные ключи больше не отображаются как подпись к фото
|
||||
- Анимация удаления сообщений (плавное сжатие + fade)
|
||||
- Фильтрация пустых push-уведомлений
|
||||
""".trimIndent()
|
||||
|
||||
fun getNotice(version: String): String =
|
||||
|
||||
@@ -440,6 +440,10 @@ interface MessageDao {
|
||||
)
|
||||
suspend fun messageExists(account: String, messageId: String): Boolean
|
||||
|
||||
/** Найти сообщение по ID */
|
||||
@Query("SELECT * FROM messages WHERE account = :account AND message_id = :messageId LIMIT 1")
|
||||
suspend fun findMessageById(account: String, messageId: String): MessageEntity?
|
||||
|
||||
/**
|
||||
* Отметить все исходящие сообщения к собеседнику как прочитанные Используется когда приходит
|
||||
* PacketRead от собеседника.
|
||||
|
||||
@@ -131,11 +131,29 @@ class RosettaFirebaseMessagingService : FirebaseMessagingService() {
|
||||
super.onMessageReceived(remoteMessage)
|
||||
Log.d(TAG, "\u2709\ufe0f onMessageReceived: messageId=${remoteMessage.messageId} from=${remoteMessage.from} data=${remoteMessage.data} notif=${remoteMessage.notification?.body}")
|
||||
|
||||
var handledByData = false
|
||||
val data = remoteMessage.data
|
||||
val notificationTitle = remoteMessage.notification?.title?.trim().orEmpty()
|
||||
val notificationBody = remoteMessage.notification?.body?.trim().orEmpty()
|
||||
|
||||
// Filter out empty/silent pushes (iOS wake-up pushes with mutable-content, empty alerts, etc.)
|
||||
val hasDataContent = data.isNotEmpty() && data.any { (key, value) ->
|
||||
key !in setOf("google.delivered_priority", "google.sent_time", "google.ttl",
|
||||
"google.original_priority", "gcm.notification.e", "gcm.notification.tag",
|
||||
"google.c.a.e", "google.c.sender.id", "google.c.fid",
|
||||
"mutable-content", "mutable_content", "content-available", "content_available") &&
|
||||
value.isNotBlank()
|
||||
}
|
||||
val hasNotificationContent = notificationTitle.isNotBlank() || notificationBody.isNotBlank()
|
||||
|
||||
if (!hasDataContent && !hasNotificationContent) {
|
||||
Log.d(TAG, "Silent/empty push ignored (iOS wake-up push)")
|
||||
// Still trigger reconnect if WebSocket is disconnected
|
||||
com.rosetta.messenger.network.ProtocolManager.reconnectNowIfNeeded("silent_push")
|
||||
return
|
||||
}
|
||||
|
||||
var handledByData = false
|
||||
|
||||
// Обрабатываем data payload (новый server формат + legacy fallback)
|
||||
if (data.isNotEmpty()) {
|
||||
val type =
|
||||
|
||||
@@ -803,6 +803,7 @@ fun ChatDetailScreen(
|
||||
// <20>🔥 Reply/Forward state
|
||||
val replyMessages by viewModel.replyMessages.collectAsState()
|
||||
val isForwardMode by viewModel.isForwardMode.collectAsState()
|
||||
val pendingDeleteIds by viewModel.pendingDeleteIds.collectAsState()
|
||||
|
||||
// Avatar-сообщения не должны попадать в selection ни при каких условиях.
|
||||
val avatarMessageIds =
|
||||
@@ -3120,6 +3121,15 @@ fun ChatDetailScreen(
|
||||
isTailPhase &&
|
||||
isGroupStart))
|
||||
|
||||
val isDeleting = message.id in pendingDeleteIds
|
||||
androidx.compose.animation.AnimatedVisibility(
|
||||
visible = !isDeleting,
|
||||
exit = androidx.compose.animation.shrinkVertically(
|
||||
animationSpec = androidx.compose.animation.core.tween(250, easing = androidx.compose.animation.core.FastOutSlowInEasing)
|
||||
) + androidx.compose.animation.fadeOut(
|
||||
animationSpec = androidx.compose.animation.core.tween(200)
|
||||
)
|
||||
) {
|
||||
Column {
|
||||
if (showDate
|
||||
) {
|
||||
@@ -3527,6 +3537,7 @@ fun ChatDetailScreen(
|
||||
} // contextMenuContent
|
||||
)
|
||||
}
|
||||
} // AnimatedVisibility
|
||||
}
|
||||
}
|
||||
androidx.compose.animation.AnimatedVisibility(
|
||||
|
||||
@@ -203,6 +203,10 @@ class ChatViewModel(application: Application) : AndroidViewModel(application) {
|
||||
val replyMessages: StateFlow<List<ReplyMessage>> = _replyMessages.asStateFlow()
|
||||
|
||||
private val _isForwardMode = MutableStateFlow(false)
|
||||
|
||||
// Animated deletion: IDs of messages currently animating out
|
||||
private val _pendingDeleteIds = MutableStateFlow<Set<String>>(emptySet())
|
||||
val pendingDeleteIds: StateFlow<Set<String>> = _pendingDeleteIds.asStateFlow()
|
||||
val isForwardMode: StateFlow<Boolean> = _isForwardMode.asStateFlow()
|
||||
|
||||
// 📌 Pinned messages state
|
||||
@@ -736,10 +740,16 @@ class ChatViewModel(application: Application) : AndroidViewModel(application) {
|
||||
) {
|
||||
val account = myPublicKey ?: return
|
||||
try {
|
||||
// Never downgrade delivery status (e.g. DELIVERED→WAITING race)
|
||||
val safeDelivered = try {
|
||||
val existing = messageDao.findMessageById(account, messageId)
|
||||
if (existing != null && existing.delivered > delivered) existing.delivered else delivered
|
||||
} catch (_: Exception) { delivered }
|
||||
|
||||
messageDao.updateDeliveryStatusAndAttachments(
|
||||
account,
|
||||
messageId,
|
||||
delivered,
|
||||
safeDelivered,
|
||||
attachmentsJson
|
||||
)
|
||||
} catch (e: Exception) {}
|
||||
@@ -2654,22 +2664,28 @@ class ChatViewModel(application: Application) : AndroidViewModel(application) {
|
||||
val opponent = opponentKey ?: return
|
||||
val dialogKey = getDialogKey(account, opponent)
|
||||
|
||||
// Удаляем из UI сразу на main
|
||||
val updatedMessages = _messages.value.filter { it.id != messageId }
|
||||
_messages.value = updatedMessages
|
||||
// Синхронизируем глобальный кэш диалога, иначе удалённые сообщения могут вернуться
|
||||
// при повторном открытии чата из stale cache.
|
||||
updateCacheWithLimit(account, dialogKey, updatedMessages)
|
||||
messageRepository.clearDialogCache(opponent)
|
||||
// 1. Mark as pending delete (triggers shrink+fade animation)
|
||||
_pendingDeleteIds.value = _pendingDeleteIds.value + messageId
|
||||
|
||||
// Удаляем из БД в IO + удаляем pin если был
|
||||
viewModelScope.launch(Dispatchers.IO) {
|
||||
pinnedMessageDao.removePin(account, dialogKey, messageId)
|
||||
messageDao.deleteMessage(account, messageId)
|
||||
if (account == opponent) {
|
||||
dialogDao.updateSavedMessagesDialogFromMessages(account)
|
||||
} else {
|
||||
dialogDao.updateDialogFromMessages(account, opponent)
|
||||
// 2. After animation completes, remove from list and DB
|
||||
viewModelScope.launch {
|
||||
kotlinx.coroutines.delay(300) // wait for animation
|
||||
|
||||
val updatedMessages = _messages.value.filter { it.id != messageId }
|
||||
_messages.value = updatedMessages
|
||||
_pendingDeleteIds.value = _pendingDeleteIds.value - messageId
|
||||
|
||||
updateCacheWithLimit(account, dialogKey, updatedMessages)
|
||||
messageRepository.clearDialogCache(opponent)
|
||||
|
||||
withContext(Dispatchers.IO) {
|
||||
pinnedMessageDao.removePin(account, dialogKey, messageId)
|
||||
messageDao.deleteMessage(account, messageId)
|
||||
if (account == opponent) {
|
||||
dialogDao.updateSavedMessagesDialogFromMessages(account)
|
||||
} else {
|
||||
dialogDao.updateDialogFromMessages(account, opponent)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -108,16 +108,6 @@ fun CallsHistoryScreen(
|
||||
modifier = modifier.fillMaxSize().background(backgroundColor),
|
||||
contentPadding = PaddingValues(bottom = 16.dp)
|
||||
) {
|
||||
item(key = "start_new_call") {
|
||||
Text(
|
||||
text = "You can add up to 200 participants to a call.",
|
||||
color = secondaryTextColor,
|
||||
fontSize = 13.sp,
|
||||
modifier = Modifier.fillMaxWidth().padding(horizontal = 16.dp, vertical = 10.dp)
|
||||
)
|
||||
Divider(color = dividerColor, thickness = 0.5.dp)
|
||||
}
|
||||
|
||||
if (items.isEmpty()) {
|
||||
item(key = "empty_calls") {
|
||||
EmptyCallsState(
|
||||
|
||||
@@ -2851,7 +2851,14 @@ private suspend fun processDownloadedImage(
|
||||
var decryptDebug = MessageCrypto.AttachmentDecryptDebugResult(null, emptyList())
|
||||
val decrypted =
|
||||
if (groupPassword != null) {
|
||||
val plain = CryptoManager.decryptWithPassword(encryptedContent, groupPassword!!)
|
||||
// Try raw group key first, then hex-encoded (Desktop v1.2.1+ sends hex-encrypted attachments)
|
||||
var plain = CryptoManager.decryptWithPassword(encryptedContent, groupPassword!!)
|
||||
if (plain == null) {
|
||||
val hexKey = groupPassword!!.toByteArray(Charsets.ISO_8859_1)
|
||||
.joinToString("") { "%02x".format(it.toInt() and 0xff) }
|
||||
logPhotoDebug("Group raw key failed, trying hex key: id=$idShort, hexKeyLen=${hexKey.length}")
|
||||
plain = CryptoManager.decryptWithPassword(encryptedContent, hexKey)
|
||||
}
|
||||
decryptDebug = MessageCrypto.AttachmentDecryptDebugResult(plain, emptyList())
|
||||
plain
|
||||
} else {
|
||||
@@ -2968,6 +2975,11 @@ internal suspend fun downloadAndDecryptImage(
|
||||
} else if (isGroupStoredKey(chachaKey)) {
|
||||
val groupPassword = decodeGroupPassword(chachaKey, privateKey) ?: return@withContext null
|
||||
CryptoManager.decryptWithPassword(encryptedContent, groupPassword)
|
||||
?: run {
|
||||
val hexKey = groupPassword.toByteArray(Charsets.ISO_8859_1)
|
||||
.joinToString("") { "%02x".format(it.toInt() and 0xff) }
|
||||
CryptoManager.decryptWithPassword(encryptedContent, hexKey)
|
||||
}
|
||||
} else {
|
||||
val keyCandidates = MessageCrypto.decryptKeyFromSenderCandidates(chachaKey, privateKey)
|
||||
if (keyCandidates.isEmpty()) return@withContext null
|
||||
|
||||
@@ -8,9 +8,22 @@ object AttachmentDownloadDebugLogger {
|
||||
private val _logs = MutableStateFlow<List<String>>(emptyList())
|
||||
val logs: StateFlow<List<String>> = _logs.asStateFlow()
|
||||
|
||||
fun log(@Suppress("UNUSED_PARAMETER") message: String) {
|
||||
// Disabled by request: no runtime accumulation of photo debug logs.
|
||||
return
|
||||
private var appContext: android.content.Context? = null
|
||||
|
||||
fun init(context: android.content.Context) {
|
||||
appContext = context.applicationContext
|
||||
}
|
||||
|
||||
fun log(message: String) {
|
||||
val ctx = appContext ?: return
|
||||
try {
|
||||
val ts = java.text.SimpleDateFormat("HH:mm:ss.SSS", java.util.Locale.getDefault()).format(java.util.Date())
|
||||
val line = "$ts [PhotoDL] $message"
|
||||
android.util.Log.d("PhotoDL", message)
|
||||
val dir = java.io.File(ctx.filesDir, "crash_reports")
|
||||
if (!dir.exists()) dir.mkdirs()
|
||||
java.io.File(dir, "image_logs.txt").appendText("$line\n")
|
||||
} catch (_: Exception) {}
|
||||
}
|
||||
|
||||
fun clear() {
|
||||
|
||||
@@ -637,7 +637,9 @@ fun ImageViewerScreen(
|
||||
// ═══════════════════════════════════════════════════════════
|
||||
// 📝 CAPTION BAR - Telegram-style снизу с анимацией
|
||||
// ═══════════════════════════════════════════════════════════
|
||||
val currentCaption = currentImage?.caption ?: ""
|
||||
val rawCaption = currentImage?.caption ?: ""
|
||||
// Filter out encrypted/garbled captions (base64 strings without spaces)
|
||||
val currentCaption = if (rawCaption.isNotBlank() && !rawCaption.contains(' ') && rawCaption.length > 40 && rawCaption.matches(Regex("^[A-Za-z0-9+/=:]+$"))) "" else rawCaption
|
||||
if (currentCaption.isNotEmpty()) {
|
||||
AnimatedVisibility(
|
||||
visible = showControls && animationState == 1 && !isClosing,
|
||||
@@ -977,53 +979,76 @@ private fun ZoomableImage(
|
||||
* 2) из локального encrypted attachment файла
|
||||
* 3) с transport (с последующим сохранением в локальный файл)
|
||||
*/
|
||||
private fun viewerLog(context: Context, msg: String) {
|
||||
try {
|
||||
val ts = java.text.SimpleDateFormat("HH:mm:ss.SSS", java.util.Locale.getDefault()).format(java.util.Date())
|
||||
val dir = java.io.File(context.filesDir, "crash_reports")
|
||||
if (!dir.exists()) dir.mkdirs()
|
||||
java.io.File(dir, "image_logs.txt").appendText("$ts [Viewer] $msg\n")
|
||||
} catch (_: Exception) {}
|
||||
}
|
||||
|
||||
private suspend fun loadBitmapForViewerImage(
|
||||
context: Context,
|
||||
image: ViewableImage,
|
||||
privateKey: String
|
||||
): Bitmap? {
|
||||
val id = image.attachmentId.take(10)
|
||||
viewerLog(context, "=== LOAD id=$id blob=${image.blob.length} preview=${image.preview.length} chacha=${image.chachaKey.length} tag=${image.transportTag.take(12)} sender=${image.senderPublicKey.take(12)} ===")
|
||||
return try {
|
||||
// 0. In-memory кэш
|
||||
val cached = ImageBitmapCache.get("img_${image.attachmentId}")
|
||||
if (cached != null) {
|
||||
viewerLog(context, " [0] HIT cache ${cached.width}x${cached.height}")
|
||||
return cached
|
||||
}
|
||||
viewerLog(context, " [0] MISS cache")
|
||||
|
||||
// 1. Blob в сообщении
|
||||
if (image.blob.isNotEmpty()) {
|
||||
viewerLog(context, " [1] blob ${image.blob.length} chars")
|
||||
val bmp = base64ToBitmapSafe(image.blob)
|
||||
if (bmp != null) {
|
||||
return bmp
|
||||
}
|
||||
if (bmp != null) { viewerLog(context, " [1] OK ${bmp.width}x${bmp.height}"); return bmp }
|
||||
viewerLog(context, " [1] decode FAIL")
|
||||
}
|
||||
|
||||
// 2. Локальный encrypted cache
|
||||
viewerLog(context, " [2] readAttachment sender=${image.senderPublicKey.take(12)}")
|
||||
val localBlob =
|
||||
AttachmentFileManager.readAttachment(context, image.attachmentId, image.senderPublicKey, privateKey)
|
||||
if (localBlob != null) {
|
||||
viewerLog(context, " [2] local ${localBlob.length} chars")
|
||||
val bmp = base64ToBitmapSafe(localBlob)
|
||||
if (bmp != null) {
|
||||
return bmp
|
||||
}
|
||||
}
|
||||
if (bmp != null) { viewerLog(context, " [2] OK ${bmp.width}x${bmp.height}"); return bmp }
|
||||
viewerLog(context, " [2] decode FAIL")
|
||||
} else { viewerLog(context, " [2] NOT found") }
|
||||
|
||||
// 2.5. Ждём bitmap из кеша
|
||||
viewerLog(context, " [2.5] await 3s...")
|
||||
val awaitedFromCache = ImageBitmapCache.awaitCached("img_${image.attachmentId}", 3000)
|
||||
if (awaitedFromCache != null) {
|
||||
viewerLog(context, " [2.5] OK ${awaitedFromCache.width}x${awaitedFromCache.height}")
|
||||
return awaitedFromCache
|
||||
}
|
||||
viewerLog(context, " [2.5] timeout")
|
||||
|
||||
// 3. CDN download
|
||||
var downloadTag = getDownloadTag(image.preview)
|
||||
if (downloadTag.isEmpty() && image.transportTag.isNotEmpty()) {
|
||||
downloadTag = image.transportTag
|
||||
}
|
||||
viewerLog(context, " [3] tag=${downloadTag.take(12)} preview=${image.preview.take(20)}")
|
||||
if (downloadTag.isEmpty()) {
|
||||
viewerLog(context, " [3] NO TAG → FAIL")
|
||||
return null
|
||||
}
|
||||
|
||||
val server = TransportManager.getTransportServer() ?: "none"
|
||||
viewerLog(context, " [3] downloading from $server")
|
||||
val encryptedContent = TransportManager.downloadFile(image.attachmentId, downloadTag)
|
||||
viewerLog(context, " [3] downloaded ${encryptedContent.length} bytes")
|
||||
if (encryptedContent.isEmpty()) {
|
||||
viewerLog(context, " [3] EMPTY → FAIL")
|
||||
return null
|
||||
}
|
||||
|
||||
@@ -1037,7 +1062,14 @@ private suspend fun loadBitmapForViewerImage(
|
||||
val groupPassword = CryptoManager.decryptWithPassword(
|
||||
image.chachaKey.removePrefix("group:"), privateKey
|
||||
)
|
||||
if (groupPassword != null) CryptoManager.decryptWithPassword(encryptedContent, groupPassword) else null
|
||||
if (groupPassword != null) {
|
||||
CryptoManager.decryptWithPassword(encryptedContent, groupPassword)
|
||||
?: run {
|
||||
val hexKey = groupPassword.toByteArray(Charsets.ISO_8859_1)
|
||||
.joinToString("") { "%02x".format(it.toInt() and 0xff) }
|
||||
CryptoManager.decryptWithPassword(encryptedContent, hexKey)
|
||||
}
|
||||
} else null
|
||||
} else if (image.chachaKey.isNotEmpty()) {
|
||||
val decryptedKeyAndNonce = MessageCrypto.decryptKeyFromSender(image.chachaKey, privateKey)
|
||||
MessageCrypto.decryptAttachmentBlobWithPlainKey(encryptedContent, decryptedKeyAndNonce)
|
||||
@@ -1045,11 +1077,14 @@ private suspend fun loadBitmapForViewerImage(
|
||||
null
|
||||
}
|
||||
|
||||
viewerLog(context, " [3] decrypt=${if (decrypted != null) "${decrypted.length} chars" else "NULL"} method=${when { image.chachaKeyPlainHex.isNotEmpty() -> "plainHex"; image.chachaKey.startsWith("group:") -> "group"; image.chachaKey.isNotEmpty() -> "chacha"; else -> "none" }}")
|
||||
if (decrypted == null) {
|
||||
viewerLog(context, " [3] DECRYPT FAIL")
|
||||
return null
|
||||
}
|
||||
|
||||
val decodedBitmap = base64ToBitmapSafe(decrypted)
|
||||
viewerLog(context, " [3] bitmap=${if (decodedBitmap != null) "${decodedBitmap.width}x${decodedBitmap.height}" else "NULL"}")
|
||||
if (decodedBitmap == null) {
|
||||
return null
|
||||
}
|
||||
@@ -1062,8 +1097,10 @@ private suspend fun loadBitmapForViewerImage(
|
||||
publicKey = image.senderPublicKey,
|
||||
privateKey = privateKey
|
||||
)
|
||||
viewerLog(context, " DONE OK")
|
||||
decodedBitmap
|
||||
} catch (e: Exception) {
|
||||
viewerLog(context, " EXCEPTION: ${e.javaClass.simpleName}: ${e.message}")
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
@@ -342,7 +342,9 @@ fun OtherProfileScreen(
|
||||
timestamp = Date(media.timestamp),
|
||||
width = media.width,
|
||||
height = media.height,
|
||||
caption = media.caption
|
||||
caption = media.caption,
|
||||
transportTag = media.transportTag,
|
||||
transportServer = media.transportServer
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -891,10 +893,19 @@ fun OtherProfileScreen(
|
||||
}
|
||||
val previewBitmap = remember(media.preview) {
|
||||
if (media.preview.isNotBlank()) {
|
||||
// Extract blurhash from "UUID::blurhash" format
|
||||
val blurhash = if (media.preview.contains("::")) {
|
||||
media.preview.substringAfter("::")
|
||||
} else {
|
||||
media.preview
|
||||
}
|
||||
runCatching {
|
||||
val bytes = Base64.decode(media.preview, Base64.DEFAULT)
|
||||
BitmapFactory.decodeByteArray(bytes, 0, bytes.size)
|
||||
com.vanniktech.blurhash.BlurHash.decode(blurhash, 32, 32)
|
||||
}.getOrNull()
|
||||
?: runCatching {
|
||||
val bytes = Base64.decode(media.preview, Base64.DEFAULT)
|
||||
BitmapFactory.decodeByteArray(bytes, 0, bytes.size)
|
||||
}.getOrNull()
|
||||
} else null
|
||||
}
|
||||
val isLoaded = resolvedBitmap != null || model != null
|
||||
@@ -1211,7 +1222,9 @@ private data class SharedPhotoItem(
|
||||
val width: Int,
|
||||
val height: Int,
|
||||
val caption: String,
|
||||
val timestamp: Long
|
||||
val timestamp: Long,
|
||||
val transportTag: String = "",
|
||||
val transportServer: String = ""
|
||||
)
|
||||
|
||||
private data class SharedFileItem(
|
||||
@@ -1292,7 +1305,9 @@ private suspend fun buildOtherProfileSharedContent(
|
||||
width = attachment.width,
|
||||
height = attachment.height,
|
||||
caption = decryptedText,
|
||||
timestamp = message.timestamp
|
||||
timestamp = message.timestamp,
|
||||
transportTag = attachment.transportTag,
|
||||
transportServer = attachment.transportServer
|
||||
)
|
||||
|
||||
mediaPhotos.add(mediaItem)
|
||||
@@ -1385,11 +1400,11 @@ private fun extractDownloadTagFromPreview(preview: String): String {
|
||||
|
||||
private fun decryptStoredMessageText(encryptedText: String, privateKey: String): String {
|
||||
if (encryptedText.isBlank()) return ""
|
||||
if (privateKey.isBlank()) return encryptedText
|
||||
if (privateKey.isBlank()) return ""
|
||||
return try {
|
||||
CryptoManager.decryptWithPassword(encryptedText, privateKey) ?: encryptedText
|
||||
CryptoManager.decryptWithPassword(encryptedText, privateKey) ?: ""
|
||||
} catch (_: Exception) {
|
||||
encryptedText
|
||||
""
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user