commit 008c094cd4e1dfa31991a3102a01c73329ec3cdf
parent 0c99ae15ba87871403a7c5345707ea6d76a5c194
Author: rhunk <101876869+rhunk@users.noreply.github.com>
Date: Wed, 26 Feb 2025 16:22:14 +0100
feat(core/tweaks): double tap chat action
- like, delete, copy message text, mark as read
Signed-off-by: rhunk <101876869+rhunk@users.noreply.github.com>
Diffstat:
7 files changed, 110 insertions(+), 0 deletions(-)
diff --git a/common/src/main/assets/lang/en_US.json b/common/src/main/assets/lang/en_US.json
@@ -743,6 +743,10 @@
"remove_groups_locked_status": {
"name": "Remove Groups Locked Status",
"description": "Allows you to view group information after being kicked"
+ },
+ "double_tap_chat_action": {
+ "name": "Double Tap Chat Action",
+ "description": "Performs a custom action when double tapping a message in chat"
}
}
},
@@ -1385,6 +1389,12 @@
"not_subscribed": "Not Subscribed",
"basic": "Basic",
"ad_free": "Ad Free"
+ },
+ "double_tap_chat_action": {
+ "like_message": "Like Message",
+ "copy_text": "Copy Text to Clipboard",
+ "delete_message": "Delete Message",
+ "mark_as_read": "Mark as Read"
}
}
},
diff --git a/common/src/main/kotlin/me/rhunk/snapenhance/common/config/impl/MessagingTweaks.kt b/common/src/main/kotlin/me/rhunk/snapenhance/common/config/impl/MessagingTweaks.kt
@@ -99,4 +99,5 @@ class MessagingTweaks : ConfigContainer() {
val bypassMessageRetentionPolicy = boolean("bypass_message_retention_policy") { addNotices(FeatureNotice.UNSTABLE); requireRestart() }
val bypassMessageActionRestrictions = boolean("bypass_message_action_restrictions") { requireRestart() }
val removeGroupsLockedStatus = boolean("remove_groups_locked_status") { requireRestart() }
+ val doubleTapChatAction = unique("double_tap_chat_action", "like_message", "copy_text", "delete_message", "mark_as_read") { requireRestart() }
}
\ No newline at end of file
diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/core/features/FeatureManager.kt b/core/src/main/kotlin/me/rhunk/snapenhance/core/features/FeatureManager.kt
@@ -141,6 +141,7 @@ class FeatureManager(
BetterTranscript(),
VoiceNoteOverride(),
FriendNotes(),
+ DoubleTapChatAction(),
)
features.values.toList().forEach { feature ->
diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/core/features/impl/tweaks/DoubleTapChatAction.kt b/core/src/main/kotlin/me/rhunk/snapenhance/core/features/impl/tweaks/DoubleTapChatAction.kt
@@ -0,0 +1,56 @@
+package me.rhunk.snapenhance.core.features.impl.tweaks
+
+import me.rhunk.snapenhance.common.data.ContentType
+import me.rhunk.snapenhance.common.data.MessageUpdate
+import me.rhunk.snapenhance.common.util.ktx.copyToClipboard
+import me.rhunk.snapenhance.common.util.ktx.findFieldsToString
+import me.rhunk.snapenhance.common.util.protobuf.ProtoReader
+import me.rhunk.snapenhance.core.features.Feature
+import me.rhunk.snapenhance.core.features.impl.messaging.Messaging
+import me.rhunk.snapenhance.core.util.hook.HookStage
+import me.rhunk.snapenhance.core.util.hook.hook
+import me.rhunk.snapenhance.core.wrapper.impl.getMessageText
+import me.rhunk.snapenhance.mapper.impl.ChatEventDispatcherMapper
+
+class DoubleTapChatAction: Feature("Double Tap Chat Action") {
+ override fun init() {
+ var action = context.config.messaging.doubleTapChatAction.getNullable() ?: return
+
+ context.mappings.useMapper(ChatEventDispatcherMapper::class) {
+ classReference.getAsClass()?.hook("onChatItemDoubleClickEvent", HookStage.BEFORE) { param ->
+ param.setResult(null)
+ val event = param.arg<Any>(0)
+ val viewModel = event.javaClass.findFieldsToString(event, once = true) { field, value -> value.contains("ChatViewModel") }.firstOrNull()?.get(event)?.toString() ?: return@hook
+
+ val (conversationId, _, clientMessageId) = viewModel.substringAfter("messageId=").substringBefore(",").split(":").takeIf { it.size == 3 } ?: return@hook
+
+ val messageId = clientMessageId.toLongOrNull() ?: return@hook
+
+ if (action == "like_message") {
+ context.feature(Messaging::class).conversationManager?.reactToMessage(
+ conversationId,
+ messageId,
+ intentionType = 1L,
+ onError = {},
+ onSuccess = {}
+ )
+ }
+
+ if (action == "copy_text") {
+ var messageContent = context.database.getConversationMessageFromId(messageId)?.messageContent ?: return@hook
+ var proto = ProtoReader(messageContent).followPath(4, 4) ?: return@hook
+ context.androidContext.copyToClipboard(proto.getBuffer().getMessageText(ContentType.fromMessageContainer(proto) ?: ContentType.CHAT) ?: return@hook, "Chat Message")
+ }
+
+ if (action == "delete_message" || action == "mark_as_read") {
+ context.feature(Messaging::class).conversationManager?.updateMessage(
+ conversationId,
+ messageId,
+ if (action == "delete_message") MessageUpdate.ERASE else MessageUpdate.READ,
+ onResult = {}
+ )
+ }
+ }
+ }
+ }
+}+
\ No newline at end of file
diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/core/wrapper/impl/ConversationManager.kt b/core/src/main/kotlin/me/rhunk/snapenhance/core/wrapper/impl/ConversationManager.kt
@@ -27,6 +27,7 @@ class ConversationManager(
private val clearConversation by lazy { findMethodByName("clearConversation") }
private val getOneOnOneConversationIds by lazy { findMethodByName("getOneOnOneConversationIds") }
private val dismissStreakRestore by lazy { findMethodByName("dismissStreakRestore") }
+ private val reactToMessageMethod by lazy { findMethodByName("reactToMessage") }
private fun getCallbackClass(name: String): Class<*> {
@@ -183,4 +184,24 @@ class ConversationManager(
.override("onError") { onError(it.arg<Any>(0).toString()) }.build()
dismissStreakRestore.invoke(instanceNonNull(), conversationId.toSnapUUID().instanceNonNull(), callback)
}
+
+ fun reactToMessage(conversationId: String, messageId: Long, emoji: String? = null, intentionType: Long? = null, onSuccess: () -> Unit, onError: (error: String) -> Unit) {
+ reactToMessageMethod.invoke(
+ instanceNonNull(),
+ conversationId.toSnapUUID().instanceNonNull(),
+ messageId,
+ reactToMessageMethod.parameterTypes[2].dataBuilder {
+ set("mEmoji", emoji)
+ set("mIntentionType", intentionType)
+ },
+ reactToMessageMethod.parameterTypes[3].dataBuilder {
+ set("mMetricsMessageMediaType", "NO_MEDIA")
+ set("mMetricsMessageType", "TEXT")
+ set("mReactionSource", "NONE")
+ },
+ CallbackBuilder(getCallbackClass("Callback"))
+ .override("onSuccess") { onSuccess() }
+ .override("onError") { onError(it.arg<Any>(0).toString()) }.build()
+ )
+ }
}
\ No newline at end of file
diff --git a/mapper/src/main/kotlin/me/rhunk/snapenhance/mapper/ClassMapper.kt b/mapper/src/main/kotlin/me/rhunk/snapenhance/mapper/ClassMapper.kt
@@ -29,6 +29,7 @@ class ClassMapper(
PlusSubscriptionMapper(),
StoryBoostStateMapper(),
FriendsFeedEventDispatcherMapper(),
+ ChatEventDispatcherMapper(),
CompositeConfigurationProviderMapper(),
ScoreUpdateMapper(),
FriendRelationshipChangerMapper(),
diff --git a/mapper/src/main/kotlin/me/rhunk/snapenhance/mapper/impl/ChatEventDispatcherMapper.kt b/mapper/src/main/kotlin/me/rhunk/snapenhance/mapper/impl/ChatEventDispatcherMapper.kt
@@ -0,0 +1,18 @@
+package me.rhunk.snapenhance.mapper.impl
+
+import me.rhunk.snapenhance.mapper.AbstractClassMapper
+import me.rhunk.snapenhance.mapper.ext.getClassName
+
+class ChatEventDispatcherMapper : AbstractClassMapper("ChatEventDispatcher") {
+ val classReference = classReference("class")
+
+ init {
+ mapper {
+ for (clazz in classes) {
+ if (clazz.methods.firstOrNull { it.name == "onChatItemDoubleClickEvent" } == null) continue
+ classReference.set(clazz.getClassName())
+ return@mapper
+ }
+ }
+ }
+}+
\ No newline at end of file