fix: prevent message list auto scroll

Signed-off-by: rhunk <101876869+rhunk@users.noreply.github.com>
This commit is contained in:
rhunk
2025-07-04 01:28:33 +02:00
parent 0f2edca459
commit 4e4f77a213
3 changed files with 67 additions and 58 deletions

View File

@@ -1,78 +1,51 @@
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.event.events.impl.ConversationUpdateEvent
import android.util.SparseIntArray
import androidx.core.util.size
import me.rhunk.snapenhance.core.features.Feature
import me.rhunk.snapenhance.core.util.hook.HookStage
import me.rhunk.snapenhance.core.util.hook.hook
import me.rhunk.snapenhance.core.wrapper.impl.SnapUUID
import me.rhunk.snapenhance.core.util.ktx.getObjectField
import me.rhunk.snapenhance.core.util.ktx.setObjectField
import me.rhunk.snapenhance.mapper.impl.FoldingLayoutMapper
class PreventMessageListAutoScroll : Feature("PreventMessageListAutoScroll") {
private var openedConversationId: String? = null
private val focusedMessages = mutableMapOf<View, Long>()
private var firstFocusedMessageId: Long? = null
private val delayedMessageUpdates = mutableListOf<() -> Unit>()
companion object {
private const val MIN_SCROLL_ITEMS = 4
}
override fun init() {
if (!context.config.userInterface.preventMessageListAutoScroll.get()) return
onNextActivityCreate {
context.event.subscribe(ConversationUpdateEvent::class) { event ->
val updatedMessage = event.messages.firstOrNull() ?: return@subscribe
if (openedConversationId != updatedMessage.messageDescriptor?.conversationId.toString()) return@subscribe
val foldingLayoutManager = findClass("com.snap.messaging.chat.features.messagelist.FoldingLayoutManager")
val recyclerViewClass = findClass("androidx.recyclerview.widget.RecyclerView")
// cancel if the message is already in focus
if (focusedMessages.entries.any { entry -> entry.value == updatedMessage.messageDescriptor?.messageId && entry.key.isAttachedToWindow }) return@subscribe
val computeVerticalScrollOffsetMethod = recyclerViewClass.getMethod("computeVerticalScrollOffset")
val computeVerticalScrollExtentMethod = recyclerViewClass.getMethod("computeVerticalScrollExtent")
val computeVerticalScrollRangeMethod = recyclerViewClass.getMethod("computeVerticalScrollRange")
val conversationLastMessages = context.database.getMessagesFromConversationId(
openedConversationId.toString(),
4
) ?: return@subscribe
context.mappings.useMapper(FoldingLayoutMapper::class) {
foldingLayoutManager.hook(onLayoutCompletedMethod.getAsString() ?: throw NoSuchMethodError("onLayoutCompleted"), HookStage.BEFORE) { param ->
val instance = param.thisObject<Any>()
val shouldScrollToBottom = instance.getObjectField(shouldScrollToBottomField.getAsString()!!) as Boolean
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 {
event.adapter.invokeOriginal()
if (shouldScrollToBottom) {
val sparseIntArray = param.thisObject<Any>().getObjectField(sizeSparseArrayField.getAsString()!!) as SparseIntArray
val recyclerView = param.thisObject<Any>().getObjectField(recyclerViewField.getAsString()!!)
val scrollOffset = computeVerticalScrollRangeMethod.invoke(recyclerView) as Int - (computeVerticalScrollOffsetMethod.invoke(recyclerView) as Int + computeVerticalScrollExtentMethod.invoke(recyclerView) as Int)
var layoutSizeSum = 0
for (i in 0 until sparseIntArray.size) {
if (sparseIntArray.keyAt(i) < MIN_SCROLL_ITEMS) {
layoutSizeSum += sparseIntArray.valueAt(i)
}
}
event.adapter.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
}
}
if (scrollOffset <= 0 || scrollOffset > layoutSizeSum) {
instance.setObjectField(shouldScrollToBottomField.getAsString()!!, false)
}
}
}