v1.4.6
This commit is contained in:
@@ -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
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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") {
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user