Compare commits

...

5 Commits

Author SHA1 Message Date
accf34f233 Релиз v1.5.0: расшифровка групповых фото (Desktop v1.2.1 parity), анимация удаления, image logs, фикс caption
Some checks failed
Android Kernel Build / build (push) Has been cancelled
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-10 02:27:58 +05:00
30327fade2 Фикс: расшифровка групповых фото — fallback на hex group key (Desktop v1.2.1 parity)
Desktop теперь шифрует аттачменты в группах hex-версией ключа.
Android пробует raw key, при неудаче — hex key. Фикс в 3 местах:
processDownloadedImage, downloadAndDecryptImage, loadBitmapForViewerImage.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-10 02:20:46 +05:00
e5ff42ce1d Анимация удаления сообщений (Telegram-style): shrink + fade out 250ms
Двухэтапное удаление: pendingDeleteIds → AnimatedVisibility(shrinkVertically + fadeOut) → remove.
Остальные сообщения плавно сдвигаются на место удалённого.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-10 01:55:26 +05:00
06f43b9d4e Фикс: зашифрованные ключи не отображаются как caption в фото viewer
decryptStoredMessageText возвращал зашифрованный текст при неудачной расшифровке.
Теперь возвращает пустую строку. Дополнительно: фильтр base64-like строк в caption viewer.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-10 01:43:38 +05:00
655cc10a3e Фикс: передача transportTag в SharedMedia viewer + логи загрузки фото в rosettadev1
SharedPhotoItem не передавал transportTag/transportServer в ViewableImage —
фото из медиа-галереи профиля не загружались в полноэкранном режиме.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-10 01:35:26 +05:00
9 changed files with 155 additions and 80 deletions

View File

