Compare commits
5 Commits
d02f03516c
...
accf34f233
| Author | SHA1 | Date | |
|---|---|---|---|
| accf34f233 | |||
| 30327fade2 | |||
| e5ff42ce1d | |||
| 06f43b9d4e | |||
| 655cc10a3e |
@@ -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 {
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|||||||
@@ -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 =
|
||||||
|
|||||||
@@ -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(
|
||||||
|
|||||||
@@ -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.
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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() {
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
""
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user