From e3008e0c9ced3417ec4c849fa96371c77b899ea4 Mon Sep 17 00:00:00 2001 From: RSR/ <52901789+RSR1337@users.noreply.github.com> Date: Sat, 7 Feb 2026 19:20:39 +0400 Subject: [PATCH] fix --- .../core/action/impl/BulkMessagingAction.kt | 55 +++++++++--- .../features/impl/messaging/SendOverride.kt | 83 ++++++++----------- .../core/messaging/MessageSender.kt | 2 + .../purrfectsnap/core/ui/InAppOverlay.kt | 49 ++++++++--- 4 files changed, 116 insertions(+), 73 deletions(-) diff --git a/core/src/main/kotlin/me/eternal/purrfectsnap/core/action/impl/BulkMessagingAction.kt b/core/src/main/kotlin/me/eternal/purrfectsnap/core/action/impl/BulkMessagingAction.kt index 5e2dc74e..7b65f92b 100644 --- a/core/src/main/kotlin/me/eternal/purrfectsnap/core/action/impl/BulkMessagingAction.kt +++ b/core/src/main/kotlin/me/eternal/purrfectsnap/core/action/impl/BulkMessagingAction.kt @@ -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(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() diff --git a/core/src/main/kotlin/me/eternal/purrfectsnap/core/features/impl/messaging/SendOverride.kt b/core/src/main/kotlin/me/eternal/purrfectsnap/core/features/impl/messaging/SendOverride.kt index ff1fb0db..d76a19c1 100644 --- a/core/src/main/kotlin/me/eternal/purrfectsnap/core/features/impl/messaging/SendOverride.kt +++ b/core/src/main/kotlin/me/eternal/purrfectsnap/core/features/impl/messaging/SendOverride.kt @@ -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> - 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) } } }) { diff --git a/core/src/main/kotlin/me/eternal/purrfectsnap/core/messaging/MessageSender.kt b/core/src/main/kotlin/me/eternal/purrfectsnap/core/messaging/MessageSender.kt index fe30a946..f03fbbe1 100644 --- a/core/src/main/kotlin/me/eternal/purrfectsnap/core/messaging/MessageSender.kt +++ b/core/src/main/kotlin/me/eternal/purrfectsnap/core/messaging/MessageSender.kt @@ -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) { diff --git a/core/src/main/kotlin/me/eternal/purrfectsnap/core/ui/InAppOverlay.kt b/core/src/main/kotlin/me/eternal/purrfectsnap/core/ui/InAppOverlay.kt index df09cff0..c731276c 100644 --- a/core/src/main/kotlin/me/eternal/purrfectsnap/core/ui/InAppOverlay.kt +++ b/core/src/main/kotlin/me/eternal/purrfectsnap/core/ui/InAppOverlay.kt @@ -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 ) } }