This commit is contained in:
ΞTΞRNAL
2026-03-25 21:59:31 +05:30
parent b7c8042a93
commit dbbc52b67f
12 changed files with 331 additions and 35 deletions

View File

@@ -53,8 +53,6 @@ import androidx.compose.ui.text.input.TextFieldValue
import androidx.compose.ui.unit.DpOffset import androidx.compose.ui.unit.DpOffset
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp import androidx.compose.ui.unit.sp
import androidx.compose.ui.window.Dialog
import androidx.compose.ui.window.DialogProperties
import androidx.compose.ui.zIndex import androidx.compose.ui.zIndex
import androidx.core.net.toUri import androidx.core.net.toUri
import androidx.lifecycle.Lifecycle import androidx.lifecycle.Lifecycle
@@ -78,6 +76,8 @@ import me.eternal.purrfectsnap.ui.manager.Routes
import me.eternal.purrfectsnap.ui.manager.ManagerTheme import me.eternal.purrfectsnap.ui.manager.ManagerTheme
import me.eternal.purrfectsnap.ui.manager.theme.PurrfectPalette import me.eternal.purrfectsnap.ui.manager.theme.PurrfectPalette
import me.eternal.purrfectsnap.ui.util.* import me.eternal.purrfectsnap.ui.util.*
import me.eternal.purrfectsnap.ui.util.Dialog
import me.eternal.purrfectsnap.ui.util.DialogProperties
import org.json.JSONArray import org.json.JSONArray
import org.json.JSONObject import org.json.JSONObject
import kotlin.math.max import kotlin.math.max

View File

