commit 7adbec9ab7dfc7bd12f31c079396301b02878121
parent a7e5f81b430f2d2614fb7e86cb88f93fc8f4c173
Author: rhunk <101876869+rhunk@users.noreply.github.com>
Date:   Thu,  9 May 2024 13:03:57 +0200

feat(media_downloader): quoted message support

Diffstat:
Mcommon/src/main/assets/lang/en_US.json | 2+-
Mcommon/src/main/kotlin/me/rhunk/snapenhance/common/database/impl/ConversationMessage.kt | 3+++
Mcore/src/main/kotlin/me/rhunk/snapenhance/core/features/impl/downloader/MediaDownloader.kt | 210++++++++++++++++++++++++++++++++++++++++++++++---------------------------------
Mcore/src/main/kotlin/me/rhunk/snapenhance/core/features/impl/downloader/decoder/MessageDecoder.kt | 14++++++++++++--
4 files changed, 139 insertions(+), 90 deletions(-)

diff --git a/common/src/main/assets/lang/en_US.json b/common/src/main/assets/lang/en_US.json @@ -1419,7 +1419,7 @@ "note": "Note", "original_story": "Original Story" }, - "select_attachments_title": "Select attachments to download", + "select_attachments_title": "Select attachments", "download_started_toast": "Download started", "unsupported_content_type_toast": "Unsupported content type!", "failed_no_longer_available_toast": "Media no longer available", diff --git a/common/src/main/kotlin/me/rhunk/snapenhance/common/database/impl/ConversationMessage.kt b/common/src/main/kotlin/me/rhunk/snapenhance/common/database/impl/ConversationMessage.kt @@ -6,6 +6,7 @@ import me.rhunk.snapenhance.common.database.DatabaseObject import me.rhunk.snapenhance.common.util.ktx.getBlobOrNull import me.rhunk.snapenhance.common.util.ktx.getInteger import me.rhunk.snapenhance.common.util.ktx.getLong +import me.rhunk.snapenhance.common.util.ktx.getLongOrNull import me.rhunk.snapenhance.common.util.ktx.getStringOrNull @Suppress("ArrayInDataClass") @@ -19,6 +20,7 @@ data class ConversationMessage( var contentType: Int = 0, var creationTimestamp: Long = 0, var readTimestamp: Long = 0, + var quotedServerMessageId: Long? = null, var senderId: String? = null ) : DatabaseObject { @@ -34,6 +36,7 @@ data class ConversationMessage( contentType = getInteger("content_type") creationTimestamp = getLong("creation_timestamp") readTimestamp = getLong("read_timestamp") + quotedServerMessageId = getLongOrNull("quoted_server_message_id") senderId = getStringOrNull("sender_id") } } diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/core/features/impl/downloader/MediaDownloader.kt b/core/src/main/kotlin/me/rhunk/snapenhance/core/features/impl/downloader/MediaDownloader.kt @@ -15,8 +15,7 @@ import androidx.compose.material.icons.outlined.CheckCircle import androidx.compose.material.icons.outlined.Error import androidx.compose.material.icons.outlined.Info import androidx.compose.material.icons.outlined.Warning -import kotlinx.coroutines.ExperimentalCoroutinesApi -import kotlinx.coroutines.async +import kotlinx.coroutines.launch import kotlinx.coroutines.runBlocking import me.rhunk.snapenhance.bridge.DownloadCallback import me.rhunk.snapenhance.common.data.FileType @@ -568,22 +567,118 @@ class MediaDownloader : MessagingRuleFeature("MediaDownloader", MessagingRuleTyp } } + private fun DecodedAttachment.getInfo(): String { + return "${translations["attachment_type.${type.key}"]} ${attachmentInfo?.resolution?.let { "(${it.first}x${it.second})" } ?: ""}" + } + + @SuppressLint("SetTextI18n") + private fun previewAttachment( + attachment: DecodedAttachment + ) { + var previewBitmap: Bitmap? = null + val previewCoroutine = context.coroutineScope.launch { + val downloadedMedia = RemoteMediaResolver.downloadBoltMedia(Base64.decode(attachment.mediaUrlKey!!), decryptionCallback = { + attachment.attachmentInfo?.encryption?.decryptInputStream(it) ?: it + }) ?: return@launch + + val downloadedMediaList = mutableMapOf<SplitMediaAssetType, ByteArray>() + + MediaDownloaderHelper.getSplitElements(ByteArrayInputStream(downloadedMedia)) { + type, inputStream -> + downloadedMediaList[type] = inputStream.readBytes() + } + + val originalMedia = downloadedMediaList[SplitMediaAssetType.ORIGINAL] ?: return@launch + val overlay = downloadedMediaList[SplitMediaAssetType.OVERLAY] + + var bitmap: Bitmap? = PreviewUtils.createPreview(originalMedia, isVideo = FileType.fromByteArray(originalMedia).isVideo) + + if (bitmap == null) { + context.shortToast(translations["failed_to_create_preview_toast"]) + return@launch + } + + overlay?.also { + bitmap = PreviewUtils.mergeBitmapOverlay(bitmap!!, BitmapFactory.decodeByteArray(it, 0, it.size)) + } + + previewBitmap = bitmap + } + + with(ViewAppearanceHelper.newAlertDialogBuilder(context.mainActivity)) { + val viewGroup = LinearLayout(context).apply { + layoutParams = MarginLayoutParams( + MarginLayoutParams.MATCH_PARENT, + MarginLayoutParams.MATCH_PARENT + ) + gravity = Gravity.CENTER_HORIZONTAL or Gravity.CENTER_VERTICAL + addView(ProgressBar(context).apply { + isIndeterminate = true + }) + } + + setOnDismissListener { + previewCoroutine.cancel() + } + + previewCoroutine.invokeOnCompletion { cause -> + if (previewCoroutine.isCancelled) return@invokeOnCompletion + runOnUiThread { + viewGroup.removeAllViews() + if (cause != null) { + viewGroup.addView(TextView(context).apply { + text = + translations["failed_to_create_preview_toast"] + "\n" + cause.message + setPadding(30, 30, 30, 30) + }) + return@runOnUiThread + } + + viewGroup.addView(ImageView(context).apply { + setImageBitmap(previewBitmap) + layoutParams = LinearLayout.LayoutParams( + LinearLayout.LayoutParams.MATCH_PARENT, + LinearLayout.LayoutParams.MATCH_PARENT + ) + adjustViewBounds = true + }) + } + } + + runOnUiThread { + show().apply { + setContentView(viewGroup) + window?.setLayout( + context.resources.displayMetrics.widthPixels, + context.resources.displayMetrics.heightPixels + ) + } + } + } + } @SuppressLint("SetTextI18n") - @OptIn(ExperimentalCoroutinesApi::class) fun downloadMessageId(messageId: Long, forceAllowDuplicate: Boolean = false, isPreview: Boolean = false) { val messageLogger = context.feature(MessageLogger::class) val message = context.database.getConversationMessageFromId(messageId) ?: throw Exception("Message not found in database") - //get the message author - val friendInfo: FriendInfo = context.database.getFriendInfo(message.senderId!!) ?: throw Exception("Friend not found in database") + val friendInfo = context.database.getFriendInfo(message.senderId!!) ?: throw Exception("Friend not found in database") val authorName = friendInfo.usernameForSorting!! - val decodedAttachments = (messageLogger.takeIf { it.isEnabled }?.getMessageObject(message.clientConversationId!!, message.clientMessageId.toLong())?.let { - MessageDecoder.decode(it.getAsJsonObject("mMessageContent")) - } ?: MessageDecoder.decode( - protoReader = ProtoReader(message.messageContent!!) - )).toMutableList() + val decodedAttachments = ( + messageLogger.takeIf { it.isEnabled }?.getMessageObject(message.clientConversationId!!, message.clientMessageId.toLong())?.let { + MessageDecoder.decode(it.getAsJsonObject("mMessageContent")) + } ?: MessageDecoder.decode( + protoReader = ProtoReader(message.messageContent!!) + ).toMutableList().apply { + val quotedMessage = message.quotedServerMessageId?.takeIf { it > 0 }?.let { quotedMessageId -> + context.database.getConversationServerMessage(message.clientConversationId!!, quotedMessageId) + } ?: return@apply + addAll(0, MessageDecoder.decode( + protoReader = ProtoReader(quotedMessage.messageContent ?: return@apply) + )) + } + ).toMutableList() context.feature(Messaging::class).conversationManager?.takeIf { decodedAttachments.isEmpty() @@ -623,7 +718,7 @@ class MediaDownloader : MessagingRuleFeature("MediaDownloader", MessagingRuleTyp } setMultiChoiceItems( decodedAttachments.mapIndexed { index, decodedAttachment -> - "${index + 1}: ${translations["attachment_type.${decodedAttachment.type.key}"]} ${decodedAttachment.attachmentInfo?.resolution?.let { "(${it.first}x${it.second})" } ?: ""}" + "${index + 1}: ${decodedAttachment.getInfo()}" }.toTypedArray(), decodedAttachments.map { true }.toBooleanArray() ) { _, which, isChecked -> @@ -642,88 +737,29 @@ class MediaDownloader : MessagingRuleFeature("MediaDownloader", MessagingRuleTyp } }.show() } - return } - runBlocking { - val firstAttachment = decodedAttachments.first() - - val previewCoroutine = async { - val downloadedMedia = RemoteMediaResolver.downloadBoltMedia(Base64.decode(firstAttachment.mediaUrlKey!!), decryptionCallback = { - firstAttachment.attachmentInfo?.encryption?.decryptInputStream(it) ?: it - }) ?: return@async null - - val downloadedMediaList = mutableMapOf<SplitMediaAssetType, ByteArray>() - - MediaDownloaderHelper.getSplitElements(ByteArrayInputStream(downloadedMedia)) { - type, inputStream -> - downloadedMediaList[type] = inputStream.readBytes() - } - - val originalMedia = downloadedMediaList[SplitMediaAssetType.ORIGINAL] ?: return@async null - val overlay = downloadedMediaList[SplitMediaAssetType.OVERLAY] - - var bitmap: Bitmap? = PreviewUtils.createPreview(originalMedia, isVideo = FileType.fromByteArray(originalMedia).isVideo) - - if (bitmap == null) { - context.shortToast(translations["failed_to_create_preview_toast"]) - return@async null - } - - overlay?.also { - bitmap = PreviewUtils.mergeBitmapOverlay(bitmap!!, BitmapFactory.decodeByteArray(it, 0, it.size)) - } - - bitmap - } - - with(ViewAppearanceHelper.newAlertDialogBuilder(context.mainActivity)) { - val viewGroup = LinearLayout(context).apply { - layoutParams = MarginLayoutParams(MarginLayoutParams.MATCH_PARENT, MarginLayoutParams.MATCH_PARENT) - gravity = Gravity.CENTER_HORIZONTAL or Gravity.CENTER_VERTICAL - addView(ProgressBar(context).apply { - isIndeterminate = true - }) - } + if (decodedAttachments.size == 1) { + previewAttachment(decodedAttachments.first()) + return + } - setOnDismissListener { - previewCoroutine.cancel() + runOnUiThread { + ViewAppearanceHelper.newAlertDialogBuilder(context.mainActivity).apply { + var selectedAttachment = 0 + setSingleChoiceItems( + decodedAttachments.mapIndexed { index, decodedAttachment -> "${index + 1}: ${decodedAttachment.getInfo()}" }.toTypedArray(), + 0 + ) { _, which -> + selectedAttachment = which } - - previewCoroutine.invokeOnCompletion { cause -> - runOnUiThread { - viewGroup.removeAllViews() - if (cause != null) { - viewGroup.addView(TextView(context).apply { - text = translations["failed_to_create_preview_toast"] + "\n" + cause.message - setPadding(30, 30, 30, 30) - }) - return@runOnUiThread - } - - viewGroup.addView(ImageView(context).apply { - setImageBitmap(previewCoroutine.getCompleted()) - layoutParams = LinearLayout.LayoutParams( - LinearLayout.LayoutParams.MATCH_PARENT, - LinearLayout.LayoutParams.MATCH_PARENT - ) - adjustViewBounds = true - }) - } - } - - runOnUiThread { - show().apply { - setContentView(viewGroup) - window?.setLayout( - context.resources.displayMetrics.widthPixels, - context.resources.displayMetrics.heightPixels - ) - } - previewCoroutine.start() + setTitle(translations["select_attachments_title"]) + setNegativeButton(this@MediaDownloader.context.translation["button.cancel"]) { dialog, _ -> dialog.dismiss() } + setPositiveButton(this@MediaDownloader.context.translation["chat_action_menu.preview_button"]) { _, _ -> + previewAttachment(decodedAttachments[selectedAttachment]) } - } + }.show() } } diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/core/features/impl/downloader/decoder/MessageDecoder.kt b/core/src/main/kotlin/me/rhunk/snapenhance/core/features/impl/downloader/decoder/MessageDecoder.kt @@ -79,7 +79,13 @@ object MessageDecoder { return decode( ProtoReader(messageContent.content!!), customMediaReferences = getEncodedMediaReferences(gson.toJsonTree(messageContent.instanceNonNull())) - ) + ).toMutableList().apply { + if (messageContent.quotedMessage != null && messageContent.quotedMessage!!.content != null) { + addAll(0, decode( + MessageContent(messageContent.quotedMessage!!.content!!.instanceNonNull()) + )) + } + } } fun decode(messageContent: JsonObject): List<DecodedAttachment> { @@ -88,7 +94,11 @@ object MessageDecoder { .map { it.asByte } .toByteArray()), customMediaReferences = getEncodedMediaReferences(messageContent) - ) + ).toMutableList().apply { + if (messageContent.has("mQuotedMessage") && messageContent.getAsJsonObject("mQuotedMessage").has("mContent")) { + addAll(0, decode(messageContent.getAsJsonObject("mQuotedMessage").getAsJsonObject("mContent"))) + } + } } fun decode(