Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7a8ad81d48 | ||
|
|
7db60d6eb1 |
@@ -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.9").get())
|
||||
rootProject.ext.set("appVersionCode", providers.gradleProperty("APP_VERSION_CODE").orElse("290").get().toInt())
|
||||
rootProject.ext.set("appVersionName", providers.gradleProperty("APP_VERSION_NAME").orElse("1.5.0").get())
|
||||
rootProject.ext.set("appVersionCode", providers.gradleProperty("APP_VERSION_CODE").orElse("292").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.5.0
|
||||
- Fix: Skip when marking as seen for newer versions of Snapchat
|
||||
- New: Hide Conversation Toolbox UI
|
||||
|
||||
## v1.4.9
|
||||
- Fix: Force AMOLED Theme for newer versions of Snapchat
|
||||
- New: Redesign some dialogs(Call confirmation & Mark Snaps as seen)
|
||||
|
||||
@@ -2190,6 +2190,10 @@
|
||||
"force_message_encryption": {
|
||||
"name": "Force Message Encryption",
|
||||
"description": "Prevents sending encrypted messages to people who don't have E2E Encryption enabled only when multiple conversations are selected"
|
||||
},
|
||||
"hide_conversation_toolbox_ui": {
|
||||
"name": "Hide Conversation Toolbox UI",
|
||||
"description": "Hides the PurrfectSnap conversation toolbox button that appears in Snapchat when End-To-End Encryption is enabled"
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
@@ -51,6 +51,7 @@ class Experimental : ConfigContainer() {
|
||||
class E2EEConfig : ConfigContainer(hasGlobalState = true) {
|
||||
val encryptedMessageIndicator = boolean("encrypted_message_indicator")
|
||||
val forceMessageEncryption = boolean("force_message_encryption")
|
||||
val hideConversationToolboxUi = boolean("hide_conversation_toolbox_ui")
|
||||
}
|
||||
|
||||
class AccountSwitcherConfig : ConfigContainer(hasGlobalState = true) {
|
||||
|
||||
@@ -249,30 +249,34 @@ class EndToEndEncryption : MessagingRuleFeature(
|
||||
}
|
||||
|
||||
onNextActivityCreate(defer = true) {
|
||||
context.feature(ConversationToolbox::class).addComposable(translation["confirmation_dialogs.title"], filter = {
|
||||
context.database.getDMOtherParticipant(it) != null
|
||||
}) { dialog, conversationId ->
|
||||
val friendId = remember {
|
||||
context.database.getDMOtherParticipant(conversationId)
|
||||
} ?: return@addComposable
|
||||
val fingerprint = remember {
|
||||
runCatching {
|
||||
e2eeInterface.getSecretFingerprint(friendId)
|
||||
}.getOrNull()
|
||||
}
|
||||
if (fingerprint != null) {
|
||||
Text(translation.format("toolbox.shared_key_fingerprint", "fingerprint" to fingerprint))
|
||||
} else {
|
||||
Text(translation["toolbox.no_shared_key"])
|
||||
}
|
||||
Spacer(modifier = Modifier.height(10.dp))
|
||||
Button(onClick = {
|
||||
dialog.dismiss()
|
||||
warnKeyOverwrite(friendId) {
|
||||
askForKeys(conversationId)
|
||||
val hideConversationToolboxUi by context.config.experimental.e2eEncryption.hideConversationToolboxUi
|
||||
|
||||
if (!hideConversationToolboxUi) {
|
||||
context.feature(ConversationToolbox::class).addComposable(translation["confirmation_dialogs.title"], filter = {
|
||||
context.database.getDMOtherParticipant(it) != null
|
||||
}) { dialog, conversationId ->
|
||||
val friendId = remember {
|
||||
context.database.getDMOtherParticipant(conversationId)
|
||||
} ?: return@addComposable
|
||||
val fingerprint = remember {
|
||||
runCatching {
|
||||
e2eeInterface.getSecretFingerprint(friendId)
|
||||
}.getOrNull()
|
||||
}
|
||||
if (fingerprint != null) {
|
||||
Text(translation.format("toolbox.shared_key_fingerprint", "fingerprint" to fingerprint))
|
||||
} else {
|
||||
Text(translation["toolbox.no_shared_key"])
|
||||
}
|
||||
Spacer(modifier = Modifier.height(10.dp))
|
||||
Button(onClick = {
|
||||
dialog.dismiss()
|
||||
warnKeyOverwrite(friendId) {
|
||||
askForKeys(conversationId)
|
||||
}
|
||||
}) {
|
||||
Text(translation["toolbox.initiate_exchange_button"])
|
||||
}
|
||||
}) {
|
||||
Text(translation["toolbox.initiate_exchange_button"])
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -47,16 +47,31 @@ fun View.addForegroundDrawable(tag: String, drawable: Drawable) {
|
||||
updateForegroundDrawable()
|
||||
}
|
||||
|
||||
fun View.triggerCloseTouchEvent() {
|
||||
arrayOf(MotionEvent.ACTION_DOWN, MotionEvent.ACTION_UP).forEach {
|
||||
this.dispatchTouchEvent(
|
||||
MotionEvent.obtain(
|
||||
SystemClock.uptimeMillis(),
|
||||
SystemClock.uptimeMillis(),
|
||||
it, 0f, 0f, 0
|
||||
)
|
||||
)
|
||||
}
|
||||
fun View.dispatchSyntheticTap(x: Float, y: Float, tapDurationMs: Long = 50L) {
|
||||
val downTime = SystemClock.uptimeMillis()
|
||||
val downEvent = MotionEvent.obtain(downTime, downTime, MotionEvent.ACTION_DOWN, x, y, 0)
|
||||
dispatchTouchEvent(downEvent)
|
||||
downEvent.recycle()
|
||||
|
||||
val upEvent = MotionEvent.obtain(downTime, downTime + tapDurationMs, MotionEvent.ACTION_UP, x, y, 0)
|
||||
dispatchTouchEvent(upEvent)
|
||||
upEvent.recycle()
|
||||
}
|
||||
|
||||
fun View.triggerCloseTouchEvent(x: Float = 0f, y: Float = 0f, tapDurationMs: Long = 50L) {
|
||||
dispatchSyntheticTap(x, y, tapDurationMs)
|
||||
}
|
||||
|
||||
fun View.triggerCloseTouchEventAtFraction(
|
||||
xFraction: Float,
|
||||
yFraction: Float = 0.5f,
|
||||
tapDurationMs: Long = 50L
|
||||
) {
|
||||
val targetWidth = width.takeIf { it > 0 } ?: measuredWidth
|
||||
val targetHeight = height.takeIf { it > 0 } ?: measuredHeight
|
||||
val x = if (targetWidth > 0) targetWidth * xFraction.coerceIn(0f, 1f) else 0f
|
||||
val y = if (targetHeight > 0) targetHeight * yFraction.coerceIn(0f, 1f) else 0f
|
||||
triggerCloseTouchEvent(x, y, tapDurationMs)
|
||||
}
|
||||
|
||||
fun Activity.triggerRootCloseTouchEvent() {
|
||||
|
||||
@@ -45,6 +45,7 @@ import me.eternal.purrfectsnap.core.ui.iterateParent
|
||||
import me.eternal.purrfectsnap.core.ui.menu.AbstractMenu
|
||||
import me.eternal.purrfectsnap.core.ui.randomTag
|
||||
import me.eternal.purrfectsnap.core.ui.triggerCloseTouchEvent
|
||||
import me.eternal.purrfectsnap.core.ui.triggerCloseTouchEventAtFraction
|
||||
import me.eternal.purrfectsnap.core.util.SNAPCHAT_13_80_VERSION
|
||||
import me.eternal.purrfectsnap.core.util.hook.HookStage
|
||||
import me.eternal.purrfectsnap.core.util.hook.hook
|
||||
@@ -259,23 +260,113 @@ class OperaViewerIcons : AbstractMenu() {
|
||||
visibleRect.width() > 0
|
||||
}
|
||||
|
||||
private fun hasVisibleOpenLayout(view: View): Boolean {
|
||||
private fun findVisibleOpenLayout(view: View): View? {
|
||||
if (view.javaClass.hasNameSuffixInHierarchy("OpenLayout") && isActuallyVisible(view)) {
|
||||
return true
|
||||
return view
|
||||
}
|
||||
|
||||
val viewGroup = view as? ViewGroup ?: return false
|
||||
val viewGroup = view as? ViewGroup ?: return null
|
||||
for (index in 0 until viewGroup.childCount) {
|
||||
if (hasVisibleOpenLayout(viewGroup.getChildAt(index))) {
|
||||
return true
|
||||
}
|
||||
findVisibleOpenLayout(viewGroup.getChildAt(index))?.let { return it }
|
||||
}
|
||||
return false
|
||||
return null
|
||||
}
|
||||
|
||||
private fun findVisibleModernViewerContainer(): View? {
|
||||
val contentView = context.mainActivity?.findViewById<ViewGroup>(android.R.id.content) ?: return null
|
||||
return findVisibleOpenLayout(contentView)
|
||||
}
|
||||
|
||||
private fun hasVisibleModernViewerContainer(): Boolean {
|
||||
val contentView = context.mainActivity?.findViewById<ViewGroup>(android.R.id.content) ?: return false
|
||||
return hasVisibleOpenLayout(contentView)
|
||||
return findVisibleModernViewerContainer() != null
|
||||
}
|
||||
|
||||
private fun currentViewerMessageContext(mediaDownloader: MediaDownloader): OperaViewerMessageContext? {
|
||||
return mediaDownloader.resolveViewerMessageContextFromParamMap()?.also {
|
||||
viewerMessageContextState.value = it
|
||||
} ?: viewerMessageContextState.value
|
||||
}
|
||||
|
||||
private fun hasViewerAdvanced(
|
||||
mediaDownloader: MediaDownloader,
|
||||
originalMessageContext: OperaViewerMessageContext
|
||||
): Boolean {
|
||||
if (!hasVisibleModernViewerContainer()) {
|
||||
return true
|
||||
}
|
||||
|
||||
return currentViewerMessageContext(mediaDownloader)?.let { it != originalMessageContext } == true
|
||||
}
|
||||
|
||||
private suspend fun waitForViewerAdvance(
|
||||
mediaDownloader: MediaDownloader,
|
||||
originalMessageContext: OperaViewerMessageContext,
|
||||
timeoutMs: Long
|
||||
): Boolean {
|
||||
var elapsedMs = 0L
|
||||
while (elapsedMs < timeoutMs) {
|
||||
delay(40)
|
||||
elapsedMs += 40
|
||||
if (hasViewerAdvanced(mediaDownloader, originalMessageContext)) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return hasViewerAdvanced(mediaDownloader, originalMessageContext)
|
||||
}
|
||||
|
||||
private fun dispatchLegacySkipGesture(parent: ViewGroup?) {
|
||||
if (parent != null) {
|
||||
var touchedParent = false
|
||||
parent.iterateParent {
|
||||
touchedParent = true
|
||||
it.triggerCloseTouchEvent()
|
||||
false
|
||||
}
|
||||
if (touchedParent) return
|
||||
}
|
||||
|
||||
context.mainActivity
|
||||
?.findViewById<View>(android.R.id.content)
|
||||
?.triggerCloseTouchEvent()
|
||||
}
|
||||
|
||||
private fun dispatchForwardHotZoneTap(target: View?, xFraction: Float) {
|
||||
target?.triggerCloseTouchEventAtFraction(xFraction = xFraction, yFraction = 0.5f)
|
||||
}
|
||||
|
||||
private suspend fun skipMarkedSnap(
|
||||
parent: ViewGroup?,
|
||||
mediaDownloader: MediaDownloader,
|
||||
originalMessageContext: OperaViewerMessageContext
|
||||
) {
|
||||
val contentView = context.mainActivity?.findViewById<ViewGroup>(android.R.id.content)
|
||||
val skipAttempts = listOf<suspend () -> Unit>(
|
||||
{
|
||||
dispatchLegacySkipGesture(parent)
|
||||
},
|
||||
{
|
||||
dispatchForwardHotZoneTap(findVisibleModernViewerContainer() ?: contentView, 0.88f)
|
||||
},
|
||||
{
|
||||
dispatchForwardHotZoneTap(contentView ?: findVisibleModernViewerContainer(), 0.88f)
|
||||
},
|
||||
{
|
||||
val target = findVisibleModernViewerContainer() ?: contentView
|
||||
dispatchForwardHotZoneTap(target, 0.88f)
|
||||
delay(55)
|
||||
if (!hasViewerAdvanced(mediaDownloader, originalMessageContext)) {
|
||||
dispatchForwardHotZoneTap(target, 0.94f)
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
for ((index, attempt) in skipAttempts.withIndex()) {
|
||||
attempt()
|
||||
if (waitForViewerAdvance(mediaDownloader, originalMessageContext, if (index == 0) 120L else 180L)) {
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun Class<*>?.hasNameSuffixInHierarchy(suffix: String): Boolean {
|
||||
@@ -326,7 +417,8 @@ class OperaViewerIcons : AbstractMenu() {
|
||||
}
|
||||
|
||||
private suspend fun markCurrentSnapAsSeen(parent: ViewGroup?) {
|
||||
val messageContext = resolveCurrentMessageContext(context.feature(MediaDownloader::class)) ?: return
|
||||
val mediaDownloader = context.feature(MediaDownloader::class)
|
||||
val messageContext = resolveCurrentMessageContext(mediaDownloader) ?: return
|
||||
val result = context.feature(AutoMarkAsRead::class).markSnapAsSeen(
|
||||
messageContext.conversationId,
|
||||
messageContext.clientMessageId
|
||||
@@ -335,16 +427,7 @@ class OperaViewerIcons : AbstractMenu() {
|
||||
if (result == "DUPLICATEREQUEST" || result == null) {
|
||||
if (context.config.messaging.skipWhenMarkingAsSeen.get()) {
|
||||
withContext(Dispatchers.Main) {
|
||||
if (parent != null) {
|
||||
parent.iterateParent {
|
||||
it.triggerCloseTouchEvent()
|
||||
false
|
||||
}
|
||||
} else {
|
||||
context.mainActivity
|
||||
?.findViewById<View>(android.R.id.content)
|
||||
?.triggerCloseTouchEvent()
|
||||
}
|
||||
skipMarkedSnap(parent, mediaDownloader, messageContext)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,8 +7,8 @@ org.gradle.configuration-cache=true
|
||||
org.gradle.configuration-cache.problems=warn
|
||||
nativeAbis=arm64-v8a
|
||||
|
||||
APP_VERSION_NAME=1.4.9
|
||||
APP_VERSION_CODE=290
|
||||
APP_VERSION_NAME=1.5.0
|
||||
APP_VERSION_CODE=292
|
||||
debug_build_hash=18fe2a814d0e2eb5
|
||||
psIntegrityPinnedSha256=
|
||||
EXPECTED_CERT_SHA256=0f188cb17d8ea4902ee15ce98f4928ba4226a4790fa3c52f62d776b08e46ca1c
|
||||
|
||||
Reference in New Issue
Block a user