commit 79be5da030dd2fa582ad3c7cb0bdd57181d072dc
parent bfe367efd0405408442f34f171497e3044e3220c
Author: rhunk <101876869+rhunk@users.noreply.github.com>
Date:   Sun,  3 Dec 2023 12:42:57 +0100
feat: prevent message list auto scroll
Diffstat:
4 files changed, 88 insertions(+), 0 deletions(-)
diff --git a/common/src/main/assets/lang/en_US.json b/common/src/main/assets/lang/en_US.json
@@ -259,6 +259,10 @@
                         "name": "Enhanced Friend Map Nametags",
                         "description": "Improves the Nametags of friends on the Snapmap"
                     },
+                    "prevent_message_list_auto_scroll": {
+                        "name": "Prevent Message List Auto Scroll",
+                        "description": "Prevents the message list from scrolling to the bottom when sending/receiving a message"
+                    },
                     "streak_expiration_info": {
                         "name": "Show Streak Expiration Info",
                         "description": "Shows a Streak Expiration timer next to the Streaks counter"
diff --git a/common/src/main/kotlin/me/rhunk/snapenhance/common/config/impl/UserInterfaceTweaks.kt b/common/src/main/kotlin/me/rhunk/snapenhance/common/config/impl/UserInterfaceTweaks.kt
@@ -29,6 +29,7 @@ class UserInterfaceTweaks : ConfigContainer() {
     val snapPreview = boolean("snap_preview") { addNotices(FeatureNotice.UNSTABLE); requireRestart() }
     val bootstrapOverride = container("bootstrap_override", BootstrapOverride()) { requireRestart() }
     val mapFriendNameTags = boolean("map_friend_nametags") { requireRestart() }
+    val preventMessageListAutoScroll = boolean("prevent_message_list_auto_scroll") { requireRestart(); addNotices(FeatureNotice.UNSTABLE) }
     val streakExpirationInfo = boolean("streak_expiration_info") { requireRestart() }
     val hideFriendFeedEntry = boolean("hide_friend_feed_entry") { requireRestart() }
     val hideStreakRestore = boolean("hide_streak_restore") { requireRestart() }
diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/core/features/impl/tweaks/PreventMessageListAutoScroll.kt b/core/src/main/kotlin/me/rhunk/snapenhance/core/features/impl/tweaks/PreventMessageListAutoScroll.kt
@@ -0,0 +1,80 @@
+package me.rhunk.snapenhance.core.features.impl.tweaks
+
+import android.view.View
+import me.rhunk.snapenhance.core.event.events.impl.BindViewEvent
+import me.rhunk.snapenhance.core.features.Feature
+import me.rhunk.snapenhance.core.features.FeatureLoadParams
+import me.rhunk.snapenhance.core.util.hook.HookStage
+import me.rhunk.snapenhance.core.util.hook.hook
+import me.rhunk.snapenhance.core.wrapper.impl.Message
+import me.rhunk.snapenhance.core.wrapper.impl.SnapUUID
+
+class PreventMessageListAutoScroll : Feature("PreventMessageListAutoScroll", loadParams = FeatureLoadParams.ACTIVITY_CREATE_SYNC) {
+    private var openedConversationId: String? = null
+    private val focusedMessages = mutableMapOf<View, Long>()
+    private var firstFocusedMessageId: Long? = null
+    private val delayedMessageUpdates = mutableListOf<() -> Unit>()
+
+    override fun onActivityCreate() {
+        if (!context.config.userInterface.preventMessageListAutoScroll.get()) return
+
+        context.mappings.getMappedClass("callbacks", "ConversationManagerDelegate").hook("onConversationUpdated", HookStage.BEFORE) { param ->
+            val updatedMessage = param.arg<ArrayList<*>>(2).map { Message(it) }.firstOrNull() ?: return@hook
+            if (openedConversationId != updatedMessage.messageDescriptor?.conversationId.toString()) return@hook
+
+            // cancel if the message is already in focus
+            if (focusedMessages.entries.any { entry -> entry.value == updatedMessage.messageDescriptor?.messageId && entry.key.isAttachedToWindow }) return@hook
+
+            val conversationLastMessages = context.database.getMessagesFromConversationId(
+                openedConversationId.toString(),
+                4
+            ) ?: return@hook
+
+            if (conversationLastMessages.none {
+                focusedMessages.entries.any { entry -> entry.value == it.clientMessageId.toLong() && entry.key.isAttachedToWindow }
+            }) {
+                synchronized(delayedMessageUpdates) {
+                    if (firstFocusedMessageId == null) firstFocusedMessageId = conversationLastMessages.lastOrNull()?.clientMessageId?.toLong()
+                    delayedMessageUpdates.add {
+                        param.invokeOriginal()
+                    }
+                }
+                param.setResult(null)
+            }
+        }
+
+        context.classCache.conversationManager.apply {
+            hook("enterConversation", HookStage.BEFORE) { param ->
+                openedConversationId = SnapUUID(param.arg(0)).toString()
+            }
+            hook("exitConversation", HookStage.BEFORE) {
+                openedConversationId = null
+                firstFocusedMessageId = null
+                synchronized(focusedMessages) {
+                    focusedMessages.clear()
+                }
+                synchronized(delayedMessageUpdates) {
+                    delayedMessageUpdates.clear()
+                }
+            }
+        }
+
+        context.event.subscribe(BindViewEvent::class) { event ->
+            event.chatMessage { conversationId, messageId ->
+                if (conversationId != openedConversationId) return@chatMessage
+                synchronized(focusedMessages) {
+                    focusedMessages[event.view] = messageId.toLong()
+                }
+
+                if (delayedMessageUpdates.isNotEmpty() && focusedMessages.entries.any { entry -> entry.value == firstFocusedMessageId && entry.key.isAttachedToWindow }) {
+                    delayedMessageUpdates.apply {
+                        synchronized(this) {
+                            removeIf { it(); true }
+                            firstFocusedMessageId = null
+                        }
+                    }
+                }
+            }
+        }
+    }
+}+
\ No newline at end of file
diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/core/manager/impl/FeatureManager.kt b/core/src/main/kotlin/me/rhunk/snapenhance/core/manager/impl/FeatureManager.kt
@@ -20,6 +20,7 @@ import me.rhunk.snapenhance.core.features.impl.spying.MessageLogger
 import me.rhunk.snapenhance.core.features.impl.spying.StealthMode
 import me.rhunk.snapenhance.core.features.impl.tweaks.BypassScreenshotDetection
 import me.rhunk.snapenhance.core.features.impl.tweaks.CameraTweaks
+import me.rhunk.snapenhance.core.features.impl.tweaks.PreventMessageListAutoScroll
 import me.rhunk.snapenhance.core.features.impl.ui.*
 import me.rhunk.snapenhance.core.logger.CoreLogger
 import me.rhunk.snapenhance.core.manager.Manager
@@ -111,6 +112,7 @@ class FeatureManager(
             Stories::class,
             DisableComposerModules::class,
             FideliusIndicator::class,
+            PreventMessageListAutoScroll::class,
         )
 
         initializeFeatures()