From 58c4be44f2f5364091db59c0b32414f5ccb3bc7c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=CE=9ET=CE=9ERNAL?= Date: Fri, 20 Mar 2026 05:05:25 +0530 Subject: [PATCH] v1.4.6 --- ...kotlin-compiler-9309063494563612695.salive | 0 .../pages/features/FeaturesRootSection.kt | 64 +++++++++++++++++-- build.gradle.kts | 4 +- changelogs-stable.txt | 4 ++ .../features/impl/spying/FriendTracker.kt | 19 +++++- .../features/impl/spying/HalfSwipeNotifier.kt | 54 ++-------------- .../core/wrapper/impl/ConversationManager.kt | 33 ++++++---- gradle.properties | 4 +- 8 files changed, 112 insertions(+), 70 deletions(-) delete mode 100644 .kotlin/sessions/kotlin-compiler-9309063494563612695.salive diff --git a/.kotlin/sessions/kotlin-compiler-9309063494563612695.salive b/.kotlin/sessions/kotlin-compiler-9309063494563612695.salive deleted file mode 100644 index e69de29b..00000000 diff --git a/app/src/main/kotlin/me/eternal/purrfectsnap/ui/manager/pages/features/FeaturesRootSection.kt b/app/src/main/kotlin/me/eternal/purrfectsnap/ui/manager/pages/features/FeaturesRootSection.kt index 40b4b8d6..aed906b4 100644 --- a/app/src/main/kotlin/me/eternal/purrfectsnap/ui/manager/pages/features/FeaturesRootSection.kt +++ b/app/src/main/kotlin/me/eternal/purrfectsnap/ui/manager/pages/features/FeaturesRootSection.kt @@ -1273,11 +1273,65 @@ class FeaturesRootSection : Routes.Route() { } } - IconButton(onClick = { - haptic.performHapticFeedback(HapticFeedbackType.LongPress) - showSearchBar = !showSearchBar - }) { - Icon(imageVector = if (showSearchBar) Icons.Filled.Close else Icons.Filled.Search, contentDescription = null, tint = Color.White) + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(4.dp) + ) { + IconButton(onClick = { + haptic.performHapticFeedback(HapticFeedbackType.LongPress) + showSearchBar = !showSearchBar + }) { + Icon( + imageVector = if (showSearchBar) Icons.Filled.Close else Icons.Filled.Search, + contentDescription = null, + tint = Color.White + ) + } + + if (context.activity != null) { + Box { + IconButton(onClick = { + haptic.performHapticFeedback(HapticFeedbackType.LongPress) + showExportDropdownMenu = !showExportDropdownMenu + }) { + Icon( + imageVector = Icons.Filled.MoreVert, + contentDescription = null, + tint = Color.White + ) + } + DropdownMenu( + expanded = showExportDropdownMenu, + onDismissRequest = { showExportDropdownMenu = false }, + offset = DpOffset(0.dp, 8.dp), + containerColor = Color(0xFF161821), + shape = RoundedCornerShape(14.dp), + tonalElevation = 8.dp, + shadowElevation = 12.dp + ) { + actions.forEach { (name, icon, action) -> + DropdownMenuItem( + leadingIcon = { + Icon( + imageVector = icon, + contentDescription = null, + tint = PurrfectPalette.glowPrimary + ) + }, + text = { Text(text = name ?: "", color = Color.White) }, + onClick = { + action()() + showExportDropdownMenu = false + }, + colors = MenuDefaults.itemColors( + textColor = Color.White, + leadingIconColor = PurrfectPalette.glowPrimary + ) + ) + } + } + } + } } } } diff --git a/build.gradle.kts b/build.gradle.kts index ead7b231..86d009c7 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -33,8 +33,8 @@ tasks.register("getVersion") { } // You can still set these for legacy use by submodules or scripts: -rootProject.ext.set("appVersionName", providers.gradleProperty("APP_VERSION_NAME").orElse("1.4.5").get()) -rootProject.ext.set("appVersionCode", providers.gradleProperty("APP_VERSION_CODE").orElse("285").get().toInt()) +rootProject.ext.set("appVersionName", providers.gradleProperty("APP_VERSION_NAME").orElse("1.4.6").get()) +rootProject.ext.set("appVersionCode", providers.gradleProperty("APP_VERSION_CODE").orElse("286").get().toInt()) rootProject.ext.set("applicationId", "me.eternal.purrfectsnap") // buildHash: when PurrfectSnap or Snapchat is updated, mappings become outdated and auto-regenerate. // Include version code so each release has a different hash; use random for uniqueness within same version. diff --git a/changelogs-stable.txt b/changelogs-stable.txt index 5dd78395..d087b129 100644 --- a/changelogs-stable.txt +++ b/changelogs-stable.txt @@ -1,3 +1,7 @@ +## v1.4.6 +- Fix: Half Swipe Notifications for newer versions of snapchat +- Fix: No Config import/export button if Aphelion theme is turned off + ## v1.4.5 - New: Spoof Snap Score Locally(tq to RSR) - New: Video Recording Timer(tq to RSR) diff --git a/core/src/main/kotlin/me/eternal/purrfectsnap/core/features/impl/spying/FriendTracker.kt b/core/src/main/kotlin/me/eternal/purrfectsnap/core/features/impl/spying/FriendTracker.kt index b6604676..65eb5895 100644 --- a/core/src/main/kotlin/me/eternal/purrfectsnap/core/features/impl/spying/FriendTracker.kt +++ b/core/src/main/kotlin/me/eternal/purrfectsnap/core/features/impl/spying/FriendTracker.kt @@ -37,6 +37,11 @@ class FriendTracker : Feature("Friend Tracker") { )) } } private val conversationEntries = mutableMapOf, Long>() + private val peekingStateListeners = mutableListOf<(String, String, Boolean) -> Unit>() + + fun addOnPeekingStateChangedListener(listener: (conversationId: String, userId: String, peeking: Boolean) -> Unit) { + peekingStateListeners.add(listener) + } private fun getTrackedEvents(eventType: TrackerEventType): TrackerEventsResult? { return runCatching { @@ -207,6 +212,12 @@ class FriendTracker : Feature("Friend Tracker") { else -> {} } + when (eventType) { + TrackerEventType.STARTED_PEEKING -> peekingStateListeners.forEach { it(conversationId, userId, true) } + TrackerEventType.STOPPED_PEEKING -> peekingStateListeners.forEach { it(conversationId, userId, false) } + else -> {} + } + dispatchEvents(eventType, conversationId, userId) } @@ -261,7 +272,8 @@ class FriendTracker : Feature("Friend Tracker") { typing = stateMap[4], wasTyping = stateMap[5], speaking = stateMap[6] && stateMap[4], - peeking = stateMap[8] + // Snapchat appears to have shifted the peeking flag by one bit on newer builds. + peeking = stateMap.getOrElse(8) { false } || stateMap.getOrElse(9) { false } ) } @@ -385,7 +397,8 @@ class FriendTracker : Feature("Friend Tracker") { override fun init() { val sessionEventsConfig = context.config.friendTracker - if (sessionEventsConfig.globalState != true) return + val shouldProcessSessionEvents = sessionEventsConfig.globalState == true || peekingStateListeners.isNotEmpty() + if (!shouldProcessSessionEvents) return if (sessionEventsConfig.allowRunningInBackground.get()) { findClass("com.snapchat.client.duplex.DuplexClient\$CppProxy").apply { @@ -402,7 +415,7 @@ class FriendTracker : Feature("Friend Tracker") { } } - if (sessionEventsConfig.recordMessagingEvents.get()) { + if (sessionEventsConfig.recordMessagingEvents.get() || peekingStateListeners.isNotEmpty()) { val messageHandlerClass = findClass("com.snapchat.client.duplex.MessageHandler\$CppProxy").apply { hook("onReceive", HookStage.BEFORE) { param -> param.setResult(null) diff --git a/core/src/main/kotlin/me/eternal/purrfectsnap/core/features/impl/spying/HalfSwipeNotifier.kt b/core/src/main/kotlin/me/eternal/purrfectsnap/core/features/impl/spying/HalfSwipeNotifier.kt index 447fa1c8..3dd15ede 100644 --- a/core/src/main/kotlin/me/eternal/purrfectsnap/core/features/impl/spying/HalfSwipeNotifier.kt +++ b/core/src/main/kotlin/me/eternal/purrfectsnap/core/features/impl/spying/HalfSwipeNotifier.kt @@ -6,17 +6,10 @@ import android.app.NotificationManager import android.app.PendingIntent import me.eternal.purrfectsnap.common.Constants import me.eternal.purrfectsnap.core.features.Feature -import me.eternal.purrfectsnap.core.util.hook.HookStage -import me.eternal.purrfectsnap.core.util.hook.hook -import me.eternal.purrfectsnap.core.util.hook.hookConstructor -import me.eternal.purrfectsnap.core.util.ktx.getObjectField -import me.eternal.purrfectsnap.mapper.impl.CallbackMapper -import java.util.concurrent.ConcurrentHashMap import kotlin.time.Duration.Companion.milliseconds class HalfSwipeNotifier : Feature("Half Swipe Notifier") { - private val peekingConversations = ConcurrentHashMap>() - private val startPeekingTimestamps = ConcurrentHashMap() + private val startPeekingTimestamps = java.util.concurrent.ConcurrentHashMap() private val halfSwipeListeners = mutableListOf<(String, String, Long) -> Unit>() private val notificationManager get() = context.androidContext.getSystemService(NotificationManager::class.java) @@ -39,44 +32,11 @@ class HalfSwipeNotifier : Feature("Half Swipe Notifier") { override fun init() { if (context.config.messaging.halfSwipeNotifier.globalState != true) return - lateinit var presenceService: Any - - findClass("com.snapchat.talkcorev3.PresenceService\$CppProxy").hookConstructor(HookStage.AFTER) { - presenceService = it.thisObject() - } - - context.mappings.useMapper(CallbackMapper::class) { - callbacks.getClass("PresenceServiceDelegate")?.hook("notifyActiveConversationsChanged", HookStage.BEFORE) { - val activeConversations = presenceService::class.java.methods.find { it.name == "getActiveConversations" }?.invoke(presenceService) as? Map<*, *> ?: return@hook // conversationId, conversationInfo (this.mPeekingParticipants) - - if (activeConversations.isEmpty()) { - peekingConversations.forEach { - val conversationId = it.key - val peekingParticipantsIds = it.value - peekingParticipantsIds.forEach { userId -> - endPeeking(conversationId, userId) - } - } - peekingConversations.clear() - return@hook - } - - activeConversations.forEach { (conversationId, conversationInfo) -> - val peekingParticipantsIds = (conversationInfo?.getObjectField("mPeekingParticipants") as? List<*>)?.map { it.toString() } ?: return@forEach - val cachedPeekingParticipantsIds = peekingConversations[conversationId] ?: emptyList() - - val newPeekingParticipantsIds = peekingParticipantsIds - cachedPeekingParticipantsIds.toSet() - val exitedPeekingParticipantsIds = cachedPeekingParticipantsIds - peekingParticipantsIds.toSet() - - newPeekingParticipantsIds.forEach { userId -> - startPeeking(conversationId.toString(), userId) - } - - exitedPeekingParticipantsIds.forEach { userId -> - endPeeking(conversationId.toString(), userId) - } - peekingConversations[conversationId.toString()] = peekingParticipantsIds - } + context.feature(FriendTracker::class).addOnPeekingStateChangedListener { conversationId, userId, peeking -> + if (peeking) { + startPeeking(conversationId, userId) + } else { + endPeeking(conversationId, userId) } } } @@ -139,4 +99,4 @@ class HalfSwipeNotifier : Feature("Half Swipe Notifier") { } } } -} \ No newline at end of file +} diff --git a/core/src/main/kotlin/me/eternal/purrfectsnap/core/wrapper/impl/ConversationManager.kt b/core/src/main/kotlin/me/eternal/purrfectsnap/core/wrapper/impl/ConversationManager.kt index 55c957dd..a84886d6 100644 --- a/core/src/main/kotlin/me/eternal/purrfectsnap/core/wrapper/impl/ConversationManager.kt +++ b/core/src/main/kotlin/me/eternal/purrfectsnap/core/wrapper/impl/ConversationManager.kt @@ -117,18 +117,29 @@ class ConversationManager( set("mServerConversationId", conversationId.toSnapUUID().instanceNonNull()) set("mServerMessageId", serverMessageId) } + val conversationUuid = conversationId.toSnapUUID().instanceNonNull() - fetchMessageByServerId.invoke( - instanceNonNull(), - serverMessageIdentifier, - CallbackBuilder(getCallbackClass("FetchMessageCallback")) - .override("onFetchMessageComplete") { param -> - onSuccess(Message(param.arg(0))) - } - .override("onError") { - onError(it.arg(0).toString()) - }.build() - ) + val callback = CallbackBuilder(getCallbackClass("FetchMessageCallback")) + .override("onFetchMessageComplete") { param -> + onSuccess(Message(param.arg(0))) + } + .override("onError") { + onError(it.arg(0).toString()) + }.build() + + val args = fetchMessageByServerId.parameterTypes.mapIndexed { index, parameterType -> + when { + parameterType.isInstance(serverMessageIdentifier) -> serverMessageIdentifier + parameterType.isInstance(callback) -> callback + parameterType.isInstance(conversationUuid) -> conversationUuid + parameterType == Boolean::class.javaPrimitiveType || parameterType == Boolean::class.javaObjectType -> false + else -> throw IllegalStateException( + "Unsupported fetchMessageByServerId parameter at index $index: ${parameterType.name}" + ) + } + }.toTypedArray() + + fetchMessageByServerId.invoke(instanceNonNull(), *args) } fun fetchMessagesByServerIds(conversationId: String, serverMessageIds: List, onSuccess: (List) -> Unit, onError: (error: String) -> Unit) { diff --git a/gradle.properties b/gradle.properties index 15e33235..ba2e92e9 100644 --- a/gradle.properties +++ b/gradle.properties @@ -7,8 +7,8 @@ org.gradle.configuration-cache=true org.gradle.configuration-cache.problems=warn nativeAbis=arm64-v8a -APP_VERSION_NAME=1.4.5 -APP_VERSION_CODE=285 +APP_VERSION_NAME=1.4.6 +APP_VERSION_CODE=286 debug_build_hash=18fe2a814d0e2eb5 psIntegrityPinnedSha256= EXPECTED_CERT_SHA256=0f188cb17d8ea4902ee15ce98f4928ba4226a4790fa3c52f62d776b08e46ca1c