fix: prevent message list auto scroll
Signed-off-by: rhunk <101876869+rhunk@users.noreply.github.com>
This commit is contained in:
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -38,6 +38,7 @@ class ClassMapper(
|
||||
MemoriesPresenterMapper(),
|
||||
StreaksExpirationMapper(),
|
||||
COFObservableMapper(),
|
||||
FoldingLayoutMapper(),
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
package me.rhunk.snapenhance.mapper.impl
|
||||
|
||||
import com.android.tools.smali.dexlib2.iface.instruction.formats.Instruction22c
|
||||
import com.android.tools.smali.dexlib2.iface.instruction.formats.Instruction35c
|
||||
import com.android.tools.smali.dexlib2.iface.reference.FieldReference
|
||||
import com.android.tools.smali.dexlib2.iface.reference.MethodReference
|
||||
import me.rhunk.snapenhance.mapper.AbstractClassMapper
|
||||
|
||||
class FoldingLayoutMapper: AbstractClassMapper("FoldingLayoutMapper") {
|
||||
val onLayoutCompletedMethod = string("onLayoutCompletedMethod")
|
||||
val shouldScrollToBottomField = string("shouldScrollToBottomField")
|
||||
val sizeSparseArrayField = string("sizeSparseArrayField")
|
||||
val recyclerViewField = string("recyclerViewField")
|
||||
|
||||
init {
|
||||
mapper {
|
||||
val foldingLayoutManagerClass = getClass("Lcom/snap/messaging/chat/features/messagelist/FoldingLayoutManager;") ?: return@mapper
|
||||
|
||||
sizeSparseArrayField.set(foldingLayoutManagerClass.fields.firstOrNull { it.type == "Landroid/util/SparseIntArray;" }?.name ?: return@mapper)
|
||||
recyclerViewField.set(foldingLayoutManagerClass.fields.firstOrNull { it.type == "Landroidx/recyclerview/widget/RecyclerView;" }?.name ?: return@mapper)
|
||||
|
||||
foldingLayoutManagerClass.methods.firstOrNull {
|
||||
it.parameterTypes.size == 1 && it.returnType == "V" && it.implementation?.instructions?.any {
|
||||
((it as? Instruction35c)?.reference as? MethodReference)?.name == "invoke"
|
||||
} == true
|
||||
}?.let { method ->
|
||||
onLayoutCompletedMethod.set(method.name)
|
||||
for (instruction in method.implementation?.instructions ?: return@mapper) {
|
||||
shouldScrollToBottomField.set(((instruction as? Instruction22c)?.reference as? FieldReference)?.takeIf { it.type == "Z" }?.name ?: continue)
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user