@@ -40,7 +40,6 @@ import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp import androidx.compose.ui.unit.sp
import me.eternal.purrfectsnap.common.config.ConfigFlag import me.eternal.purrfectsnap.common.config.ConfigFlag
import androidx.compose.ui.viewinterop.AndroidView import androidx.compose.ui.viewinterop.AndroidView
import androidx.compose.ui.window.Dialog as StandardDialog
import androidx.core.net.toUri import androidx.core.net.toUri
import com.github.skydoves.colorpicker.compose.* import com.github.skydoves.colorpicker.compose.*
import com.google.gson.JsonParser import com.google.gson.JsonParser
@@ -67,6 +66,7 @@ import org.osmdroid.views.overlay.Marker
import org.osmdroid.views.overlay.Overlay import org.osmdroid.views.overlay.Overlay
import java.io.File import java.io.File
import me.eternal.purrfectsnap.ui.util.purrfectSwitchColors import me.eternal.purrfectsnap.ui.util.purrfectSwitchColors
import me.eternal.purrfectsnap.ui.util.Dialog as StandardDialog
class AlertDialogs( class AlertDialogs(
@@ -1093,7 +1093,7 @@ class AlertDialogs(
val lat = remember { mutableStateOf(coordinates.first.toString()) } val lat = remember { mutableStateOf(coordinates.first.toString()) }
val lon = remember { mutableStateOf(coordinates.second.toString()) } val lon = remember { mutableStateOf(coordinates.second.toString()) }
Dialog( StandardDialog(
onDismissRequest = { onDismissRequest = {
customCoordinatesDialog = false customCoordinatesDialog = false
}, },
@@ -1398,7 +1398,7 @@ class AlertDialogs(
// Add/Edit message dialog // Add/Edit message dialog
if (showAddDialog) { if (showAddDialog) {
Dialog( StandardDialog(
onDismissRequest = { showAddDialog = false }, onDismissRequest = { showAddDialog = false },
properties = DialogProperties( properties = DialogProperties(
usePlatformDefaultWidth = false usePlatformDefaultWidth = false

View File

@@ -4,12 +4,28 @@ package me.eternal.purrfectsnap.ui.util
import android.annotation.SuppressLint import android.annotation.SuppressLint
import android.app.Activity import android.app.Activity
import android.content.Context import android.content.Context
import android.content.ContextWrapper
import android.graphics.Outline import android.graphics.Outline
import android.os.Build import android.os.Build
import android.provider.Settings import android.provider.Settings
import android.view.* import android.view.*
import android.view.View.OnAttachStateChangeListener
import androidx.activity.ComponentDialog import androidx.activity.ComponentDialog
import androidx.activity.addCallback import androidx.activity.addCallback
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.fadeIn
import androidx.compose.animation.fadeOut
import androidx.compose.animation.scaleIn
import androidx.compose.animation.scaleOut
import androidx.compose.animation.core.spring
import androidx.compose.animation.core.tween
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.width
import androidx.compose.runtime.* import androidx.compose.runtime.*
import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.ui.ExperimentalComposeUiApi import androidx.compose.ui.ExperimentalComposeUiApi
@@ -19,9 +35,12 @@ import androidx.compose.ui.layout.Layout
import androidx.compose.ui.platform.* import androidx.compose.ui.platform.*
import androidx.compose.ui.semantics.dialog import androidx.compose.ui.semantics.dialog
import androidx.compose.ui.semantics.semantics import androidx.compose.ui.semantics.semantics
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.unit.Density import androidx.compose.ui.unit.Density
import androidx.compose.ui.unit.LayoutDirection import androidx.compose.ui.unit.LayoutDirection
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.window.Popup
import androidx.compose.ui.window.PopupProperties
import androidx.compose.ui.window.SecureFlagPolicy import androidx.compose.ui.window.SecureFlagPolicy
import androidx.core.view.WindowCompat import androidx.core.view.WindowCompat
import androidx.lifecycle.findViewTreeLifecycleOwner import androidx.lifecycle.findViewTreeLifecycleOwner
@@ -33,6 +52,12 @@ import androidx.savedstate.setViewTreeSavedStateRegistryOwner
import java.util.UUID import java.util.UUID
import kotlin.math.roundToInt import kotlin.math.roundToInt
private tailrec fun Context.findActivity(): Activity? = when (this) {
is Activity -> this
is ContextWrapper -> baseContext?.findActivity()
else -> null
}
class DialogProperties constructor( class DialogProperties constructor(
val dismissOnBackPress: Boolean = true, val dismissOnBackPress: Boolean = true,
val dismissOnClickOutside: Boolean = true, val dismissOnClickOutside: Boolean = true,
@@ -83,6 +108,17 @@ fun Dialog(
content: @Composable () -> Unit content: @Composable () -> Unit
) { ) {
val view = LocalView.current val view = LocalView.current
var forceInline by remember(view) { mutableStateOf(false) }
val hostActivity = remember(view) { view.context.findActivity() }
val shouldUseInline = forceInline || hostActivity == null || hostActivity.isFinishing || hostActivity.isDestroyed
if (shouldUseInline) {
InlineDialog(
onDismissRequest = onDismissRequest,
dismissOnClickOutside = properties.dismissOnClickOutside,
content = content
)
return
}
val density = LocalDensity.current val density = LocalDensity.current
val layoutDirection = LocalLayoutDirection.current val layoutDirection = LocalLayoutDirection.current
val composition = rememberCompositionContext() val composition = rememberCompositionContext()
@@ -110,11 +146,30 @@ fun Dialog(
} }
DisposableEffect(dialog) { DisposableEffect(dialog) {
// Set the dialog's window type to TYPE_APPLICATION_OVERLAY so it's compatible with compose overlays val showDialog = {
if (Settings.canDrawOverlays(view.context) && view.context !is Activity) { try {
dialog.window?.setType(WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY) dialog.prepareWindowForHost()
if (!dialog.isShowing) {
dialog.show()
}
} catch (_: WindowManager.BadTokenException) {
forceInline = true
}
}
if (view.isAttachedToWindow || view.windowToken != null || view.rootView?.windowToken != null) {
showDialog()
} else {
val listener = object : OnAttachStateChangeListener {
override fun onViewAttachedToWindow(v: View) {
v.removeOnAttachStateChangeListener(this)
showDialog()
}
override fun onViewDetachedFromWindow(v: View) = Unit
}
view.addOnAttachStateChangeListener(listener)
} }
dialog.show()
onDispose { onDispose {
dialog.dismiss() dialog.dismiss()
@@ -131,6 +186,75 @@ fun Dialog(
} }
} }
@Composable
private fun InlineDialog(
onDismissRequest: () -> Unit,
dismissOnClickOutside: Boolean,
content: @Composable () -> Unit
) {
val density = LocalDensity.current
val displayMetrics = LocalContext.current.resources.displayMetrics
val screenWidthDp = with(density) { displayMetrics.widthPixels.toDp() }
val screenHeightDp = with(density) { displayMetrics.heightPixels.toDp() }
val interactionSource = remember { MutableInteractionSource() }
var visible by remember { mutableStateOf(false) }
LaunchedEffect(Unit) {
visible = true
}
Popup(
alignment = androidx.compose.ui.Alignment.Center,
properties = PopupProperties(
focusable = true,
dismissOnBackPress = true,
dismissOnClickOutside = dismissOnClickOutside
),
onDismissRequest = onDismissRequest
) {
Box(
modifier = Modifier
.width(screenWidthDp)
.height(screenHeightDp)
.then(
if (dismissOnClickOutside) {
Modifier.clickable(
interactionSource = interactionSource,
indication = null,
onClick = onDismissRequest
)
} else {
Modifier
}
)
.semantics { dialog() },
contentAlignment = androidx.compose.ui.Alignment.Center
) {
AnimatedVisibility(
visible = visible,
enter = fadeIn(animationSpec = tween(180)) + scaleIn(
initialScale = 0.92f,
animationSpec = spring(dampingRatio = 0.82f, stiffness = 520f)
),
exit = fadeOut(animationSpec = tween(120)) + scaleOut(
targetScale = 0.96f,
animationSpec = tween(120)
)
) {
Box(
modifier = Modifier.clickable(
interactionSource = interactionSource,
indication = null,
onClick = {}
)
) {
content()
}
}
}
}
}
interface DialogWindowProvider { interface DialogWindowProvider {
val window: Window val window: Window
} }
@@ -279,6 +403,26 @@ private class DialogWrapper(
} }
} }
fun prepareWindowForHost() {
val hostActivity = composeView.context.findActivity()
val hostToken = composeView.applicationWindowToken
?: composeView.windowToken
?: composeView.rootView?.applicationWindowToken
?: composeView.rootView?.windowToken
if (hostActivity == null) {
when {
hostToken != null -> window?.setType(WindowManager.LayoutParams.TYPE_APPLICATION_ATTACHED_DIALOG)
Settings.canDrawOverlays(composeView.context) -> window?.setType(WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY)
}
}
if (hostToken != null) {
window?.attributes = window?.attributes?.apply {
token = hostToken
}
}
hostActivity?.let { setOwnerActivity(it) }
}
private fun setLayoutDirection(layoutDirection: LayoutDirection) { private fun setLayoutDirection(layoutDirection: LayoutDirection) {
dialogLayout.layoutDirection = when (layoutDirection) { dialogLayout.layoutDirection = when (layoutDirection) {
LayoutDirection.Ltr -> android.util.LayoutDirection.LTR LayoutDirection.Ltr -> android.util.LayoutDirection.LTR

View File

@@ -33,8 +33,8 @@ tasks.register<GetVersionTask>("getVersion") {
} }
// You can still set these for legacy use by submodules or scripts: // You can still set these for legacy use by submodules or scripts:
rootProject.ext.set("appVersionName", providers.gradleProperty("APP_VERSION_NAME").orElse("1.5.2").get()) rootProject.ext.set("appVersionName", providers.gradleProperty("APP_VERSION_NAME").orElse("1.5.3").get())
rootProject.ext.set("appVersionCode", providers.gradleProperty("APP_VERSION_CODE").orElse("296").get().toInt()) rootProject.ext.set("appVersionCode", providers.gradleProperty("APP_VERSION_CODE").orElse("298").get().toInt())
rootProject.ext.set("applicationId", "me.eternal.purrfectsnap") rootProject.ext.set("applicationId", "me.eternal.purrfectsnap")
// buildHash: when PurrfectSnap or Snapchat is updated, mappings become outdated and auto-regenerate. // 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. // Include version code so each release has a different hash; use random for uniqueness within same version.

View File

@@ -1,3 +1,9 @@
## v1.5.3
- New: Mark as Seen Mode(Limit per run[Custom] or Complete Queue)
- Fix: PurrfectSnap crash if you try to open any dialog setting through in-app overlay
- New: Translucent Dialogs & refreshed animation
- Fix: Adjusted Snap Preview Location
## v1.5.2 ## v1.5.2
- New: Continuous snap sender feature! - New: Continuous snap sender feature!
- Fix: Snap send failure for E2E Chats - Fix: Snap send failure for E2E Chats

View File

@@ -1190,6 +1190,14 @@
"name": "Mark Snap as Seen Button", "name": "Mark Snap as Seen Button",
"description": "Adds a button to mark a Snap as seen when viewing it.\nThis will work even when Stealth Mode is enabled" "description": "Adds a button to mark a Snap as seen when viewing it.\nThis will work even when Stealth Mode is enabled"
}, },
"mark_snap_as_seen_processing_mode": {
"name": "Mark Snaps as Seen Mode",
"description": "Choose whether to process a limited number of snaps per run or the entire queue at once"
},
"mark_snap_as_seen_limit": {
"name": "Mark Snaps as Seen Limit",
"description": "How many snaps to process per run when the mode is set to limit"
},
"skip_when_marking_as_seen": { "skip_when_marking_as_seen": {
"name": "Skip When Marking as Seen", "name": "Skip When Marking as Seen",
"description": "Automatically skips to the next Snap when marking a Snap as seen.\nUse in combination with Mark Snap as Seen Button" "description": "Automatically skips to the next Snap when marking a Snap as seen.\nUse in combination with Mark Snap as Seen Button"
@@ -3639,4 +3647,4 @@
"openai": "OpenAI", "openai": "OpenAI",
"openrouter": "OpenRouter" "openrouter": "OpenRouter"
} }
} }

View File

@@ -1254,6 +1254,14 @@
"name": "Mark Snap as Seen Button", "name": "Mark Snap as Seen Button",
"description": "Adds a button to mark a Snap as seen when viewing it.\nThis will work even when Stealth Mode is enabled" "description": "Adds a button to mark a Snap as seen when viewing it.\nThis will work even when Stealth Mode is enabled"
}, },
"mark_snap_as_seen_processing_mode": {
"name": "Mark Snaps as Seen Mode",
"description": "Choose whether to process a limited number of snaps per run or the entire queue at once"
},
"mark_snap_as_seen_limit": {
"name": "Mark Snaps as Seen Limit",
"description": "How many snaps to process per run when the mode is set to limit"
},
"skip_when_marking_as_seen": { "skip_when_marking_as_seen": {
"name": "Skip When Marking as Seen", "name": "Skip When Marking as Seen",
"description": "Automatically skips to the next Snap when marking a Snap as seen.\nUse in combination with Mark Snap as Seen Button" "description": "Automatically skips to the next Snap when marking a Snap as seen.\nUse in combination with Mark Snap as Seen Button"
@@ -2514,6 +2522,10 @@
"remove_audio_note_duration": "Remove Audio Note Duration", "remove_audio_note_duration": "Remove Audio Note Duration",
"remove_audio_note_transcript_capability": "Remove Audio Note Transcript Capability" "remove_audio_note_transcript_capability": "Remove Audio Note Transcript Capability"
}, },
"mark_snap_as_seen_processing_mode": {
"limit": "Limit Per Run",
"complete": "Complete Queue"
},
"hide_ui_components": { "hide_ui_components": {
"hide_profile_call_buttons": "Remove Profile Call Buttons", "hide_profile_call_buttons": "Remove Profile Call Buttons",
"hide_chat_call_buttons": "Remove Chat Call Buttons", "hide_chat_call_buttons": "Remove Chat Call Buttons",
@@ -3437,10 +3449,10 @@
"search": { "search": {
"placeholder": "Search" "placeholder": "Search"
}, },
"filters": { "filters": {
"newest_first": "Newest first", "newest_first": "Newest first",
"pick_a_date": "Pick a date", "pick_a_date": "Pick a date",
"title": "Filters", "title": "Filters",
"search_by": "Search by", "search_by": "Search by",
"since": "Since", "since": "Since",
"until": "Until", "until": "Until",
@@ -3455,7 +3467,7 @@
"started_typing": "Started typing", "started_typing": "Started typing",
"stopped_typing": "Stopped typing", "stopped_typing": "Stopped typing",
"started_speaking": "Started speaking", "started_speaking": "Started speaking",
"stopped_speaking": "Stopped speaking", "stopped_speaking": "Stopped speaking",
"started_peeking": "Started peeking", "started_peeking": "Started peeking",
"stopped_peeking": "Stopped peeking", "stopped_peeking": "Stopped peeking",
"message_read": "Read message", "message_read": "Read message",

View File

@@ -205,6 +205,12 @@ class MessagingTweaks : ConfigContainer() {
val unlimitedSnapViewTime = boolean("unlimited_snap_view_time") val unlimitedSnapViewTime = boolean("unlimited_snap_view_time")
val autoMarkAsRead = multiple("auto_mark_as_read", "snap_reply", "conversation_read", "save_snap_in_chat") { requireRestart() } val autoMarkAsRead = multiple("auto_mark_as_read", "snap_reply", "conversation_read", "save_snap_in_chat") { requireRestart() }
val markSnapAsSeenButton = boolean("mark_snap_as_seen_button") { requireRestart() } val markSnapAsSeenButton = boolean("mark_snap_as_seen_button") { requireRestart() }
val markSnapAsSeenProcessingMode = unique("mark_snap_as_seen_processing_mode", "limit", "complete").apply {
set("limit")
}
val markSnapAsSeenLimit = integer("mark_snap_as_seen_limit", defaultValue = 50) {
inputCheck = { it.toIntOrNull()?.coerceAtLeast(1) != null }
}
val skipWhenMarkingAsSeen = boolean("skip_when_marking_as_seen") { requireRestart() } val skipWhenMarkingAsSeen = boolean("skip_when_marking_as_seen") { requireRestart() }
val loopMediaPlayback = boolean("loop_media_playback") { requireRestart() } val loopMediaPlayback = boolean("loop_media_playback") { requireRestart() }
val disableReplayInFF = boolean("disable_replay_in_ff") val disableReplayInFF = boolean("disable_replay_in_ff")

View File

@@ -8,6 +8,7 @@ import androidx.compose.foundation.layout.height
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Visibility import androidx.compose.material.icons.filled.Visibility
import androidx.compose.material.icons.filled.WarningAmber import androidx.compose.material.icons.filled.WarningAmber
import androidx.compose.material.icons.filled.Info
import androidx.compose.material3.CircularProgressIndicator import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.runtime.mutableIntStateOf import androidx.compose.runtime.mutableIntStateOf
@@ -39,6 +40,54 @@ import kotlin.random.Random
class AutoMarkAsRead : Feature("Auto Mark As Read") { class AutoMarkAsRead : Feature("Auto Mark As Read") {
val canMarkConversationAsRead by lazy { context.config.messaging.autoMarkAsRead.get().contains("conversation_read") } val canMarkConversationAsRead by lazy { context.config.messaging.autoMarkAsRead.get().contains("conversation_read") }
private val markAsSeenBatchSize = 50
private val markAsSeenBatchCooldownMs = 4000L
private data class PendingSnapMessage(
val clientMessageId: Long,
val creationTimestamp: Long
)
private fun String?.isRateLimited(): Boolean {
val value = this ?: return false
return value.contains("RESOURCE_EXHAUSTED", ignoreCase = true) ||
value.contains("Rate limited", ignoreCase = true)
}
private fun showRateLimitedDialog(processed: Int, total: Int) {
val activity = context.mainActivity ?: run {
context.inAppOverlay.showStatusToast(
Icons.Default.WarningAmber,
"Rate limited after $processed/$total snaps. Try again later."
)
return
}
createComposeAlertDialog(activity) {
PurrfectOverlayTheme {
PurrfectGlassCard(
modifier = androidx.compose.ui.Modifier.fillMaxWidth(),
title = "Rate Limited",
subtitle = "Snapchat stopped the mark-as-seen run to protect your account",
icon = Icons.Default.WarningAmber
) {
Column(
modifier = androidx.compose.ui.Modifier.fillMaxWidth(),
verticalArrangement = Arrangement.spacedBy(12.dp)
) {
Text(
text = "Processed $processed of $total snaps before the request was rate limited.",
color = PurrfectOverlayPalette.textSecondary
)
Text(
text = "No bypass was attempted. Wait a bit and run it again, or lower the per-run limit in settings.",
color = PurrfectOverlayPalette.textSecondary
)
}
}
}
}.show()
}
fun markConversationsAsRead(conversationIds: List<String>) { fun markConversationsAsRead(conversationIds: List<String>) {
conversationIds.forEach { conversationId -> conversationIds.forEach { conversationId ->
@@ -63,18 +112,66 @@ class AutoMarkAsRead : Feature("Auto Mark As Read") {
} }
} }
private fun getPendingSnapMessageIds(conversationId: String, requestedLimit: Int?): List<Long> {
val collected = mutableListOf<PendingSnapMessage>()
val pageSize = 200
var page = 0
while (true) {
val messages = context.database.getMessagesFromConversationId(conversationId, pageSize, page) ?: break
messages.forEach { message ->
if (message.contentType != ContentType.SNAP.id && message.contentType != ContentType.EXTERNAL_MEDIA.id) return@forEach
if (message.isViewedByUser == 1 || message.readTimestamp > 0L) return@forEach
collected += PendingSnapMessage(
clientMessageId = message.clientMessageId.toLong(),
creationTimestamp = message.creationTimestamp
)
}
if (messages.size < pageSize) break
if (requestedLimit != null && collected.size >= requestedLimit) break
page++
}
return collected
.distinctBy { it.clientMessageId }
.sortedBy { it.creationTimestamp }
.let { pending ->
if (requestedLimit != null) pending.take(requestedLimit) else pending
}
.map { it.clientMessageId }
}
fun markSnapsAsSeen(conversationId: String) { fun markSnapsAsSeen(conversationId: String) {
val messaging = context.feature(Messaging::class) val messaging = context.feature(Messaging::class)
val messageIds = messaging.getFeedCachedMessageIds(conversationId)?.takeIf { it.isNotEmpty() } ?: run { val processingMode = context.config.messaging.markSnapAsSeenProcessingMode.get()
context.inAppOverlay.showStatusToast( val configuredLimit = context.config.messaging.markSnapAsSeenLimit.get()
Icons.Default.WarningAmber, .coerceAtLeast(1)
context.translation["mark_as_seen.no_unseen_snaps_toast"] val requestedLimit = if (processingMode == "complete") null else configuredLimit
) val messageIds = getPendingSnapMessageIds(conversationId, requestedLimit)
return .ifEmpty {
messaging.getFeedCachedMessageIds(conversationId)
?.map { it.toLong() }
?.takeIf { it.isNotEmpty() }
?.let { cached ->
if (requestedLimit != null) cached.take(requestedLimit) else cached
}
?: run {
context.inAppOverlay.showStatusToast(
Icons.Default.WarningAmber,
context.translation["mark_as_seen.no_unseen_snaps_toast"]
)
return
}
}
val targetMessageIds = if (requestedLimit == null) {
messageIds
} else {
messageIds.take(requestedLimit)
} }
var job: Job? = null var job: Job? = null
val processedCount = mutableIntStateOf(0) val processedCount = mutableIntStateOf(0)
var rateLimitedAt: Int? = null
val dialog = createComposeAlertDialog(context.mainActivity!!, builder = { val dialog = createComposeAlertDialog(context.mainActivity!!, builder = {
setOnDismissListener { job?.cancel() } setOnDismissListener { job?.cancel() }
}) { }) {
@@ -95,7 +192,7 @@ class AutoMarkAsRead : Feature("Auto Mark As Read") {
trackColor = PurrfectOverlayPalette.glowPrimary.copy(alpha = 0.18f) trackColor = PurrfectOverlayPalette.glowPrimary.copy(alpha = 0.18f)
) )
Text( Text(
text = "${processedCount.intValue}/${messageIds.size}", text = "${processedCount.intValue}/${targetMessageIds.size}",
color = PurrfectOverlayPalette.textSecondary color = PurrfectOverlayPalette.textSecondary
) )
} }
@@ -104,16 +201,34 @@ class AutoMarkAsRead : Feature("Auto Mark As Read") {
}.apply { show() } }.apply { show() }
context.coroutineScope.launch(Dispatchers.IO) { context.coroutineScope.launch(Dispatchers.IO) {
messageIds.forEachIndexed { index, messageId -> targetMessageIds.forEachIndexed { index, messageId ->
markSnapAsSeen(conversationId, messageId) val result = markSnapAsSeen(conversationId, messageId)
if (result.isRateLimited()) {
rateLimitedAt = processedCount.intValue
return@launch
}
delay(Random.nextLong(20, 60)) delay(Random.nextLong(20, 60))
context.runOnUiThread { context.runOnUiThread {
processedCount.intValue = index + 1 processedCount.intValue = index + 1
} }
val processed = index + 1
if (processed < targetMessageIds.size && processed % markAsSeenBatchSize == 0) {
delay(markAsSeenBatchCooldownMs)
}
} }
}.also { job = it }.invokeOnCompletion { }.also { job = it }.invokeOnCompletion {
context.runOnUiThread { context.runOnUiThread {
dialog.dismiss() dialog.dismiss()
if (rateLimitedAt != null) {
val processedIndex = rateLimitedAt!!
processedCount.intValue = processedIndex
showRateLimitedDialog(processedIndex, targetMessageIds.size)
} else if (requestedLimit != null && targetMessageIds.size < messageIds.size) {
context.inAppOverlay.showStatusToast(
Icons.Default.Info,
"Processed ${targetMessageIds.size} of ${messageIds.size} unseen snaps."
)
}
} }
} }
} }

View File

@@ -137,4 +137,4 @@ class MessageIndicators : Feature("Message Indicators") {
} }
} }
} }
} }

View File

@@ -28,6 +28,8 @@ class SnapPreview : Feature("SnapPreview") {
private val bitmapCache = EvictingMap<String, Bitmap>(50) // filePath => bitmap private val bitmapCache = EvictingMap<String, Bitmap>(50) // filePath => bitmap
private val fetchJobTab = randomTag() private val fetchJobTab = randomTag()
private val previewHorizontalAdjustmentDp = 16
private val previewVerticalAdjustmentDp = 10
override fun init() { override fun init() {
if (!context.config.userInterface.snapPreview.get()) return if (!context.config.userInterface.snapPreview.get()) return
@@ -47,9 +49,11 @@ class SnapPreview : Feature("SnapPreview") {
} }
onNextActivityCreate { onNextActivityCreate {
val (chatMediaCardHeight, chatMediaCardSnapMargin, chatMediaCardSnapMarginStartSdl) = context.userInterface.run { val chatMediaCardHeight = context.userInterface.dpToPx(60)
Triple(dpToPx(60), dpToPx(10), dpToPx(15)) val chatMediaCardSnapMargin = context.userInterface.dpToPx(10)
} val chatMediaCardSnapMarginStartSdl = context.userInterface.dpToPx(15)
val previewHorizontalAdjustment = context.userInterface.dpToPx(previewHorizontalAdjustmentDp)
val previewVerticalAdjustment = context.userInterface.dpToPx(previewVerticalAdjustmentDp)
fun decodeMedia(file: File) = runCatching { fun decodeMedia(file: File) = runCatching {
bitmapCache.getOrPut(file.absolutePath) { bitmapCache.getOrPut(file.absolutePath) {
@@ -91,8 +95,8 @@ class SnapPreview : Feature("SnapPreview") {
val bitmap = bitmapCache[mediaFilePath] ?: return val bitmap = bitmapCache[mediaFilePath] ?: return
canvas.drawBitmap(bitmap, canvas.drawBitmap(bitmap,
canvas.width.toFloat() - bitmap.width - chatMediaCardSnapMarginStartSdl.toFloat() - chatMediaCardSnapMargin.toFloat(), canvas.width.toFloat() - bitmap.width - chatMediaCardSnapMarginStartSdl.toFloat() - chatMediaCardSnapMargin.toFloat() + previewHorizontalAdjustment,
(canvas.height - bitmap.height) / 2f, (canvas.height - bitmap.height) / 2f + previewVerticalAdjustment,
null null
) )
} }
@@ -101,4 +105,4 @@ class SnapPreview : Feature("SnapPreview") {
} }
} }
} }
} }

View File

@@ -7,10 +7,11 @@ org.gradle.configuration-cache=true
org.gradle.configuration-cache.problems=warn org.gradle.configuration-cache.problems=warn
nativeAbis=arm64-v8a nativeAbis=arm64-v8a
APP_VERSION_NAME=1.5.2 APP_VERSION_NAME=1.5.3
APP_VERSION_CODE=296 APP_VERSION_CODE=298
debug_build_hash=18fe2a814d0e2eb5 debug_build_hash=18fe2a814d0e2eb5
psIntegrityPinnedSha256= psIntegrityPinnedSha256=
EXPECTED_CERT_SHA256=0f188cb17d8ea4902ee15ce98f4928ba4226a4790fa3c52f62d776b08e46ca1c EXPECTED_CERT_SHA256=0f188cb17d8ea4902ee15ce98f4928ba4226a4790fa3c52f62d776b08e46ca1c
android.disallowKotlinSourceSets=false android.disallowKotlinSourceSets=false
android.sourceset.disallowProvider=false android.sourceset.disallowProvider=false
ksp.incremental=false