commit 29f7e2aa1cdebb716ad038d4fc2e9d519ef41ca3
parent 9f084afebed1391dfd0a704e34e0a656ab613a8d
Author: rhunk <101876869+rhunk@users.noreply.github.com>
Date: Mon, 11 Mar 2024 22:47:13 +0100
feat(experimental): edit messages
Diffstat:
7 files changed, 120 insertions(+), 3 deletions(-)
diff --git a/common/src/main/assets/lang/en_US.json b/common/src/main/assets/lang/en_US.json
@@ -704,6 +704,10 @@
}
}
},
+ "edit_message": {
+ "name": "Edit Messages",
+ "description": "Allows you to edit messages in conversations"
+ },
"app_passcode": {
"name": "App Passcode",
"description": "Sets a passcode to lock the app"
diff --git a/common/src/main/kotlin/me/rhunk/snapenhance/common/config/impl/Experimental.kt b/common/src/main/kotlin/me/rhunk/snapenhance/common/config/impl/Experimental.kt
@@ -31,6 +31,7 @@ class Experimental : ConfigContainer() {
val storyLogger = boolean("story_logger") { requireRestart(); addNotices(FeatureNotice.UNSTABLE); }
val callRecorder = boolean("call_recorder") { requireRestart(); addNotices(FeatureNotice.UNSTABLE); }
val accountSwitcher = container("account_switcher", AccountSwitcherConfig()) { requireRestart(); addNotices(FeatureNotice.UNSTABLE) }
+ val editMessage = boolean("edit_message") { requireRestart(); addNotices(FeatureNotice.BAN_RISK) }
val appPasscode = string("app_passcode")
val appLockOnResume = boolean("app_lock_on_resume")
val infiniteStoryBoost = boolean("infinite_story_boost")
diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/core/features/impl/messaging/Messaging.kt b/core/src/main/kotlin/me/rhunk/snapenhance/core/features/impl/messaging/Messaging.kt
@@ -20,6 +20,7 @@ import me.rhunk.snapenhance.core.wrapper.impl.Snapchatter
import me.rhunk.snapenhance.core.wrapper.impl.toSnapUUID
import me.rhunk.snapenhance.mapper.impl.CallbackMapper
import me.rhunk.snapenhance.mapper.impl.FriendsFeedEventDispatcherMapper
+import java.util.UUID
import java.util.concurrent.Future
class Messaging : Feature("Messaging", loadParams = FeatureLoadParams.ACTIVITY_CREATE_SYNC or FeatureLoadParams.INIT_ASYNC or FeatureLoadParams.INIT_SYNC) {
@@ -93,7 +94,12 @@ class Messaging : Feature("Messaging", loadParams = FeatureLoadParams.ACTIVITY_C
})
}
- fun localUpdateMessage(conversationId: String, message: Message) {
+ fun localUpdateMessage(conversationId: String, message: Message, forceUpdate: Boolean = false) {
+ if (forceUpdate) {
+ message.messageMetadata?.screenRecordedBy = ArrayList<SnapUUID>(message.messageMetadata?.screenRecordedBy ?: emptyList()).apply {
+ add(SnapUUID(UUID.randomUUID().toString()))
+ }
+ }
conversationManagerDelegate?.let {
it::class.java.methods.first { method ->
method.name == "onConversationUpdated"
diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/core/ui/menu/impl/ChatActionMenu.kt b/core/src/main/kotlin/me/rhunk/snapenhance/core/ui/menu/impl/ChatActionMenu.kt
@@ -4,11 +4,17 @@ import android.view.View
import android.view.ViewGroup
import android.view.ViewGroup.MarginLayoutParams
import android.widget.Button
+import android.widget.EditText
import android.widget.LinearLayout
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.launch
+import me.rhunk.snapenhance.common.util.protobuf.ProtoReader
+import me.rhunk.snapenhance.common.util.protobuf.ProtoWriter
import me.rhunk.snapenhance.core.features.impl.downloader.MediaDownloader
import me.rhunk.snapenhance.core.features.impl.experiments.ConvertMessageLocally
import me.rhunk.snapenhance.core.features.impl.messaging.Messaging
import me.rhunk.snapenhance.core.features.impl.spying.MessageLogger
+import me.rhunk.snapenhance.core.ui.ViewAppearanceHelper
import me.rhunk.snapenhance.core.ui.ViewTagState
import me.rhunk.snapenhance.core.ui.applyTheme
import me.rhunk.snapenhance.core.ui.menu.AbstractMenu
@@ -58,7 +64,11 @@ class ChatActionMenu : AbstractMenu() {
val viewGroup = parent.parent.parent as? ViewGroup ?: return
if (viewTagState[viewGroup]) return
//close the action menu using a touch event
- val closeActionMenu = { parent.triggerCloseTouchEvent() }
+ val closeActionMenu = {
+ context.runOnUiThread {
+ parent.triggerCloseTouchEvent()
+ }
+ }
val messaging = context.feature(Messaging::class)
val messageLogger = context.feature(MessageLogger::class)
@@ -129,6 +139,72 @@ class ChatActionMenu : AbstractMenu() {
})
}
+ if (context.config.experimental.editMessage.get() && messaging.conversationManager?.isEditMessageSupported() == true) {
+ injectButton(Button(viewGroup.context).apply button@{
+ text = "Edit Message"
+ setOnClickListener {
+ messaging.conversationManager?.fetchMessage(
+ messaging.openedConversationUUID.toString(),
+ messaging.lastFocusedMessageId,
+ onSuccess = onSuccess@{ message ->
+ closeActionMenu()
+ if (message.senderId.toString() != this@ChatActionMenu.context.database.myUserId) {
+ this@ChatActionMenu.context.shortToast("You can only edit your own messages")
+ return@onSuccess
+ }
+
+ val editText = EditText(viewGroup.context).apply {
+ setText(ProtoReader(message.messageContent?.content ?: return@apply).getString(2, 1) ?: run {
+ this@ChatActionMenu.context.shortToast("You can only edit text messages")
+ return@onSuccess
+ })
+ setTextColor(resources.getColor(android.R.color.white, context.theme))
+ postDelayed({
+ requestFocus()
+ setSelection(text.length)
+ context.getSystemService(android.content.Context.INPUT_METHOD_SERVICE)
+ .let { it as android.view.inputmethod.InputMethodManager }
+ .showSoftInput(this, android.view.inputmethod.InputMethodManager.SHOW_IMPLICIT)
+ }, 200)
+ }
+
+ this@ChatActionMenu.context.runOnUiThread {
+ ViewAppearanceHelper.newAlertDialogBuilder(this@ChatActionMenu.context.mainActivity!!)
+ .setPositiveButton("Save") { _, _ ->
+ val newMessageContent = ProtoWriter().apply {
+ from(2) { addString(1, editText.text.toString()) }
+ }.toByteArray()
+ message.messageContent?.content = newMessageContent
+ messaging.conversationManager?.editMessage(
+ message.messageDescriptor?.conversationId.toString(),
+ message.messageDescriptor?.messageId ?: return@setPositiveButton,
+ newMessageContent,
+ onSuccess = {
+ this@ChatActionMenu.context.coroutineScope.launch(Dispatchers.Main) {
+ message.messageMetadata?.isEdited = true
+ messaging.localUpdateMessage(
+ message.messageDescriptor?.conversationId.toString(),
+ message,
+ forceUpdate = true
+ )
+ }
+ },
+ onError = {
+ this@ChatActionMenu.context.shortToast("Failed to edit message: $it")
+ }
+ )
+ }
+ .setNegativeButton("Cancel") { dialog, _ -> dialog.dismiss() }
+ .setView(editText)
+ .setTitle("Edit message content")
+ .show()
+ }
+ }
+ )
+ }
+ })
+ }
+
if (context.config.experimental.convertMessageLocally.get()) {
injectButton(Button(viewGroup.context).apply {
text = this@ChatActionMenu.context.translation["chat_action_menu.convert_message"]
diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/core/ui/menu/impl/NewChatActionMenu.kt b/core/src/main/kotlin/me/rhunk/snapenhance/core/ui/menu/impl/NewChatActionMenu.kt
@@ -80,7 +80,7 @@ class NewChatActionMenu : AbstractMenu() {
FlowRow(
modifier = Modifier
.fillMaxWidth()
- .padding(5.dp),
+ .padding(2.dp),
horizontalArrangement = Arrangement.SpaceEvenly,
) {
Button(onClick = {
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
@@ -159,4 +159,20 @@ class ConversationManager(
.override("onError") { onError(it.arg<Any>(0).toString()) }.build()
getOneOnOneConversationIds.invoke(instanceNonNull(), userIds.map { it.toSnapUUID().instanceNonNull() }.toMutableList(), callback)
}
+
+ fun editMessage(conversationId: String, messageId: Long, content: ByteArray, onSuccess: () -> Unit, onError: (error: String) -> Unit) {
+ val editMessageMethod = instanceNonNull()::class.java.methods.first { it.name == "editMessage" }
+ editMessageMethod.invoke(instanceNonNull(), editMessageMethod.parameterTypes[0].dataBuilder {
+ set("mConversationId", conversationId.toSnapUUID().instanceNonNull())
+ set("mMessageId", messageId)
+ }, editMessageMethod.parameterTypes[1].dataBuilder {
+ set("mContent", content)
+ set("mMentionInfo", null)
+ }, CallbackBuilder(getCallbackClass("Callback"))
+ .override("onSuccess") { onSuccess() }
+ .override("onError") { onError(it.arg<Any>(0).toString()) }.build()
+ )
+ }
+
+ fun isEditMessageSupported() = instanceNonNull()::class.java.methods.any { it.name == "editMessage" }
}
\ No newline at end of file
diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/core/wrapper/impl/MessageMetadata.kt b/core/src/main/kotlin/me/rhunk/snapenhance/core/wrapper/impl/MessageMetadata.kt
@@ -20,9 +20,23 @@ class MessageMetadata(obj: Any?) : AbstractWrapper(obj){
@get:JSGetter @set:JSSetter
var seenBy by field("mSeenBy", uuidArrayListMapper)
@get:JSGetter @set:JSSetter
+ var screenRecordedBy by field("mScreenRecordedBy", uuidArrayListMapper)
+ @get:JSGetter @set:JSSetter
+ var screenShottedBy by field("mScreenShottedBy", uuidArrayListMapper)
+ @get:JSGetter @set:JSSetter
var reactions by field("mReactions") {
(it as ArrayList<*>).map { i -> UserIdToReaction(i) }.toMutableList()
}
@get:JSGetter @set:JSSetter
var isSaveable by field<Boolean>("mIsSaveable")
+ @get:JSGetter @set:JSSetter
+ var isEditable by field<Boolean>("mIsEditable")
+ @get:JSGetter @set:JSSetter
+ var isEdited by field<Boolean>("mIsEdited")
+ @get:JSGetter @set:JSSetter
+ var isErasable by field<Boolean>("mIsErasable")
+ @get:JSGetter @set:JSSetter
+ var isFriendLinkPending by field<Boolean>("mIsFriendLinkPending")
+ @get:JSGetter @set:JSSetter
+ var isReactable by field<Boolean>("mIsReactable")
}
\ No newline at end of file