This commit is contained in:
ΞTΞRNAL
2026-03-20 05:05:25 +05:30
parent 3d83c3116c
commit 58c4be44f2
8 changed files with 112 additions and 70 deletions

View File

@@ -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
)
)
}
}
}
}
}
}
}

View File

@@ -33,8 +33,8 @@ tasks.register<GetVersionTask>("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.

View File

@@ -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)

View File

@@ -37,6 +37,11 @@ class FriendTracker : Feature("Friend Tracker") {
))
} }
private val conversationEntries = mutableMapOf<Pair<String, String>, 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)

View File

@@ -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<String, List<String>>()
private val startPeekingTimestamps = ConcurrentHashMap<String, Long>()
private val startPeekingTimestamps = java.util.concurrent.ConcurrentHashMap<String, Long>()
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") {
}
}
}
}
}

View File

@@ -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<Any>(0).toString())
}.build()
)
val callback = CallbackBuilder(getCallbackClass("FetchMessageCallback"))
.override("onFetchMessageComplete") { param ->
onSuccess(Message(param.arg(0)))
}
.override("onError") {
onError(it.arg<Any>(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<Long>, onSuccess: (List<Message>) -> Unit, onError: (error: String) -> Unit) {

View File

@@ -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