@@ -23,8 +23,8 @@ val gitShortSha = safeGitOutput("rev-parse", "--short", "HEAD") ?: "unknown"
// ═══════════════════════════════════════════════════════════ // ═══════════════════════════════════════════════════════════
// Rosetta versioning — bump here on each release // Rosetta versioning — bump here on each release
// ═══════════════════════════════════════════════════════════ // ═══════════════════════════════════════════════════════════
val rosettaVersionName = "1.4.9" val rosettaVersionName = "1.5.0"
val rosettaVersionCode = 51 // Increment on each release val rosettaVersionCode = 52 // Increment on each release
val customWebRtcAar = file("libs/libwebrtc-custom.aar") val customWebRtcAar = file("libs/libwebrtc-custom.aar")
android { android {

View File

@@ -141,6 +141,7 @@ class MainActivity : FragmentActivity() {
// 🔥 Инициализируем ProtocolManager для обработки онлайн статусов // 🔥 Инициализируем ProtocolManager для обработки онлайн статусов
ProtocolManager.initialize(this) ProtocolManager.initialize(this)
CallManager.initialize(this) CallManager.initialize(this)
com.rosetta.messenger.ui.chats.components.AttachmentDownloadDebugLogger.init(this)
// 🔔 Инициализируем Firebase для push-уведомлений // 🔔 Инициализируем Firebase для push-уведомлений
initializeFirebase() initializeFirebase()

View File

@@ -18,62 +18,47 @@ object ReleaseNotes {
Update v$VERSION_PLACEHOLDER Update v$VERSION_PLACEHOLDER
QR-коды и шеринг QR-коды и шеринг
- Новый экран QR-кода профиля в стиле Telegram (обои, цветной QR, логотип Rosetta по центру) - Экран QR-кода профиля в стиле Telegram (обои, цветной QR, логотип Rosetta)
- 6 тем оформления (3 тёмных + 3 светлых) с circular reveal анимацией при смене - 6 тем оформления с circular reveal анимацией при смене
- Кнопка смены темы приложения прямо с экрана QR (sun/moon) - Смена тёмной/светлой темы прямо с экрана QR
- QR-сканер через камеру (CameraX + ML Kit) — распознаёт профили и группы - QR-сканер через камеру — распознаёт профили и группы
- Кнопки Share и Copy Link для шеринга профиля
- Deep link поддержка: rosetta:// и https://rosetta.im - Deep link поддержка: rosetta:// и https://rosetta.im
- Scan QR в боковом меню
Группы Группы
- Telegram-style выбор участников при создании группы (поиск, чекмарки, чипы) - Выбор участников при создании группы (поиск, чекмарки)
- Автоматическая отправка приглашений выбранным участникам после создания - Автоотправка приглашений выбранным участникам
- Исправлена расшифровка фото в группах (совместимость с Desktop v1.2.1)
Forward сообщений Forward сообщений
- Полностью переработан: убран ре-аплоад картинок на CDN (Desktop/iOS parity) - Убран ре-аплоад картинок на CDN (мгновенный forward)
- Добавлен chacha_key_plain для кросс-платформенной совместимости шифрования - Кросс-платформенная совместимость шифрования (chacha_key_plain)
- Forward bubble подстраивается под размер контента (фотки, текста) - Пузырь подстраивается под размер контента
- Длинные имена обрезаются "Forwarded from Alex M..." вместо растяжения пузыря
- Исправлена отправка forward — сообщения теперь реально доставляются
Звонки Звонки
- Анимированный градиентный фон при звонке (3 blob-а, как в iOS) - Анимированный градиентный фон (3 blob-а, как в iOS)
- Аватарки в уведомлениях звонков и на экране входящего - Аватарки в уведомлениях и на экране входящего
- Кнопка Call на экране чужого профиля - Кнопка Call на профиле собеседника
- Мгновенное сообщение "Missed call" / "Rejected call" для обеих сторон - Мгновенное "Missed call" для обеих сторон
Доставка сообщений Доставка и фото
- Исправлен баг когда галочки доставки не появлялись (DELIVERED → SENT откат) - Статус доставки больше не откатывается (монотонный: SENDING → SENT → DELIVERED → READ)
- Статус доставки теперь монотонный: SENDING → SENT → DELIVERED → READ - Исправлен "Failed to load image" в полноэкранном просмотре
- Листание предыдущих аватарок пользователя
Просмотр фото - Зашифрованные ключи больше не показываются как подпись к фото
- Исправлен "Failed to load image" в полноэкранном просмотре (fallback на transportTag) - Анимация удаления сообщений (shrink + fade out)
- Глобальный ImageBitmapCache доступен в viewer
- Исправлена расшифровка фото в reply (chachaKey оригинального сообщения)
- Листание предыдущих аватарок пользователя (как на Desktop)
Онбординг Онбординг
- Новый экран установки профиля (имя + username + аватар) после регистрации - Экран профиля (имя + username + аватар) после регистрации
- Отдельный экран биометрии с красивым UI - Отдельный экран биометрии
- Проверка доступности username в реальном времени - Проверка доступности username в реальном времени
- Биометрия теперь привязана к аккаунту (per-account) - Биометрия привязана к аккаунту
- Убран экран подтверждения seed phrase - Переработанный экран пароля
- Экран пароля переработан (Telegram-style, без дёрганья)
UI улучшения UI
- Подсказка эмодзи в стиле Telegram (floating карточка, press-эффект) - Подсказка эмодзи в стиле Telegram
- Аватарки в результатах поиска - Аватарки в поиске
- Унифицированы иконки навигации (ChevronLeft) по всему приложению - Чёрные иконки статус-бара на белом фоне
- Статус-бар: чёрные иконки на белом фоне, восстановление при уходе с экрана - Фильтрация пустых push-уведомлений (iOS wake-up)
- Плавная анимация navbar при смене темы
- Клавиатура прячется при скролле профиля и навигации между экранами
- Emoji-safe обрезка текста в reply-превью
- Сепараторы участников в группах
- Исправлены дубли дат в чате
Уведомления
- Исправлено декодирование аватарки в push-уведомлениях (base64 prefix)
""".trimIndent() """.trimIndent()
fun getNotice(version: String): String = fun getNotice(version: String): String =

View File

@@ -803,6 +803,7 @@ fun ChatDetailScreen(
// <20>🔥 Reply/Forward state // <20>🔥 Reply/Forward state
val replyMessages by viewModel.replyMessages.collectAsState() val replyMessages by viewModel.replyMessages.collectAsState()
val isForwardMode by viewModel.isForwardMode.collectAsState() val isForwardMode by viewModel.isForwardMode.collectAsState()
val pendingDeleteIds by viewModel.pendingDeleteIds.collectAsState()
// Avatar-сообщения не должны попадать в selection ни при каких условиях. // Avatar-сообщения не должны попадать в selection ни при каких условиях.
val avatarMessageIds = val avatarMessageIds =
@@ -3120,6 +3121,15 @@ fun ChatDetailScreen(
isTailPhase && isTailPhase &&
isGroupStart)) 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 { Column {
if (showDate if (showDate
) { ) {
@@ -3527,6 +3537,7 @@ fun ChatDetailScreen(
} // contextMenuContent } // contextMenuContent
) )
} }
} // AnimatedVisibility
} }
} }
androidx.compose.animation.AnimatedVisibility( androidx.compose.animation.AnimatedVisibility(

View File

@@ -203,6 +203,10 @@ class ChatViewModel(application: Application) : AndroidViewModel(application) {
val replyMessages: StateFlow<List<ReplyMessage>> = _replyMessages.asStateFlow() val replyMessages: StateFlow<List<ReplyMessage>> = _replyMessages.asStateFlow()
private val _isForwardMode = MutableStateFlow(false) 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() val isForwardMode: StateFlow<Boolean> = _isForwardMode.asStateFlow()
// 📌 Pinned messages state // 📌 Pinned messages state
@@ -2660,16 +2664,21 @@ class ChatViewModel(application: Application) : AndroidViewModel(application) {
val opponent = opponentKey ?: return val opponent = opponentKey ?: return
val dialogKey = getDialogKey(account, opponent) val dialogKey = getDialogKey(account, opponent)
// Удаляем из UI сразу на main // 1. Mark as pending delete (triggers shrink+fade animation)
_pendingDeleteIds.value = _pendingDeleteIds.value + messageId
// 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 } val updatedMessages = _messages.value.filter { it.id != messageId }
_messages.value = updatedMessages _messages.value = updatedMessages
// Синхронизируем глобальный кэш диалога, иначе удалённые сообщения могут вернуться _pendingDeleteIds.value = _pendingDeleteIds.value - messageId
// при повторном открытии чата из stale cache.
updateCacheWithLimit(account, dialogKey, updatedMessages) updateCacheWithLimit(account, dialogKey, updatedMessages)
messageRepository.clearDialogCache(opponent) messageRepository.clearDialogCache(opponent)
// Удаляем из БД в IO + удаляем pin если был withContext(Dispatchers.IO) {
viewModelScope.launch(Dispatchers.IO) {
pinnedMessageDao.removePin(account, dialogKey, messageId) pinnedMessageDao.removePin(account, dialogKey, messageId)
messageDao.deleteMessage(account, messageId) messageDao.deleteMessage(account, messageId)
if (account == opponent) { if (account == opponent) {
@@ -2679,6 +2688,7 @@ class ChatViewModel(application: Application) : AndroidViewModel(application) {
} }
} }
} }
}
/** /**
* Resolve a publicKey to a SearchUser for profile navigation. * Resolve a publicKey to a SearchUser for profile navigation.

View File

@@ -2851,7 +2851,14 @@ private suspend fun processDownloadedImage(
var decryptDebug = MessageCrypto.AttachmentDecryptDebugResult(null, emptyList()) var decryptDebug = MessageCrypto.AttachmentDecryptDebugResult(null, emptyList())
val decrypted = val decrypted =
if (groupPassword != null) { 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()) decryptDebug = MessageCrypto.AttachmentDecryptDebugResult(plain, emptyList())
plain plain
} else { } else {
@@ -2968,6 +2975,11 @@ internal suspend fun downloadAndDecryptImage(
} else if (isGroupStoredKey(chachaKey)) { } else if (isGroupStoredKey(chachaKey)) {
val groupPassword = decodeGroupPassword(chachaKey, privateKey) ?: return@withContext null val groupPassword = decodeGroupPassword(chachaKey, privateKey) ?: return@withContext null
CryptoManager.decryptWithPassword(encryptedContent, groupPassword) 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 { } else {
val keyCandidates = MessageCrypto.decryptKeyFromSenderCandidates(chachaKey, privateKey) val keyCandidates = MessageCrypto.decryptKeyFromSenderCandidates(chachaKey, privateKey)
if (keyCandidates.isEmpty()) return@withContext null if (keyCandidates.isEmpty()) return@withContext null

View File

@@ -8,9 +8,22 @@ object AttachmentDownloadDebugLogger {
private val _logs = MutableStateFlow<List<String>>(emptyList()) private val _logs = MutableStateFlow<List<String>>(emptyList())
val logs: StateFlow<List<String>> = _logs.asStateFlow() val logs: StateFlow<List<String>> = _logs.asStateFlow()
fun log(@Suppress("UNUSED_PARAMETER") message: String) { private var appContext: android.content.Context? = null
// Disabled by request: no runtime accumulation of photo debug logs.
return 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() { fun clear() {

View File

@@ -637,7 +637,9 @@ fun ImageViewerScreen(
// ═══════════════════════════════════════════════════════════ // ═══════════════════════════════════════════════════════════
// 📝 CAPTION BAR - Telegram-style снизу с анимацией // 📝 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()) { if (currentCaption.isNotEmpty()) {
AnimatedVisibility( AnimatedVisibility(
visible = showControls && animationState == 1 && !isClosing, visible = showControls && animationState == 1 && !isClosing,
@@ -977,53 +979,76 @@ private fun ZoomableImage(
* 2) из локального encrypted attachment файла * 2) из локального encrypted attachment файла
* 3) с transport (с последующим сохранением в локальный файл) * 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( private suspend fun loadBitmapForViewerImage(
context: Context, context: Context,
image: ViewableImage, image: ViewableImage,
privateKey: String privateKey: String
): Bitmap? { ): 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 { return try {
// 0. In-memory кэш // 0. In-memory кэш
val cached = ImageBitmapCache.get("img_${image.attachmentId}") val cached = ImageBitmapCache.get("img_${image.attachmentId}")
if (cached != null) { if (cached != null) {
viewerLog(context, " [0] HIT cache ${cached.width}x${cached.height}")
return cached return cached
} }
viewerLog(context, " [0] MISS cache")
// 1. Blob в сообщении // 1. Blob в сообщении
if (image.blob.isNotEmpty()) { if (image.blob.isNotEmpty()) {
viewerLog(context, " [1] blob ${image.blob.length} chars")
val bmp = base64ToBitmapSafe(image.blob) val bmp = base64ToBitmapSafe(image.blob)
if (bmp != null) { if (bmp != null) { viewerLog(context, " [1] OK ${bmp.width}x${bmp.height}"); return bmp }
return bmp viewerLog(context, " [1] decode FAIL")
}
} }
// 2. Локальный encrypted cache // 2. Локальный encrypted cache
viewerLog(context, " [2] readAttachment sender=${image.senderPublicKey.take(12)}")
val localBlob = val localBlob =
AttachmentFileManager.readAttachment(context, image.attachmentId, image.senderPublicKey, privateKey) AttachmentFileManager.readAttachment(context, image.attachmentId, image.senderPublicKey, privateKey)
if (localBlob != null) { if (localBlob != null) {
viewerLog(context, " [2] local ${localBlob.length} chars")
val bmp = base64ToBitmapSafe(localBlob) val bmp = base64ToBitmapSafe(localBlob)
if (bmp != null) { if (bmp != null) { viewerLog(context, " [2] OK ${bmp.width}x${bmp.height}"); return bmp }
return bmp viewerLog(context, " [2] decode FAIL")
} } else { viewerLog(context, " [2] NOT found") }
}
// 2.5. Ждём bitmap из кеша // 2.5. Ждём bitmap из кеша
viewerLog(context, " [2.5] await 3s...")
val awaitedFromCache = ImageBitmapCache.awaitCached("img_${image.attachmentId}", 3000) val awaitedFromCache = ImageBitmapCache.awaitCached("img_${image.attachmentId}", 3000)
if (awaitedFromCache != null) { if (awaitedFromCache != null) {
viewerLog(context, " [2.5] OK ${awaitedFromCache.width}x${awaitedFromCache.height}")
return awaitedFromCache return awaitedFromCache
} }
viewerLog(context, " [2.5] timeout")
// 3. CDN download // 3. CDN download
var downloadTag = getDownloadTag(image.preview) var downloadTag = getDownloadTag(image.preview)
if (downloadTag.isEmpty() && image.transportTag.isNotEmpty()) { if (downloadTag.isEmpty() && image.transportTag.isNotEmpty()) {
downloadTag = image.transportTag downloadTag = image.transportTag
} }
viewerLog(context, " [3] tag=${downloadTag.take(12)} preview=${image.preview.take(20)}")
if (downloadTag.isEmpty()) { if (downloadTag.isEmpty()) {
viewerLog(context, " [3] NO TAG → FAIL")
return null return null
} }
val server = TransportManager.getTransportServer() ?: "none"
viewerLog(context, " [3] downloading from $server")
val encryptedContent = TransportManager.downloadFile(image.attachmentId, downloadTag) val encryptedContent = TransportManager.downloadFile(image.attachmentId, downloadTag)
viewerLog(context, " [3] downloaded ${encryptedContent.length} bytes")
if (encryptedContent.isEmpty()) { if (encryptedContent.isEmpty()) {
viewerLog(context, " [3] EMPTY → FAIL")
return null return null
} }
@@ -1037,7 +1062,14 @@ private suspend fun loadBitmapForViewerImage(
val groupPassword = CryptoManager.decryptWithPassword( val groupPassword = CryptoManager.decryptWithPassword(
image.chachaKey.removePrefix("group:"), privateKey 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()) { } else if (image.chachaKey.isNotEmpty()) {
val decryptedKeyAndNonce = MessageCrypto.decryptKeyFromSender(image.chachaKey, privateKey) val decryptedKeyAndNonce = MessageCrypto.decryptKeyFromSender(image.chachaKey, privateKey)
MessageCrypto.decryptAttachmentBlobWithPlainKey(encryptedContent, decryptedKeyAndNonce) MessageCrypto.decryptAttachmentBlobWithPlainKey(encryptedContent, decryptedKeyAndNonce)
@@ -1045,11 +1077,14 @@ private suspend fun loadBitmapForViewerImage(
null 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) { if (decrypted == null) {
viewerLog(context, " [3] DECRYPT FAIL")
return null return null
} }
val decodedBitmap = base64ToBitmapSafe(decrypted) val decodedBitmap = base64ToBitmapSafe(decrypted)
viewerLog(context, " [3] bitmap=${if (decodedBitmap != null) "${decodedBitmap.width}x${decodedBitmap.height}" else "NULL"}")
if (decodedBitmap == null) { if (decodedBitmap == null) {
return null return null
} }
@@ -1062,8 +1097,10 @@ private suspend fun loadBitmapForViewerImage(
publicKey = image.senderPublicKey, publicKey = image.senderPublicKey,
privateKey = privateKey privateKey = privateKey
) )
viewerLog(context, " DONE OK")
decodedBitmap decodedBitmap
} catch (e: Exception) { } catch (e: Exception) {
viewerLog(context, " EXCEPTION: ${e.javaClass.simpleName}: ${e.message}")
null null
} }
} }

View File

@@ -342,7 +342,9 @@ fun OtherProfileScreen(
timestamp = Date(media.timestamp), timestamp = Date(media.timestamp),
width = media.width, width = media.width,
height = media.height, height = media.height,
caption = media.caption caption = media.caption,
transportTag = media.transportTag,
transportServer = media.transportServer
) )
} }
} }
@@ -1211,7 +1213,9 @@ private data class SharedPhotoItem(
val width: Int, val width: Int,
val height: Int, val height: Int,
val caption: String, val caption: String,
val timestamp: Long val timestamp: Long,
val transportTag: String = "",
val transportServer: String = ""
) )
private data class SharedFileItem( private data class SharedFileItem(
@@ -1292,7 +1296,9 @@ private suspend fun buildOtherProfileSharedContent(
width = attachment.width, width = attachment.width,
height = attachment.height, height = attachment.height,
caption = decryptedText, caption = decryptedText,
timestamp = message.timestamp timestamp = message.timestamp,
transportTag = attachment.transportTag,
transportServer = attachment.transportServer
) )
mediaPhotos.add(mediaItem) mediaPhotos.add(mediaItem)
@@ -1385,11 +1391,11 @@ private fun extractDownloadTagFromPreview(preview: String): String {
private fun decryptStoredMessageText(encryptedText: String, privateKey: String): String { private fun decryptStoredMessageText(encryptedText: String, privateKey: String): String {
if (encryptedText.isBlank()) return "" if (encryptedText.isBlank()) return ""
if (privateKey.isBlank()) return encryptedText if (privateKey.isBlank()) return ""
return try { return try {
CryptoManager.decryptWithPassword(encryptedText, privateKey) ?: encryptedText CryptoManager.decryptWithPassword(encryptedText, privateKey) ?: ""
} catch (_: Exception) { } catch (_: Exception) {
encryptedText ""
} }
} }