fix
This commit is contained in:
@@ -3,11 +3,14 @@ package me.eternal.purrfectsnap.core.action.impl
|
|||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.graphics.Bitmap
|
import android.graphics.Bitmap
|
||||||
import android.graphics.BitmapFactory
|
import android.graphics.BitmapFactory
|
||||||
|
import android.graphics.Color as AndroidColor
|
||||||
|
import android.graphics.drawable.GradientDrawable
|
||||||
import android.view.Gravity
|
import android.view.Gravity
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.widget.LinearLayout
|
import android.widget.LinearLayout
|
||||||
import android.widget.ProgressBar
|
import android.widget.ProgressBar
|
||||||
import android.widget.TextView
|
import android.widget.TextView
|
||||||
|
import android.content.res.ColorStateList
|
||||||
import androidx.compose.foundation.BorderStroke
|
import androidx.compose.foundation.BorderStroke
|
||||||
import androidx.compose.foundation.ExperimentalFoundationApi
|
import androidx.compose.foundation.ExperimentalFoundationApi
|
||||||
import androidx.compose.foundation.Image
|
import androidx.compose.foundation.Image
|
||||||
@@ -145,8 +148,11 @@ class BulkMessagingAction : AbstractAction() {
|
|||||||
action: suspend (id: String, setDialogMessage: (String) -> Unit) -> Unit = { _, _ -> }
|
action: suspend (id: String, setDialogMessage: (String) -> Unit) -> Unit = { _, _ -> }
|
||||||
) = context.coroutineScope.launch {
|
) = context.coroutineScope.launch {
|
||||||
val statusTextView = TextView(ctx)
|
val statusTextView = TextView(ctx)
|
||||||
|
val progressBar = ProgressBar(ctx).apply {
|
||||||
|
layoutParams = LinearLayout.LayoutParams(LinearLayout.LayoutParams.WRAP_CONTENT, LinearLayout.LayoutParams.WRAP_CONTENT)
|
||||||
|
}
|
||||||
val dialog = withContext(Dispatchers.Main) {
|
val dialog = withContext(Dispatchers.Main) {
|
||||||
ViewAppearanceHelper.newAlertDialogBuilder(ctx)
|
val d = ViewAppearanceHelper.newAlertDialogBuilder(ctx)
|
||||||
.setTitle("...")
|
.setTitle("...")
|
||||||
.setView(LinearLayout(ctx).apply {
|
.setView(LinearLayout(ctx).apply {
|
||||||
val padding = (16 * ctx.resources.displayMetrics.density).toInt()
|
val padding = (16 * ctx.resources.displayMetrics.density).toInt()
|
||||||
@@ -160,12 +166,28 @@ class BulkMessagingAction : AbstractAction() {
|
|||||||
setSingleLine(false)
|
setSingleLine(false)
|
||||||
setPadding(0, 0, 0, spacing)
|
setPadding(0, 0, 0, spacing)
|
||||||
})
|
})
|
||||||
addView(ProgressBar(ctx).apply {
|
addView(progressBar)
|
||||||
layoutParams = LinearLayout.LayoutParams(LinearLayout.LayoutParams.WRAP_CONTENT, LinearLayout.LayoutParams.WRAP_CONTENT)
|
|
||||||
})
|
|
||||||
})
|
})
|
||||||
.setCancelable(false)
|
.setCancelable(false)
|
||||||
.show()
|
.show()
|
||||||
|
// Style dialog to match app UI (gradient, app colors)
|
||||||
|
val density = ctx.resources.displayMetrics.density
|
||||||
|
d.window?.setBackgroundDrawable(GradientDrawable(
|
||||||
|
GradientDrawable.Orientation.TOP_BOTTOM,
|
||||||
|
intArrayOf(
|
||||||
|
AndroidColor.parseColor("#2A2452"),
|
||||||
|
AndroidColor.parseColor("#1A143A")
|
||||||
|
)
|
||||||
|
).apply {
|
||||||
|
cornerRadius = (20 * density).toFloat()
|
||||||
|
})
|
||||||
|
val titleId = ctx.resources.getIdentifier("alertTitle", "id", "android")
|
||||||
|
if (titleId != 0) {
|
||||||
|
(d.window?.decorView?.findViewById<View>(titleId) as? TextView)?.setTextColor(AndroidColor.WHITE)
|
||||||
|
}
|
||||||
|
statusTextView.setTextColor(AndroidColor.parseColor("#E0E0E0"))
|
||||||
|
progressBar.indeterminateTintList = ColorStateList.valueOf(AndroidColor.parseColor("#8C7BFF"))
|
||||||
|
d
|
||||||
}
|
}
|
||||||
|
|
||||||
ids.forEachIndexed { index, id ->
|
ids.forEachIndexed { index, id ->
|
||||||
@@ -350,12 +372,19 @@ class BulkMessagingAction : AbstractAction() {
|
|||||||
val myLocation = betterLocation.locationHistory[context.database.myUserId]
|
val myLocation = betterLocation.locationHistory[context.database.myUserId]
|
||||||
|
|
||||||
withContext(Dispatchers.IO) {
|
withContext(Dispatchers.IO) {
|
||||||
val friendIdsStillInFeed = runCatching {
|
// Sync with Snapchat feed: only show friends whose DM conversation still exists in feed
|
||||||
context.database.getFeedEntries(Int.MAX_VALUE)
|
// (FriendsFeedView excludes cleared via "clearedTimestamp < lastInteractionTimestamp")
|
||||||
.filter { it.conversationType == 0 && it.participantsSize == 2 }
|
// BUT: only apply this filtering to certain filters that specifically need it
|
||||||
.mapNotNull { it.participants?.firstOrNull { id -> id != context.database.myUserId } }
|
val friendIdsStillInFeed = if (filter in setOf(Filter.MY_FRIENDS)) {
|
||||||
.toSet()
|
runCatching {
|
||||||
}.getOrElse { emptySet() }
|
context.database.getFeedEntries(Int.MAX_VALUE)
|
||||||
|
.filter { it.conversationType == 0 && it.participantsSize == 2 }
|
||||||
|
.mapNotNull { it.participants?.firstOrNull { id -> id != context.database.myUserId } }
|
||||||
|
.toSet()
|
||||||
|
}.getOrElse { emptySet() }
|
||||||
|
} else {
|
||||||
|
emptySet()
|
||||||
|
}
|
||||||
|
|
||||||
val incomingRequestUserIds = if (filter == Filter.INCOMING || filter == Filter.INCOMING_FOLLOWER) {
|
val incomingRequestUserIds = if (filter == Filter.INCOMING || filter == Filter.INCOMING_FOLLOWER) {
|
||||||
runCatching { context.database.getIncomingRequestUserIds() }.getOrElse { emptySet() }
|
runCatching { context.database.getIncomingRequestUserIds() }.getOrElse { emptySet() }
|
||||||
@@ -368,10 +397,14 @@ class BulkMessagingAction : AbstractAction() {
|
|||||||
.filter { it.userId?.let { id -> !hiddenFriendIds.contains(id) } == true }
|
.filter { it.userId?.let { id -> !hiddenFriendIds.contains(id) } == true }
|
||||||
.filter { friend ->
|
.filter { friend ->
|
||||||
when {
|
when {
|
||||||
|
// Only show incoming/follower requests that exist in FriendWhoAddedMe (real pending requests)
|
||||||
filter == Filter.INCOMING || filter == Filter.INCOMING_FOLLOWER ->
|
filter == Filter.INCOMING || filter == Filter.INCOMING_FOLLOWER ->
|
||||||
friend.userId != null && friend.userId in incomingRequestUserIds
|
friend.userId != null && friend.userId in incomingRequestUserIds
|
||||||
else ->
|
// Only apply feed filtering to MY_FRIENDS filter
|
||||||
|
filter == Filter.MY_FRIENDS ->
|
||||||
friendIdsStillInFeed.isEmpty() || friend.userId in friendIdsStillInFeed
|
friendIdsStillInFeed.isEmpty() || friend.userId in friendIdsStillInFeed
|
||||||
|
// All other filters: don't restrict by feed presence
|
||||||
|
else -> true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.toMutableList()
|
.toMutableList()
|
||||||
|
|||||||
@@ -257,6 +257,18 @@ class SendOverride : Feature("Send Override") {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
ContentType.NOTE -> {
|
||||||
|
if (stripMediaMetadata.contains("remove_audio_note_duration")) {
|
||||||
|
edit(6, 1, 1) {
|
||||||
|
remove(13)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (stripMediaMetadata.contains("remove_audio_note_transcript_capability")) {
|
||||||
|
edit(6, 1) {
|
||||||
|
remove(3)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
else -> {}
|
else -> {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -368,6 +380,12 @@ class SendOverride : Feature("Send Override") {
|
|||||||
|
|
||||||
event.canceled = true
|
event.canceled = true
|
||||||
|
|
||||||
|
fun invokeOriginalAndRestoreResult(ev: SendMessageWithContentEvent) {
|
||||||
|
val result = ev.adapter.invokeOriginal()
|
||||||
|
ev.adapter.setResult(result)
|
||||||
|
ev.canceled = false
|
||||||
|
}
|
||||||
|
|
||||||
fun sendMedia(overrideType: String, snapDurationMs: Int?): Boolean {
|
fun sendMedia(overrideType: String, snapDurationMs: Int?): Boolean {
|
||||||
val bypassLimit = context.config.experimental.nativeHooks.valdiHooks.bypassCameraRollLimit.get()
|
val bypassLimit = context.config.experimental.nativeHooks.valdiHooks.bypassCameraRollLimit.get()
|
||||||
if (overrideType != "ORIGINAL" && !bypassLimit && (messageProtoReader.followPath(3)?.getCount(3) ?: 0) > 1) {
|
if (overrideType != "ORIGINAL" && !bypassLimit && (messageProtoReader.followPath(3)?.getCount(3) ?: 0) > 1) {
|
||||||
@@ -412,7 +430,7 @@ class SendOverride : Feature("Send Override") {
|
|||||||
edit(11, 5, 2) {
|
edit(11, 5, 2) {
|
||||||
arrayOf(6, 7, 8).forEach { remove(it) }
|
arrayOf(6, 7, 8).forEach { remove(it) }
|
||||||
addVarInt(5, messageProtoReader.getVarInt(3, 3, 5, 2, 5) ?: messageProtoReader.getVarInt(11, 5, 2, 5) ?: 1)
|
addVarInt(5, messageProtoReader.getVarInt(3, 3, 5, 2, 5) ?: messageProtoReader.getVarInt(11, 5, 2, 5) ?: 1)
|
||||||
if (snapDurationMs != null) {
|
if (snapDurationMs != null && overrideType != "SAVEABLE_SNAP") {
|
||||||
addVarInt(8, snapDurationMs / 1000)
|
addVarInt(8, snapDurationMs / 1000)
|
||||||
if (snapDurationMs / 1000 <= 0) {
|
if (snapDurationMs / 1000 <= 0) {
|
||||||
addVarInt(99, snapDurationMs)
|
addVarInt(99, snapDurationMs)
|
||||||
@@ -422,53 +440,12 @@ class SendOverride : Feature("Send Override") {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// set app source (same as SnapEnhance - no save policy in proto for story+chat)
|
||||||
edit(11, 22) {
|
edit(11, 22) {
|
||||||
remove(4)
|
remove(4)
|
||||||
addVarInt(4, 5)
|
addVarInt(4, 5) // APP_SOURCE_CAMERA
|
||||||
}
|
|
||||||
|
|
||||||
edit(11, 5) {
|
|
||||||
if (getOrNull(7) != null) {
|
|
||||||
remove(7)
|
|
||||||
}
|
|
||||||
addVarInt(7, savePolicyValue)
|
|
||||||
}
|
|
||||||
// also set at root of snap doc
|
|
||||||
edit(11) {
|
|
||||||
if (getOrNull(7) != null) {
|
|
||||||
remove(7)
|
|
||||||
}
|
|
||||||
addVarInt(7, savePolicyValue)
|
|
||||||
}
|
}
|
||||||
}.toByteArray()
|
}.toByteArray()
|
||||||
|
|
||||||
try {
|
|
||||||
val savePolicyEnumClass = runCatching {
|
|
||||||
XposedHelpers.findClass("com.snapchat.client.messaging.SavePolicy",
|
|
||||||
localMessageContent.instanceNonNull().javaClass.classLoader)
|
|
||||||
}.getOrNull()
|
|
||||||
|
|
||||||
if (savePolicyEnumClass != null && savePolicyEnumClass.isEnum) {
|
|
||||||
@Suppress("UNCHECKED_CAST")
|
|
||||||
val enumClass = savePolicyEnumClass as Class<out Enum<*>>
|
|
||||||
val enumName = if (overrideType == "SAVEABLE_SNAP") "LIFETIME" else "PROHIBITED"
|
|
||||||
val targetEnum = runCatching {
|
|
||||||
java.lang.Enum.valueOf(enumClass, enumName)
|
|
||||||
}.getOrNull()
|
|
||||||
|
|
||||||
if (targetEnum != null) {
|
|
||||||
val savePolicyField = localMessageContent.instanceNonNull().javaClass.declaredFields
|
|
||||||
.find { it.name == "mSavePolicy" }
|
|
||||||
|
|
||||||
if (savePolicyField != null) {
|
|
||||||
savePolicyField.isAccessible = true
|
|
||||||
XposedHelpers.setObjectField(localMessageContent.instanceNonNull(), "mSavePolicy", targetEnum)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (e: Exception) {
|
|
||||||
context.log.warn("SendOverride: Failed to set mSavePolicy: ${e.message}")
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
"NOTE" -> {
|
"NOTE" -> {
|
||||||
// Check if "prevent audio" is enabled in UnsaveableMessages
|
// Check if "prevent audio" is enabled in UnsaveableMessages
|
||||||
@@ -477,9 +454,15 @@ class SendOverride : Feature("Send Override") {
|
|||||||
postSavePolicy = 1 // PROHIBITED
|
postSavePolicy = 1 // PROHIBITED
|
||||||
}
|
}
|
||||||
localMessageContent.contentType = ContentType.NOTE
|
localMessageContent.contentType = ContentType.NOTE
|
||||||
|
val stripMeta = context.config.messaging.stripMediaMetadata.get()
|
||||||
|
val omitTranscript = stripMeta.contains("remove_audio_note_transcript_capability")
|
||||||
|
val rawDurationMs = messageProtoReader.getVarInt(3, 3, 5, 1, 1, 15)?.toLong()
|
||||||
|
?: messageProtoReader.getVarInt(3, 3, 5, 2, 8)?.toLong()?.times(1000)
|
||||||
|
?: (context.feature(MediaFilePicker::class).lastMediaDuration ?: 0).toLong()
|
||||||
|
val durationForProto = minOf(rawDurationMs, MessageSender.VOICE_NOTE_MAX_DURATION_MS)
|
||||||
val audioNoteProto = MessageSender.audioNoteProto(
|
val audioNoteProto = MessageSender.audioNoteProto(
|
||||||
messageProtoReader.getVarInt(3, 3, 5, 1, 1, 15) ?: context.feature(MediaFilePicker::class).lastMediaDuration ?: 0,
|
durationForProto,
|
||||||
Locale.getDefault().toLanguageTag()
|
if (omitTranscript) null else Locale.getDefault().toLanguageTag()
|
||||||
)
|
)
|
||||||
|
|
||||||
// Set save policy in the proto if prevent audio is enabled
|
// Set save policy in the proto if prevent audio is enabled
|
||||||
@@ -543,8 +526,8 @@ class SendOverride : Feature("Send Override") {
|
|||||||
|
|
||||||
val resolvedOverrideType = configOverrideType?.takeIf { it != "always_ask" }
|
val resolvedOverrideType = configOverrideType?.takeIf { it != "always_ask" }
|
||||||
if (resolvedOverrideType != null) {
|
if (resolvedOverrideType != null) {
|
||||||
if (sendMedia(resolvedOverrideType, 10)) {
|
if (sendMedia(resolvedOverrideType, 10000)) {
|
||||||
event.invokeOriginal()
|
invokeOriginalAndRestoreResult(event)
|
||||||
}
|
}
|
||||||
return@subscribe
|
return@subscribe
|
||||||
}
|
}
|
||||||
@@ -953,7 +936,7 @@ class SendOverride : Feature("Send Override") {
|
|||||||
context.bridgeClient.getTaskInterface().updateTaskProgress(taskHash, "Sending...", 100)
|
context.bridgeClient.getTaskInterface().updateTaskProgress(taskHash, "Sending...", 100)
|
||||||
|
|
||||||
if (sendMedia(finalSelectedType, if (finalSelectedType != "SAVEABLE_SNAP") convertDuration(customDuration) else null)) {
|
if (sendMedia(finalSelectedType, if (finalSelectedType != "SAVEABLE_SNAP") convertDuration(customDuration) else null)) {
|
||||||
event.invokeOriginal()
|
invokeOriginalAndRestoreResult(event)
|
||||||
val successText = context.translation.format("schedule_sent_to", "name" to recipientNameForTask) ?: "Sent to $recipientNameForTask"
|
val successText = context.translation.format("schedule_sent_to", "name" to recipientNameForTask) ?: "Sent to $recipientNameForTask"
|
||||||
context.inAppOverlay.showStatusToast(
|
context.inAppOverlay.showStatusToast(
|
||||||
icon = Icons.Filled.CheckCircle,
|
icon = Icons.Filled.CheckCircle,
|
||||||
@@ -1001,7 +984,7 @@ class SendOverride : Feature("Send Override") {
|
|||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
if (sendMedia(finalSelectedType, if (finalSelectedType != "SAVEABLE_SNAP") convertDuration(customDuration) else null)) {
|
if (sendMedia(finalSelectedType, if (finalSelectedType != "SAVEABLE_SNAP") convertDuration(customDuration) else null)) {
|
||||||
event.invokeOriginal()
|
invokeOriginalAndRestoreResult(event)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}) {
|
}) {
|
||||||
|
|||||||
@@ -14,6 +14,8 @@ class MessageSender(
|
|||||||
private val context: ModContext,
|
private val context: ModContext,
|
||||||
) {
|
) {
|
||||||
companion object {
|
companion object {
|
||||||
|
const val VOICE_NOTE_MAX_DURATION_MS = 100_000L
|
||||||
|
|
||||||
val audioNoteProto: (Long, String?) -> ByteArray = { duration, userLocale ->
|
val audioNoteProto: (Long, String?) -> ByteArray = { duration, userLocale ->
|
||||||
ProtoWriter().apply {
|
ProtoWriter().apply {
|
||||||
from(6, 1) {
|
from(6, 1) {
|
||||||
|
|||||||
@@ -568,6 +568,12 @@ class InAppOverlay(
|
|||||||
modifier: Modifier = Modifier
|
modifier: Modifier = Modifier
|
||||||
) {
|
) {
|
||||||
val progress = remember { Animatable(1f) }
|
val progress = remember { Animatable(1f) }
|
||||||
|
val progressGradient = Brush.horizontalGradient(
|
||||||
|
listOf(
|
||||||
|
Color(0xFF6F28A8).copy(alpha = 0.7f),
|
||||||
|
Color(0xFF0059B7).copy(alpha = 0.7f)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
LaunchedEffect(Unit) {
|
LaunchedEffect(Unit) {
|
||||||
progress.animateTo(
|
progress.animateTo(
|
||||||
@@ -576,10 +582,19 @@ class InAppOverlay(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
LinearProgressIndicator(
|
Box(
|
||||||
progress = { progress.value },
|
|
||||||
modifier = modifier
|
modifier = modifier
|
||||||
)
|
.height(3.dp)
|
||||||
|
.clip(RoundedCornerShape(2.dp))
|
||||||
|
.background(Color(0xFF6F28A8).copy(alpha = 0.25f))
|
||||||
|
) {
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth(progress.value)
|
||||||
|
.fillMaxHeight()
|
||||||
|
.background(brush = progressGradient, shape = RoundedCornerShape(2.dp))
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun showStatusToast(
|
fun showStatusToast(
|
||||||
@@ -688,7 +703,7 @@ class InAppOverlay(
|
|||||||
Box(
|
Box(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.align(Alignment.TopCenter)
|
.align(Alignment.TopCenter)
|
||||||
.padding(top = 16.dp)
|
.padding(top = 12.dp)
|
||||||
.graphicsLayer {
|
.graphicsLayer {
|
||||||
translationX = offsetX
|
translationX = offsetX
|
||||||
translationY = offsetY
|
translationY = offsetY
|
||||||
@@ -697,23 +712,33 @@ class InAppOverlay(
|
|||||||
alpha = progress
|
alpha = progress
|
||||||
}
|
}
|
||||||
) {
|
) {
|
||||||
val backgroundColor = if (isWorking) Color(0xFF1B5E20).copy(alpha = 0.8f) else Color(0xFFB71C1C).copy(alpha = 0.8f)
|
val bypassGradient = if (isWorking) Brush.horizontalGradient(
|
||||||
|
listOf(
|
||||||
|
Color(0xFF6F28A8).copy(alpha = 0.7f),
|
||||||
|
Color(0xFF0059B7).copy(alpha = 0.7f)
|
||||||
|
)
|
||||||
|
) else Brush.horizontalGradient(
|
||||||
|
listOf(
|
||||||
|
Color(0xFF8B1538).copy(alpha = 0.7f),
|
||||||
|
Color(0xFFB71C1C).copy(alpha = 0.7f)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
Row(
|
Row(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.background(
|
.background(
|
||||||
color = backgroundColor,
|
brush = bypassGradient,
|
||||||
shape = MaterialTheme.shapes.large
|
shape = MaterialTheme.shapes.medium
|
||||||
)
|
)
|
||||||
.padding(horizontal = 20.dp, vertical = 12.dp),
|
.padding(horizontal = 12.dp, vertical = 6.dp),
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
horizontalArrangement = Arrangement.spacedBy(10.dp)
|
horizontalArrangement = Arrangement.spacedBy(6.dp)
|
||||||
) {
|
) {
|
||||||
Icon(
|
Icon(
|
||||||
imageVector = if (isWorking) Icons.Filled.Check else Icons.Filled.Close,
|
imageVector = if (isWorking) Icons.Filled.Check else Icons.Filled.Close,
|
||||||
contentDescription = null,
|
contentDescription = null,
|
||||||
tint = Color.White,
|
tint = Color.White,
|
||||||
modifier = Modifier.size(20.dp)
|
modifier = Modifier.size(14.dp)
|
||||||
)
|
)
|
||||||
|
|
||||||
Text(
|
Text(
|
||||||
@@ -722,8 +747,8 @@ class InAppOverlay(
|
|||||||
else
|
else
|
||||||
context.translation["manager.sections.bypass_status.inactive"],
|
context.translation["manager.sections.bypass_status.inactive"],
|
||||||
color = Color.White,
|
color = Color.White,
|
||||||
fontSize = 15.sp,
|
fontSize = 12.sp,
|
||||||
fontWeight = FontWeight.Bold
|
fontWeight = FontWeight.SemiBold
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user