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>
This commit is contained in:
rhunk 2025-02-26 16:22:14 +01:00
parent 0c99ae15ba
commit 008c094cd4
7 changed files with 108 additions and 0 deletions

View File

@ -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"
}
}
},

View File

@ -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() }
}

View File

@ -141,6 +141,7 @@ class FeatureManager(
BetterTranscript(),
VoiceNoteOverride(),
FriendNotes(),
DoubleTapChatAction(),
)
features.values.toList().forEach { feature ->

View File

@ -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 = {}
)
}
}
}
}
}

View File

@ -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()
)
}
}

View File

@ -29,6 +29,7 @@ class ClassMapper(
PlusSubscriptionMapper(),
StoryBoostStateMapper(),
FriendsFeedEventDispatcherMapper(),
ChatEventDispatcherMapper(),
CompositeConfigurationProviderMapper(),
ScoreUpdateMapper(),
FriendRelationshipChangerMapper(),

View File

@ -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
}
}
}
}