From 5b12844a24ab17e10cd6a552dead84851967ca56 Mon Sep 17 00:00:00 2001 From: schrodingerspet Date: Sun, 19 Apr 2026 23:39:21 +0530 Subject: [PATCH] fix(ui): hide selected users from the real friend feed Use more reliable hidden-ID matching for friend-feed rows and add a fallback hide during binding to prevent row reappearance. Keep Best Friends, send, and search behavior unchanged. --- .../features/impl/ui/HideFriendFeedEntry.kt | 96 ++++++++++++++++--- 1 file changed, 81 insertions(+), 15 deletions(-) diff --git a/core/src/main/kotlin/me/eternal/purrfectsnap/core/features/impl/ui/HideFriendFeedEntry.kt b/core/src/main/kotlin/me/eternal/purrfectsnap/core/features/impl/ui/HideFriendFeedEntry.kt index b9f47a7c..87dd8d32 100644 --- a/core/src/main/kotlin/me/eternal/purrfectsnap/core/features/impl/ui/HideFriendFeedEntry.kt +++ b/core/src/main/kotlin/me/eternal/purrfectsnap/core/features/impl/ui/HideFriendFeedEntry.kt @@ -1,17 +1,26 @@ package me.eternal.purrfectsnap.core.features.impl.ui +import android.view.View import me.eternal.purrfectsnap.common.data.MessagingRuleType import me.eternal.purrfectsnap.common.data.RuleState - +import me.eternal.purrfectsnap.core.event.events.impl.BindViewEvent import me.eternal.purrfectsnap.core.features.MessagingRuleFeature +import me.eternal.purrfectsnap.core.ui.hideViewCompletely import me.eternal.purrfectsnap.core.util.dataBuilder import me.eternal.purrfectsnap.core.util.hook.HookStage import me.eternal.purrfectsnap.core.util.hook.hook import me.eternal.purrfectsnap.core.util.ktx.getObjectField import me.eternal.purrfectsnap.core.wrapper.impl.SnapUUID import me.eternal.purrfectsnap.mapper.impl.CallbackMapper +import java.util.ArrayList class HideFriendFeedEntry : MessagingRuleFeature("HideFriendFeedEntry", ruleType = MessagingRuleType.HIDE_FRIEND_FEED) { + @Volatile + private var cachedRuleIds: Set = emptySet() + + @Volatile + private var cachedRuleIdsAt = 0L + private fun createDeletedFeedEntry(conversationIdInstance: Any) = findClass("com.snapchat.client.messaging.DeletedFeedEntry").dataBuilder { from("mFeedEntryIdentifier") { set("mConversationId", conversationIdInstance) @@ -19,10 +28,47 @@ class HideFriendFeedEntry : MessagingRuleFeature("HideFriendFeedEntry", ruleType set("mReason", "CLEAR_CONVERSATION") } - private fun filterFriendFeed(entries: ArrayList, deletedEntries: ArrayList? = null) { + private fun getRuleIdsSnapshot(): Set { + val now = System.currentTimeMillis() + if (now - cachedRuleIdsAt <= 1_000L) return cachedRuleIds + + return context.bridgeClient.getRuleIds(ruleType).toSet().also { + cachedRuleIds = it + cachedRuleIdsAt = now + } + } + + private fun resolveRuleTargets(conversationId: String): Set { + val targets = linkedSetOf(conversationId) + context.database.getDMOtherParticipant(conversationId)?.let { targets.add(it) } + context.database.getFeedEntryByConversationId(conversationId)?.let { entry -> + entry.friendUserId?.let { targets.add(it) } + entry.participants?.forEach { targets.add(it) } + } + return targets + } + + private fun shouldHideConversation( + conversationId: String, + ruleIds: Set, + ruleState: RuleState? + ): Boolean { + if (ruleState == null) return false + val isExplicitRuleMatch = resolveRuleTargets(conversationId).any { it in ruleIds } + return if (ruleState == RuleState.BLACKLIST) !isExplicitRuleMatch else isExplicitRuleMatch + } + + private fun filterFriendFeed( + entries: ArrayList, + ruleIds: Set, + ruleState: RuleState?, + deletedEntries: ArrayList? = null + ) { + if (ruleState == null || entries.isEmpty()) return entries.removeIf { feedEntry -> val conversationIdInstance = feedEntry.getObjectField("mConversationId") ?: return@removeIf false - if (canUseRule(SnapUUID(conversationIdInstance).toString())) { + val conversationId = SnapUUID(conversationIdInstance).toString() + if (shouldHideConversation(conversationId, ruleIds, ruleState)) { deletedEntries?.add(createDeletedFeedEntry(conversationIdInstance)!!) true } else { @@ -31,6 +77,12 @@ class HideFriendFeedEntry : MessagingRuleFeature("HideFriendFeedEntry", ruleType } } + private fun hideBoundChatFeedRow(view: View) { + view.hideViewCompletely() + (view.parent as? View)?.hideViewCompletely() + (view.parent?.parent as? View)?.hideViewCompletely() + } + private fun hookCallbackMethod( hookedCallbacks: MutableSet, callbackClassName: String, @@ -51,6 +103,14 @@ class HideFriendFeedEntry : MessagingRuleFeature("HideFriendFeedEntry", ruleType override fun init() { if (!context.config.userInterface.hideFriendFeedEntry.get()) return + context.event.subscribe(BindViewEvent::class) { event -> + event.friendFeedItem { conversationId -> + if (shouldHideConversation(conversationId, getRuleIdsSnapshot(), getRuleState())) { + hideBoundChatFeedRow(event.view) + } + } + } + context.mappings.useMapper(CallbackMapper::class) { classLoader = context.androidContext.classLoader val hasFetchAndSyncCallback = callbacks.getAsMap()?.entries?.any { @@ -69,13 +129,13 @@ class HideFriendFeedEntry : MessagingRuleFeature("HideFriendFeedEntry", ruleType when { callbackName.startsWith("FetchAndSyncFeed") && callbackName.endsWith("Callback") -> { hookCallbackMethod(hookedCallbacks, callbackClassName ?: return@forEach, "onFetchAndSyncFeedComplete") { param -> - val deletedConversations: ArrayList = param.arg(2) - filterFriendFeed(param.arg(0), deletedConversations) + val entries = param.argNullable>(0) ?: return@hookCallbackMethod + val deletedConversations = param.argNullable>(2) + val ruleIds = getRuleIdsSnapshot() + val ruleState = getRuleState() + filterFriendFeed(entries, ruleIds, ruleState, deletedConversations) - if (deletedConversations.any { - val uuid = SnapUUID(it.getObjectField("mFeedEntryIdentifier")?.getObjectField("mConversationId")).toString() - context.database.getFeedEntryByConversationId(uuid) != null - }) { + if (deletedConversations?.isNotEmpty() == true) { param.setArg(4, true) } } @@ -83,34 +143,40 @@ class HideFriendFeedEntry : MessagingRuleFeature("HideFriendFeedEntry", ruleType callbackName.contains("SyncFeed") && callbackName.endsWith("Callback") -> { hookCallbackMethod(hookedCallbacks, callbackClassName ?: return@forEach, "onSyncFeedComplete") { param -> - filterFriendFeed(param.arg(0), param.argNullable(2)) + val entries = param.argNullable>(0) ?: return@hookCallbackMethod + filterFriendFeed(entries, getRuleIdsSnapshot(), getRuleState(), param.argNullable(2)) } } callbackName == "FetchFeedCallback" || callbackName.contains("FetchFeedCallback") -> { hookCallbackMethod(hookedCallbacks, callbackClassName ?: return@forEach, "onFetchFeedComplete") { param -> - filterFriendFeed(param.arg(0)) + val entries = param.argNullable>(0) ?: return@hookCallbackMethod + filterFriendFeed(entries, getRuleIdsSnapshot(), getRuleState()) } } callbackName == "FetchFeedEntriesCallback" || callbackName.contains("FetchFeedEntriesCallback") -> { hookCallbackMethod(hookedCallbacks, callbackClassName ?: return@forEach, "onFetchFeedEntriesComplete") { param -> - filterFriendFeed(param.arg(0)) + val entries = param.argNullable>(0) ?: return@hookCallbackMethod + filterFriendFeed(entries, getRuleIdsSnapshot(), getRuleState()) } } callbackName == "QueryFeedCallback" || callbackName.contains("QueryFeedCallback") -> { hookCallbackMethod(hookedCallbacks, callbackClassName ?: return@forEach, "onQueryFeedComplete") { param -> - filterFriendFeed(param.arg(0)) + val entries = param.argNullable>(0) ?: return@hookCallbackMethod + filterFriendFeed(entries, getRuleIdsSnapshot(), getRuleState()) } } callbackName == "FeedManagerDelegate" -> { hookCallbackMethod(hookedCallbacks, callbackClassName ?: return@forEach, "onFeedEntriesUpdated") { param -> - filterFriendFeed(param.arg(0)) + val entries = param.argNullable>(0) ?: return@hookCallbackMethod + filterFriendFeed(entries, getRuleIdsSnapshot(), getRuleState()) } hookCallbackMethod(hookedCallbacks, callbackClassName ?: return@forEach, "onInternalSyncFeed") { param -> - filterFriendFeed(param.arg(0)) + val entries = param.argNullable>(0) ?: return@hookCallbackMethod + filterFriendFeed(entries, getRuleIdsSnapshot(), getRuleState()) } } }