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:
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(