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:
Mcommon/src/main/assets/lang/en_US.json | 4++++
Mcommon/src/main/kotlin/me/rhunk/snapenhance/common/config/impl/Experimental.kt | 1+
Mcore/src/main/kotlin/me/rhunk/snapenhance/core/features/impl/messaging/Messaging.kt | 8+++++++-
Mcore/src/main/kotlin/me/rhunk/snapenhance/core/ui/menu/impl/ChatActionMenu.kt | 78+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-
Mcore/src/main/kotlin/me/rhunk/snapenhance/core/ui/menu/impl/NewChatActionMenu.kt | 2+-
Mcore/src/main/kotlin/me/rhunk/snapenhance/core/wrapper/impl/ConversationManager.kt | 16++++++++++++++++
Mcore/src/main/kotlin/me/rhunk/snapenhance/core/wrapper/impl/MessageMetadata.kt | 14++++++++++++++
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