diff --git a/common/src/main/assets/lang/en_US.json b/common/src/main/assets/lang/en_US.json index 1880c6a0..8c8097c1 100644 --- 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 index 50cc9825..39acaf73 100644 --- 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 index b98afcb2..d18d335d 100644 --- 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 new file mode 100644 index 00000000..e7e71705 --- /dev/null +++ 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(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 index 2647ed31..51f10ff3 100644 --- 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(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(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 index a17e11f7..2fad41af 100644 --- 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 new file mode 100644 index 00000000..a4859447 --- /dev/null +++ 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