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 1/2] 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 ) } } From 1ac1542384f2d597acd84fc3560d4e0c375ea265 Mon Sep 17 00:00:00 2001 From: RSR/ <52901789+RSR1337@users.noreply.github.com> Date: Mon, 16 Feb 2026 20:31:42 +0400 Subject: [PATCH 2/2] fix MediaFilePicker on snap13.79 and added mapper --- .../impl/experiments/MediaFilePicker.kt | 76 ++++++++++++------- gradle.properties | 3 +- .../purrfectsnap/mapper/ClassMapper.kt | 1 + .../mapper/impl/ChatMediaDrawerMapper.kt | 48 ++++++++++++ 4 files changed, 101 insertions(+), 27 deletions(-) create mode 100644 mapper/src/main/kotlin/me/eternal/purrfectsnap/mapper/impl/ChatMediaDrawerMapper.kt diff --git a/core/src/main/kotlin/me/eternal/purrfectsnap/core/features/impl/experiments/MediaFilePicker.kt b/core/src/main/kotlin/me/eternal/purrfectsnap/core/features/impl/experiments/MediaFilePicker.kt index ba6fed6c..0168f483 100644 --- a/core/src/main/kotlin/me/eternal/purrfectsnap/core/features/impl/experiments/MediaFilePicker.kt +++ b/core/src/main/kotlin/me/eternal/purrfectsnap/core/features/impl/experiments/MediaFilePicker.kt @@ -48,6 +48,7 @@ import me.eternal.purrfectsnap.core.ui.ViewAppearanceHelper import me.eternal.purrfectsnap.core.util.dataBuilder import me.eternal.purrfectsnap.core.util.hook.HookStage import me.eternal.purrfectsnap.core.util.hook.hook +import me.eternal.purrfectsnap.mapper.impl.ChatMediaDrawerMapper import java.io.InputStream import java.lang.reflect.Method import kotlin.random.Random @@ -62,14 +63,27 @@ class MediaFilePicker : Feature("Media File Picker") { onNextActivityCreate(defer = true) { lateinit var chatMediaDrawerActionHandler: Any - lateinit var sendItemsMethod: Method + var sendItemsMethod: Method? = null + var drawerViewClass: Class<*>? = null + var sendItemsListItemClassFallback: Class<*>? = null - findClass("com.snap.composer.memories.ChatMediaDrawer").genericSuperclass?.getTypeArguments()?.getOrNull(1)?.apply { - methods.first { - it.parameterTypes.size == 1 && it.parameterTypes[0].name.endsWith("ChatMediaDrawerActionHandler") - }.also { method -> - sendItemsMethod = method.parameterTypes[0].methods.first { it.name == "sendItems" } - }.hook(HookStage.AFTER) { + context.mappings.useMapper(ChatMediaDrawerMapper::class) { + val drawerCls = chatMediaDrawerClass.getAsClass() ?: return@useMapper + val actionHandlerCls = actionHandlerClass.getAsClass() ?: return@useMapper + val sendItemsName = sendItemsMethodName.getAsString() ?: "sendItems" + drawerViewClass = drawerCls + sendItemsListItemClassFallback = sendItemsListItemClass.getAsClass() + + val contextType = drawerCls.genericSuperclass?.getTypeArguments()?.getOrNull(1) ?: return@useMapper + val handlerParamMethod = contextType.methods.firstOrNull { method -> + method.parameterTypes.size == 1 && ( + method.parameterTypes[0].name.endsWith("ChatMediaDrawerActionHandler") || + actionHandlerCls.isAssignableFrom(method.parameterTypes[0]) + ) + } ?: return@useMapper + val sendItems = handlerParamMethod.parameterTypes[0].methods.firstOrNull { it.name == sendItemsName } ?: return@useMapper + sendItemsMethod = sendItems + handlerParamMethod.hook(HookStage.AFTER) { chatMediaDrawerActionHandler = it.arg(0) } } @@ -102,7 +116,7 @@ class MediaFilePicker : Feature("Media File Picker") { } context.event.subscribe(ActivityResultEvent::class) { event -> - if (event.requestCode != requestCode || event.resultCode != Activity.RESULT_OK) return@subscribe + if (sendItemsMethod == null || event.requestCode != requestCode || event.resultCode != Activity.RESULT_OK) return@subscribe requestCode = null firstVideoId = context.androidContext.contentResolver.query( @@ -128,25 +142,35 @@ class MediaFilePicker : Feature("Media File Picker") { } fun sendMedia() { - sendItemsMethod.invoke(chatMediaDrawerActionHandler, listOf(), listOf( - sendItemsMethod.genericParameterTypes[1].getTypeArguments().first().dataBuilder { - from("_item") { - set("_cameraRollSource", "Snapchat") - set("_contentUri", "") - set("_durationMs", 0.0) - set("_disabled", false) - set("_imageRotation", 0.0) - set("_width", 1080.0) - set("_height", 1920.0) - set("_timestampMs", System.currentTimeMillis().toDouble()) - from("_itemId") { - set("_itemId", firstVideoId.toString()) - set("_type", "VIDEO") - } + val method = sendItemsMethod ?: return + val itemClass = method.genericParameterTypes.getOrNull(1)?.getTypeArguments()?.firstOrNull() + ?: sendItemsListItemClassFallback + if (itemClass == null) { + context.log.warn("MediaFilePicker: sendItems second parameter type has no generic info (type erasure). genericParameterTypes[1]=${method.genericParameterTypes.getOrNull(1)}") + context.inAppOverlay.showStatusToast(Icons.Default.Error, "Failed to send media (incompatible version).") + return + } + val item = itemClass.dataBuilder { + from("_item") { + set("_cameraRollSource", "Snapchat") + set("_contentUri", "") + set("_durationMs", 0.0) + set("_disabled", false) + set("_imageRotation", 0.0) + set("_width", 1080.0) + set("_height", 1920.0) + set("_timestampMs", System.currentTimeMillis().toDouble()) + from("_itemId") { + set("_itemId", firstVideoId.toString()) + set("_type", "VIDEO") } - set("_order", 0.0) } - )) + set("_order", 0.0) + } ?: run { + context.inAppOverlay.showStatusToast(Icons.Default.Error, "Failed to build media item.") + return + } + method.invoke(chatMediaDrawerActionHandler, listOf(), listOf(item)) } fun startConversion(audioOnly: Boolean) { @@ -204,7 +228,7 @@ class MediaFilePicker : Feature("Media File Picker") { val buttonTag = Random.nextInt(0, 65535) context.event.subscribe(AddViewEvent::class) { event -> - if (event.parent !is FrameLayout || !event.view::class.java.name.endsWith("ChatMediaDrawer")) return@subscribe + if (event.parent !is FrameLayout || drawerViewClass?.isInstance(event.view) != true) return@subscribe event.view.addOnAttachStateChangeListener(object: View.OnAttachStateChangeListener { override fun onViewAttachedToWindow(v: View) { diff --git a/gradle.properties b/gradle.properties index 78756ceb..e32555bb 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,6 +1,7 @@ org.gradle.jvmargs=-Xms2G -Xmx4G -Dfile.encoding=UTF-8 --enable-native-access=ALL-UNNAMED org.gradle.parallel=true -org.gradle.workers.max=8 +# Limit workers to avoid R8/minify file locking on Windows (classes.dex in use by another process) +org.gradle.workers.max=1 android.useAndroidX=true kotlin.code.style=official android.nonTransitiveRClass=true diff --git a/mapper/src/main/kotlin/me/eternal/purrfectsnap/mapper/ClassMapper.kt b/mapper/src/main/kotlin/me/eternal/purrfectsnap/mapper/ClassMapper.kt index 8f788d03..9f2eedc5 100644 --- a/mapper/src/main/kotlin/me/eternal/purrfectsnap/mapper/ClassMapper.kt +++ b/mapper/src/main/kotlin/me/eternal/purrfectsnap/mapper/ClassMapper.kt @@ -40,6 +40,7 @@ class ClassMapper( COFObservableMapper(), FoldingLayoutMapper(), PlatformClientAttestationMapper(), + ChatMediaDrawerMapper(), ) } diff --git a/mapper/src/main/kotlin/me/eternal/purrfectsnap/mapper/impl/ChatMediaDrawerMapper.kt b/mapper/src/main/kotlin/me/eternal/purrfectsnap/mapper/impl/ChatMediaDrawerMapper.kt new file mode 100644 index 00000000..dd1f2c5e --- /dev/null +++ b/mapper/src/main/kotlin/me/eternal/purrfectsnap/mapper/impl/ChatMediaDrawerMapper.kt @@ -0,0 +1,48 @@ +package me.eternal.purrfectsnap.mapper.impl + +import me.eternal.purrfectsnap.mapper.AbstractClassMapper +import me.eternal.purrfectsnap.mapper.ext.getClassName +import me.eternal.purrfectsnap.mapper.ext.getSuperClassName + +class ChatMediaDrawerMapper : AbstractClassMapper("ChatMediaDrawer") { + val chatMediaDrawerClass = classReference("chatMediaDrawerClass") + val actionHandlerClass = classReference("actionHandlerClass") + val sendItemsMethodName = string("sendItemsMethodName") + /** List element type for sendItems(List, List) second parameter — used when generic type is erased. */ + val sendItemsListItemClass = classReference("sendItemsListItemClass") + + init { + mapper { + val drawerClass = classes.firstOrNull { clazz -> + val name = clazz.getClassName() + val superName = clazz.getSuperClassName() ?: return@firstOrNull false + name.contains("ChatMediaDrawer") && + !name.contains("ActionHandler") && + superName.contains("ValdiGeneratedRootView") + } ?: return@mapper + + val actionHandlerClazz = classes.firstOrNull { clazz -> + clazz.getClassName().contains("ChatMediaDrawerActionHandler") + } ?: return@mapper + + val sendItemsMethod = actionHandlerClazz.methods.firstOrNull { method -> + method.name == "sendItems" && method.parameterTypes.size == 2 + } ?: actionHandlerClazz.methods.firstOrNull { method -> + method.parameterTypes.size == 2 && + method.parameterTypes[0].startsWith("Ljava/util/") && + method.parameterTypes[1].startsWith("Ljava/util/") + } ?: return@mapper + + chatMediaDrawerClass.set(drawerClass.getClassName().replace("/", ".")) + actionHandlerClass.set(actionHandlerClazz.getClassName().replace("/", ".")) + sendItemsMethodName.set(sendItemsMethod.name) + + // Find the list item type (e.g. Azc) by structure: has _order and _item (Valdi schema) + val listItemClass = classes.firstOrNull { clazz -> + val fieldNames = clazz.fields.map { it.name }.toSet() + fieldNames.contains("_order") && fieldNames.contains("_item") + } + listItemClass?.let { sendItemsListItemClass.set(it.getClassName().replace("/", ".")) } + } + } +}