commit a877c04461f07e282575ed288e6663b356d14b80
parent a43e4049a3998f618c03389196299b61e2f5865d
Author: rhunk <101876869+rhunk@users.noreply.github.com>
Date:   Tue, 20 Aug 2024 00:40:26 +0200

feat(core): better send override
- compose ui
- fix "Media Upload" label

Diffstat:
Mcommon/src/main/assets/lang/en_US.json | 8++++++--
Mcore/src/main/kotlin/me/rhunk/snapenhance/core/features/impl/messaging/SendOverride.kt | 264++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++---------------
2 files changed, 221 insertions(+), 51 deletions(-)

diff --git a/common/src/main/assets/lang/en_US.json b/common/src/main/assets/lang/en_US.json @@ -1524,7 +1524,8 @@ "negative": "No", "cancel": "Cancel", "open": "Open", - "download": "Download" + "download": "Download", + "send": "Send" }, "better_notifications": { @@ -1698,6 +1699,9 @@ "sigColorStoryRingDiscoverTabThumbnailStoryRing": "Story Ring Discover Tab Thumbnail Story Ring Color" }, "send_override_dialog": { - "title": "Send media as ..." + "title": "Send media as {type}", + "duration": "Duration: {duration}", + "saveable_snap_hint": "Make Snap saveable in the chat", + "unlimited_duration": "Unlimited" } } diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/core/features/impl/messaging/SendOverride.kt b/core/src/main/kotlin/me/rhunk/snapenhance/core/features/impl/messaging/SendOverride.kt @@ -1,8 +1,23 @@ package me.rhunk.snapenhance.core.features.impl.messaging +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.* import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.MusicNote +import androidx.compose.material.icons.filled.Photo +import androidx.compose.material.icons.filled.PhotoCamera import androidx.compose.material.icons.filled.WarningAmber +import androidx.compose.material3.* +import androidx.compose.runtime.* +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp import me.rhunk.snapenhance.common.data.ContentType +import me.rhunk.snapenhance.common.ui.createComposeAlertDialog import me.rhunk.snapenhance.common.util.protobuf.ProtoEditor import me.rhunk.snapenhance.common.util.protobuf.ProtoReader import me.rhunk.snapenhance.common.util.protobuf.ProtoWriter @@ -12,19 +27,17 @@ import me.rhunk.snapenhance.core.event.events.impl.SendMessageWithContentEvent import me.rhunk.snapenhance.core.features.Feature import me.rhunk.snapenhance.core.features.impl.experiments.MediaFilePicker import me.rhunk.snapenhance.core.messaging.MessageSender -import me.rhunk.snapenhance.core.ui.ViewAppearanceHelper -import me.rhunk.snapenhance.nativelib.NativeLib +import me.rhunk.snapenhance.core.util.ktx.getObjectFieldOrNull import java.util.Locale +import kotlin.time.DurationUnit +import kotlin.time.toDuration + class SendOverride : Feature("Send Override") { - private val typeNames by lazy { - mutableListOf("ORIGINAL", "SNAP", "NOTE").also { - if (NativeLib.initialized) { - it.add("SAVEABLE_SNAP") - } - }.associateWith { it } - } + private var selectedType by mutableStateOf("SNAP") + private var customDuration by mutableStateOf(10f) + @OptIn(ExperimentalLayoutApi::class) override fun init() { val stripSnapMetadata = context.config.messaging.stripMediaMetadata.get() var postSavePolicy: Int? = null @@ -83,19 +96,28 @@ class SendOverride : Feature("Send Override") { event.onMediaUploaded { result -> result.messageContent.content = ProtoEditor(result.messageContent.content!!).apply { edit(11, 5) { - // remove media upload hint when viewing snap edit(1) { edit(1) { + snapDocPlayback.getVarInt(2, 99)?.let { customDuration -> + remove(15) + addVarInt(15, customDuration) + } remove(27) + remove(26) addBuffer(26, byteArrayOf()) } } + // set back the original snap duration remove(2) snapDocPlayback.getByteArray(2)?.let { addBuffer(2, it) } } + + edit(11, 5, 2) { + remove(99) + } }.toByteArray() } } @@ -104,17 +126,17 @@ class SendOverride : Feature("Send Override") { context.event.subscribe(NativeUnaryCallEvent::class) { event -> if (event.uri != "/messagingcoreservice.MessagingCoreService/CreateContentMessage") return@subscribe postSavePolicy?.let { savePolicy -> - context.log.verbose("post save policy $savePolicy") + context.log.verbose("postSavePolicy=$savePolicy") event.buffer = ProtoEditor(event.buffer).apply { - edit { - edit(4) { - remove(7) - addVarInt(7, savePolicy) - } - add(6) { - from(9) { - addVarInt(1, 1) - } + edit(4) { + remove(7) + addVarInt(7, savePolicy) + } + + // remove Keep Snaps in Chat ability + if (savePolicy == 1/* PROHIBITED */) { + edit(6, 9) { + remove(1) } } }.toByteArray() @@ -125,16 +147,16 @@ class SendOverride : Feature("Send Override") { postSavePolicy = null if (event.destinations.stories?.isNotEmpty() == true && event.destinations.conversations?.isEmpty() == true) return@subscribe val localMessageContent = event.messageContent - if (localMessageContent.contentType != ContentType.EXTERNAL_MEDIA) return@subscribe - //prevent story replies - val messageProtoReader = ProtoReader(localMessageContent.content!!) + if (localMessageContent.contentType != ContentType.EXTERNAL_MEDIA && localMessageContent.instanceNonNull().getObjectFieldOrNull("mExternalContentMetadata")?.getObjectFieldOrNull("mContainsExternalContent") != true) return@subscribe + + val messageProtoReader = ProtoReader(localMessageContent.content ?: return@subscribe) if (messageProtoReader.contains(7)) return@subscribe event.canceled = true - fun sendMedia(overrideType: String): Boolean { - if (overrideType != "ORIGINAL" && messageProtoReader.followPath(3)?.getCount(3) != 1) { + fun sendMedia(overrideType: String, snapDurationMs: Int?): Boolean { + if (overrideType != "ORIGINAL" && (messageProtoReader.followPath(3)?.getCount(3) ?: 0) > 1) { context.inAppOverlay.showStatusToast( icon = Icons.Default.WarningAmber, context.translation["gallery_media_send_override.multiple_media_toast"] @@ -147,26 +169,49 @@ class SendOverride : Feature("Send Override") { postSavePolicy = if (overrideType == "SAVEABLE_SNAP") 3 /* VIEW_SESSION */ else 1 /* PROHIBITED */ val extras = messageProtoReader.followPath(3, 3, 13)?.getBuffer() - localMessageContent.contentType = ContentType.SNAP - localMessageContent.content = ProtoWriter().apply { - from(11) { - from(5) { - from(1) { + + if (localMessageContent.contentType != ContentType.SNAP) { + localMessageContent.content = ProtoWriter().apply { + from(11) { + from(5) { from(1) { - addVarInt(2, 0) - addVarInt(12, 0) - addVarInt(15, 0) + from(1) { + addVarInt(2, 0) + addVarInt(12, 0) + addVarInt(15, 0) + } + addVarInt(6, 0) } - addVarInt(6, 0) + from(2) {} } - messageProtoReader.getByteArray(3, 3, 5, 2)?.let { - addBuffer(2, it) + extras?.let { + addBuffer(13, it) } + from(22) {} } - extras?.let { - addBuffer(13, it) + }.toByteArray() + } + + localMessageContent.contentType = ContentType.SNAP + localMessageContent.content = ProtoEditor(localMessageContent.content!!).apply { + edit(11, 5, 2) { + arrayOf(6, 7, 8).forEach { remove(it) } + // set snap duration + if (snapDurationMs != null) { + addVarInt(8, snapDurationMs / 1000) + if (snapDurationMs / 1000 <= 0) { + addVarInt(99, snapDurationMs) + } + } else { + addBuffer(6, byteArrayOf()) } } + + // set app source + edit(11, 22) { + remove(4) + addVarInt(4, 5) // APP_SOURCE_CAMERA + } }.toByteArray() } "NOTE" -> { @@ -183,25 +228,146 @@ class SendOverride : Feature("Send Override") { } if (configOverrideType != "always_ask") { - if (sendMedia(configOverrideType)) { + if (sendMedia(configOverrideType, 10)) { event.invokeOriginal() } return@subscribe } context.runOnUiThread { - ViewAppearanceHelper.newAlertDialogBuilder(context.mainActivity!!) - .setItems(typeNames.values.map { - context.translation["features.options.gallery_media_send_override.$it"] - }.toTypedArray()) { dialog, which -> - dialog.dismiss() - if (sendMedia(typeNames.keys.toTypedArray()[which])) { - event.invokeOriginal() + createComposeAlertDialog(context.mainActivity!!) { alertDialog -> + val mainTranslation = remember { + context.translation.getCategory("send_override_dialog") + } + + @Composable + fun ActionTile( + modifier: Modifier = Modifier, + selected: Boolean = false, + icon: ImageVector, + title: String, + onClick: () -> Unit + ) { + Card( + modifier = modifier, + onClick = onClick, + elevation = if (selected) CardDefaults.elevatedCardElevation(disabledElevation = 3.dp) else CardDefaults.cardElevation(), + colors = if (selected) CardDefaults.elevatedCardColors() else CardDefaults.cardColors() + ) { + Column( + modifier = Modifier + .padding(16.dp) + .size(75.dp), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.Center + ) { + Icon(icon, contentDescription = title, modifier = Modifier + .size(32.dp) + .padding(4.dp)) + Text(title, modifier = Modifier.fillMaxWidth(), fontSize = 12.sp, fontWeight = FontWeight.Light, softWrap = true, lineHeight = 14.sp, textAlign = TextAlign.Center) + } + } + } + + Column( + modifier = Modifier + .fillMaxWidth() + .padding(8.dp), + verticalArrangement = Arrangement.spacedBy(8.dp) + ) { + val translation = remember { + context.translation.getCategory("features.options.gallery_media_send_override") + } + + Text(fontSize = 20.sp, fontWeight = FontWeight.Medium, text = "Send as ${ + translation[selectedType]}", modifier = Modifier.padding(5.dp)) + FlowRow( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceEvenly + ) { + ActionTile(selected = selectedType == "ORIGINAL", icon = Icons.Filled.Photo, title = + translation["ORIGINAL"]) { + selectedType = "ORIGINAL" + } + ActionTile(selected = selectedType == "SNAP" || selectedType == "SAVEABLE_SNAP", icon = Icons.Filled.PhotoCamera, title = translation["SNAP"]) { + selectedType = "SNAP" + } + ActionTile(selected = selectedType == "NOTE", icon = Icons.Filled.MusicNote, title = translation["NOTE"]) { + selectedType = "NOTE" + } + } + + fun convertDuration(duration: Float): Int? { + return when { + duration in -2f..-1f -> 100 + duration in -1f..-0f -> 250 + duration in -0f..1f -> 500 + duration >= 11f -> null + else -> ((duration * 1000).toInt() / 1000) * 1000 + } + } + + when (selectedType) { + "SNAP", "SAVEABLE_SNAP" -> { + fun toggleSaveable() { + selectedType = if (selectedType == "SAVEABLE_SNAP") "SNAP" else "SAVEABLE_SNAP" + } + Row( + modifier = Modifier.fillMaxWidth().clickable { + toggleSaveable() + }, + horizontalArrangement = Arrangement.spacedBy(4.dp), + verticalAlignment = Alignment.CenterVertically + ){ + Checkbox( + checked = selectedType == "SAVEABLE_SNAP", + onCheckedChange = { + toggleSaveable() + } + ) + Text(text = mainTranslation["saveable_snap_hint"], lineHeight = 15.sp) + } + Column( + modifier = Modifier.padding(start = 8.dp) + ) { + Text( + text = mainTranslation.format("duration", + "duration" to (convertDuration(customDuration)?.toDuration(DurationUnit.MILLISECONDS)?.toString(DurationUnit.SECONDS, 2) ?: mainTranslation["unlimited_duration"]) + ) + ) + Slider( + modifier = Modifier.fillMaxWidth(), + value = customDuration, + onValueChange = { + customDuration = it + }, + valueRange = -2f..11f, + ) + } + } + } + + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceEvenly, + verticalAlignment = Alignment.CenterVertically + ) { + OutlinedButton(onClick = { + alertDialog.dismiss() + }) { + Text(context.translation["button.cancel"]) + } + Button(onClick = { + alertDialog.dismiss() + if (sendMedia(selectedType, convertDuration(customDuration))) { + event.invokeOriginal() + } + }) { + Text(context.translation["button.send"]) + } } } - .setTitle(context.translation["send_override_dialog.title"]) - .setNegativeButton(context.translation["button.cancel"], null) - .show() + }.show() } } }