fix
This commit is contained in:
@@ -3,11 +3,14 @@ package me.eternal.purrfectsnap.core.action.impl
|
||||
import android.content.Context
|
||||
import android.graphics.Bitmap
|
||||
import android.graphics.BitmapFactory
|
||||
import android.graphics.Color as AndroidColor
|
||||
import android.graphics.drawable.GradientDrawable
|
||||
import android.view.Gravity
|
||||
import android.view.View
|
||||
import android.widget.LinearLayout
|
||||
import android.widget.ProgressBar
|
||||
import android.widget.TextView
|
||||
import android.content.res.ColorStateList
|
||||
import androidx.compose.foundation.BorderStroke
|
||||
import androidx.compose.foundation.ExperimentalFoundationApi
|
||||
import androidx.compose.foundation.Image
|
||||
@@ -145,8 +148,11 @@ class BulkMessagingAction : AbstractAction() {
|
||||
action: suspend (id: String, setDialogMessage: (String) -> Unit) -> Unit = { _, _ -> }
|
||||
) = context.coroutineScope.launch {
|
||||
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) {
|
||||
ViewAppearanceHelper.newAlertDialogBuilder(ctx)
|
||||
val d = ViewAppearanceHelper.newAlertDialogBuilder(ctx)
|
||||
.setTitle("...")
|
||||
.setView(LinearLayout(ctx).apply {
|
||||
val padding = (16 * ctx.resources.displayMetrics.density).toInt()
|
||||
@@ -160,12 +166,28 @@ class BulkMessagingAction : AbstractAction() {
|
||||
setSingleLine(false)
|
||||
setPadding(0, 0, 0, spacing)
|
||||
})
|
||||
addView(ProgressBar(ctx).apply {
|
||||
layoutParams = LinearLayout.LayoutParams(LinearLayout.LayoutParams.WRAP_CONTENT, LinearLayout.LayoutParams.WRAP_CONTENT)
|
||||
})
|
||||
addView(progressBar)
|
||||
})
|
||||
.setCancelable(false)
|
||||
.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 ->
|
||||
@@ -350,12 +372,19 @@ class BulkMessagingAction : AbstractAction() {
|
||||
val myLocation = betterLocation.locationHistory[context.database.myUserId]
|
||||
|
||||
withContext(Dispatchers.IO) {
|
||||
val friendIdsStillInFeed = runCatching {
|
||||
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() }
|
||||
// Sync with Snapchat feed: only show friends whose DM conversation still exists in feed
|
||||
// (FriendsFeedView excludes cleared via "clearedTimestamp < lastInteractionTimestamp")
|
||||
// BUT: only apply this filtering to certain filters that specifically need it
|
||||
val friendIdsStillInFeed = if (filter in setOf(Filter.MY_FRIENDS)) {
|
||||
runCatching {
|
||||
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) {
|
||||
runCatching { context.database.getIncomingRequestUserIds() }.getOrElse { emptySet() }
|
||||
@@ -368,10 +397,14 @@ class BulkMessagingAction : AbstractAction() {
|
||||
.filter { it.userId?.let { id -> !hiddenFriendIds.contains(id) } == true }
|
||||
.filter { friend ->
|
||||
when {
|
||||
// Only show incoming/follower requests that exist in FriendWhoAddedMe (real pending requests)
|
||||
filter == Filter.INCOMING || filter == Filter.INCOMING_FOLLOWER ->
|
||||
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
|
||||
// All other filters: don't restrict by feed presence
|
||||
else -> true
|
||||
}
|
||||
}
|
||||
.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 -> {}
|
||||
}
|
||||
}
|
||||
@@ -368,6 +380,12 @@ class SendOverride : Feature("Send Override") {
|
||||
|
||||
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 {
|
||||
val bypassLimit = context.config.experimental.nativeHooks.valdiHooks.bypassCameraRollLimit.get()
|
||||
if (overrideType != "ORIGINAL" && !bypassLimit && (messageProtoReader.followPath(3)?.getCount(3) ?: 0) > 1) {
|
||||
@@ -412,7 +430,7 @@ class SendOverride : Feature("Send Override") {
|
||||
edit(11, 5, 2) {
|
||||
arrayOf(6, 7, 8).forEach { remove(it) }
|
||||
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)
|
||||
if (snapDurationMs / 1000 <= 0) {
|
||||
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) {
|
||||
remove(4)
|
||||
addVarInt(4, 5)
|
||||
}
|
||||
|
||||
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)
|
||||
addVarInt(4, 5) // APP_SOURCE_CAMERA
|
||||
}
|
||||
}.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" -> {
|
||||
// Check if "prevent audio" is enabled in UnsaveableMessages
|
||||
@@ -477,9 +454,15 @@ class SendOverride : Feature("Send Override") {
|
||||
postSavePolicy = 1 // PROHIBITED
|
||||
}
|
||||
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(
|
||||
messageProtoReader.getVarInt(3, 3, 5, 1, 1, 15) ?: context.feature(MediaFilePicker::class).lastMediaDuration ?: 0,
|
||||
Locale.getDefault().toLanguageTag()
|
||||
durationForProto,
|
||||
if (omitTranscript) null else Locale.getDefault().toLanguageTag()
|
||||
)
|
||||
|
||||
// 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" }
|
||||
if (resolvedOverrideType != null) {
|
||||
if (sendMedia(resolvedOverrideType, 10)) {
|
||||
event.invokeOriginal()
|
||||
if (sendMedia(resolvedOverrideType, 10000)) {
|
||||
invokeOriginalAndRestoreResult(event)
|
||||
}
|
||||
return@subscribe
|
||||
}
|
||||
@@ -953,7 +936,7 @@ class SendOverride : Feature("Send Override") {
|
||||
context.bridgeClient.getTaskInterface().updateTaskProgress(taskHash, "Sending...", 100)
|
||||
|
||||
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"
|
||||
context.inAppOverlay.showStatusToast(
|
||||
icon = Icons.Filled.CheckCircle,
|
||||
@@ -1001,7 +984,7 @@ class SendOverride : Feature("Send Override") {
|
||||
}
|
||||
} else {
|
||||
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,
|
||||
) {
|
||||
companion object {
|
||||
const val VOICE_NOTE_MAX_DURATION_MS = 100_000L
|
||||
|
||||
val audioNoteProto: (Long, String?) -> ByteArray = { duration, userLocale ->
|
||||
ProtoWriter().apply {
|
||||
from(6, 1) {
|
||||
|
||||
@@ -568,6 +568,12 @@ class InAppOverlay(
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
val progress = remember { Animatable(1f) }
|
||||
val progressGradient = Brush.horizontalGradient(
|
||||
listOf(
|
||||
Color(0xFF6F28A8).copy(alpha = 0.7f),
|
||||
Color(0xFF0059B7).copy(alpha = 0.7f)
|
||||
)
|
||||
)
|
||||
|
||||
LaunchedEffect(Unit) {
|
||||
progress.animateTo(
|
||||
@@ -576,10 +582,19 @@ class InAppOverlay(
|
||||
)
|
||||
}
|
||||
|
||||
LinearProgressIndicator(
|
||||
progress = { progress.value },
|
||||
Box(
|
||||
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(
|
||||
@@ -688,7 +703,7 @@ class InAppOverlay(
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.align(Alignment.TopCenter)
|
||||
.padding(top = 16.dp)
|
||||
.padding(top = 12.dp)
|
||||
.graphicsLayer {
|
||||
translationX = offsetX
|
||||
translationY = offsetY
|
||||
@@ -697,23 +712,33 @@ class InAppOverlay(
|
||||
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(
|
||||
modifier = Modifier
|
||||
.background(
|
||||
color = backgroundColor,
|
||||
shape = MaterialTheme.shapes.large
|
||||
brush = bypassGradient,
|
||||
shape = MaterialTheme.shapes.medium
|
||||
)
|
||||
.padding(horizontal = 20.dp, vertical = 12.dp),
|
||||
.padding(horizontal = 12.dp, vertical = 6.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.spacedBy(10.dp)
|
||||
horizontalArrangement = Arrangement.spacedBy(6.dp)
|
||||
) {
|
||||
Icon(
|
||||
imageVector = if (isWorking) Icons.Filled.Check else Icons.Filled.Close,
|
||||
contentDescription = null,
|
||||
tint = Color.White,
|
||||
modifier = Modifier.size(20.dp)
|
||||
modifier = Modifier.size(14.dp)
|
||||
)
|
||||
|
||||
Text(
|
||||
@@ -722,8 +747,8 @@ class InAppOverlay(
|
||||
else
|
||||
context.translation["manager.sections.bypass_status.inactive"],
|
||||
color = Color.White,
|
||||
fontSize = 15.sp,
|
||||
fontWeight = FontWeight.Bold
|
||||
fontSize = 12.sp,
|
||||
fontWeight = FontWeight.SemiBold
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user