diff --git a/README.md b/README.md
index bfec7f8f..c67963a1 100644
--- a/README.md
+++ b/README.md
@@ -16,7 +16,7 @@
[](https://github.com/particle-box/PurrfectSnap/releases)
[](https://github.com/particle-box/PurrfectSnap/blob/dev/LICENSE)
-[Installation](#installation) • [Features](#features) • [Changelog](#whats-new) • [Community](#community)
+[Installation](#installation) • [Features](#features) • [Build](#build-from-source) • [Community](#community)
@@ -40,44 +40,9 @@ No aggressive donation requests. No minimal changes disguised as “major update
----
-
-
-
-## 🔄 How It Works
-
-```mermaid
-graph TD
- A[Install PurrfectSnap] --> B{Device Type?}
- B -->|Non-Rooted| C[Auto Patcher Runs]
- B -->|Rooted| D[Xposed Framework]
- C --> E[Snapchat Patched Automatically]
- D --> E
- E --> F[Launch Snapchat]
- F --> G[Access PurrfectSnap Settings]
- G --> H[Configure Features]
- H --> I{Enable Features}
- I --> J[Media Downloader]
- I --> K[Privacy Controls]
- I --> L[UI Customization]
- I --> M[Camera Enhancements]
- I --> N[AI Intelligence Layer]
- J --> O[Enhanced Snapchat Experience]
- K --> O
- L --> O
- M --> O
- N --> O
-```
-
-
-
----
-
-
-
## Features
-PurrfectSnap offers deep control across media, privacy, automation, and UI—designed for both casual users and power users.
+PurrfectSnap offers deep control across media, privacy, automation, and UI:designed for both casual users and power users.
@@ -246,152 +211,6 @@ Extensibility through custom scripts.
----
-
-
-
-## What's New
-
-Here's what makes PurrfectSnap different from upstream SnapEnhance.
-
-
-
-### Aurora Design System
-
-A complete visual language rebuilt from the ground up. Not just a theme, an entire design philosophy.
-
-The interface now flows with purpose. Subtle animations guide your interactions. Every screen has been reconsidered, every transition refined. The result is an experience that feels premium without being ostentatious.
-
-
-
-### PurrAura
-
-The ban problem? Solved.
-
-No more sacrificing features to stay safe. Add friends, block users, attach music; everything works as it should. No workarounds, no compromises, no disadvantages.
-
-
-
-### Auto Patcher
-
-Installation was complicated. Now it isn’t.
-
-We've eliminated 90% of the installation friction. No Shizuku. No LSPatch. Just a straightforward process that respects your time.
-
-
-
-### Intelligence Layer
-
-**Auto Reply**
-Let AI handle routine responses while you focus on what matters. Configure automatic replies for chats, stories, and half-swipes. Essential for creators managing volume. Requires an API key from Google AI Studio, they offer free tier access.
-
-**Scheduled Snaps**
-Time-shifted communication. Set a snap to send at midnight for a birthday. Queue content for optimal timing:your schedule, automated.
-
-**Message Translator**
-Language barriers, eliminated. Incoming messages automatically translate to your preferred language. Set it once, communicate globally.
-
-
-
-### Privacy Enhancements
-
-**Granular Controls**
-Whitelist and blacklist modes for typing indicators and message logging. Choose exactly who sees what, friend by friend.
-
-**Auto Delete Messages**
-Set expiration times for your messages. They disappear on your schedule, not Snapchat's.
-
-**Message Logger Viewer**
-Review exported logs directly in-app. No external tools required.
-
-
-
-### Data Persistence
-
-**Friend Notes Backup**
-Your notes are valuable. Now they're protected. Export and restore with ease. Never lose context again.
-
-
-
-### Device Management
-
-**ID Spoofing**
-Device bans happen. Sometimes unjustly. We offer a solution for legitimate cases, approval required with proof of wrongful ban. This feature is restricted to prevent abuse.
-
-**Model Spoofing**
-Identify as a high-end device model. Potential performance improvements, enhanced camera processing, and better feature access.
-
-**Network Spoofing**
-Spoof your network status to Wi-Fi always to prevent several restrictions on mobile data.
-
-
-
-### Restored Functionality
-
-Features that disappeared? They're back.
-
-Bulk messaging works again. Friend list management is fully operational. These aren't new features, they're restored capabilities that should never have been lost.
-
-
-
-### Discovery
-
-**Scripts Catalog**
-Browse and install scripts without leaving the app. No more hunting for import links. One-tap installation. Always current.
-
-**Friend Tracker Catalog**
-Browse and install tracking rules without leaving the app. One-tap installation. Always current.
-
-**Friend Tracker Import/Export**
-Backup and restore your Friend Tracker configurations with ease.
-
-
-
-### Advanced Tracking
-
-**I Can See You Rule**
-Track the exact time and duration when a friend enters your chat. Precision visibility tracking for Friend Tracker.
-
-**Auto Open Snaps Enhancement**
-More granular control with queue size configuration and customizable delays.
-
-
-
-### Visual Refinements
-
-**AMOLED Theme**
-True black. Battery-saving. The most requested feature, delivered.
-
-**Haptic Feedback**
-Subtle tactile responses. Every interaction confirmed through touch.
-
-**Customizable Bottom Bar**
-Your navigation, your rules. Rearrange tabs, remove what you don't use, set your default view. Complete flexibility.
-
-**Search History**
-Recently searched features, instantly accessible. No more hunting through menus.
-
-
-
-### Infrastructure
-
-**In-App Updates**
-New versions install directly from within the app. No GitHub hunting. No manual downloads. Just seamless updates.
-
-**Update Notifications**
-Stay current automatically. Get notified when new releases are available.
-
-**Config Export/Import Summary**
-See exactly which features a config will enable before importing or exporting. Know what you're getting.
-
-
-
-### Quality
-
-Hundreds of refinements you'll feel but might not see. Smoother animations. Faster response times. Eliminated edge cases. The kind of polish that separates good from exceptional.
-
-
-
> ⚠️Some features are intended for educational, recovery, or accessibility purposes. Misuse may violate Snapchat’s terms of service.
> **We are not affiliated with Snap Inc. and are not responsible for any violations by the user. Our sole purpose is for education purposes only!**
@@ -425,15 +244,79 @@ Questions? Ideas? Found a bug? Our community is active and responsive.
-## Contributing
+## Build from Source
We welcome contributions from developers who share our vision. Whether it's code, documentation, etc., contributions are always appreciated. Feel free to open a PR :)
-
+### 1) Fork
----
+1. Fork this repository on GitHub.
+```
+
+### 2) Generate a Release Keystore (Certificate)
+
+Create a signing keystore (`.jks`) and keep it secure:
+
+```bash
+keytool -genkeypair -v \
+ -keystore purrfectsnap-release.keystore \
+ -storetype JKS \
+ -alias purrfectsnap \
+ -keyalg RSA \
+ -keysize 2048 \
+ -validity 10000
+```
+
+Optional local placement for Gradle signing:
+
+```bash
+mkdir -p ~/.android
+cp purrfectsnap-release.keystore ~/.android/purrfectsnap-release.keystore
+```
+
+### 3) Convert Keystore to Base64
+
+For GitHub Actions secret `PS_BASE_64`:
+
+Linux/macOS:
+
+```bash
+base64 -w 0 purrfectsnap-release.keystore > keystore.base64
+```
+
+PowerShell (Windows):
+
+```powershell
+[Convert]::ToBase64String([IO.File]::ReadAllBytes("purrfectsnap-release.keystore")) | Set-Content -NoNewline keystore.base64
+```
+
+### 4) Add Required GitHub Secrets (Your Fork)
+
+Go to:
+`Settings -> Secrets and variables -> Actions -> New repository secret`
+
+Create these exact secrets (from `.github/workflows/release.yml`):
+
+- `PS_BASE_64` = contents of `keystore.base64`
+- `PS_RELEASE_KEY_ALIAS` = `purrfectsnap` (or your chosen alias)
+- `PS_RELEASE_KEY_PASSWORD` = key password from `keytool`
+- `PS_RELEASE_STORE_PASSWORD` = keystore password from `keytool`
+
+### 5) Run Release Workflow
+
+1. Open `Actions` in your fork.
+2. Run `PurrfectSnap Release CI` (`workflow_dispatch`).
+3. Workflow builds and signs:
+ - `armv8` release APK
+ - `armv7` release APK
+4. Workflow creates a GitHub Release and uploads APK assets.
+
+### Notes
+
+- CI uses `JDK 21 (Temurin)`.
+- Signing requires all four secrets above.
+- Keep keystore and passwords private. Rotate immediately if exposed.
-
## Credits
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/fastlane/metadata/android/en-US/changelogs/270.txt b/fastlane/metadata/android/en-US/changelogs/270.txt
new file mode 100644
index 00000000..cea775f4
--- /dev/null
+++ b/fastlane/metadata/android/en-US/changelogs/270.txt
@@ -0,0 +1,18 @@
+- New: Bulk Manage Friend Requests(tq to RSR)
+- Fix: Bulk Action Progress Dialogs(tq to RSR)
+- Fix: Audio note send override(tq to RSR)
+- Fix: Improve PurrAura Active indicator(tq to RSR)
+- Fix: Prevent Story Rewatch(tq to Brandon)
+- Fix: Location search(tq to Brandon)
+- Fix: story download & decryption(tq to Hazelnut)
+- New: Add confirmation dialogs for Reset PurrfectSnap option
+- New: Google Maps Integration(tq to Brandon)
+- Fix: support more translation keys
+- Fix: several inconsistencies
+- Fix: add proper support for scripts
+- New: Swap wiki with new website
+- Fix: Custom emoji is now stable!
+- Fix: Poor Call recorder quality
+- New: order feature for chat exports
+- Fix: Gemini Auto Reply(tq to RSR)
+- New: Spotlight Creator Info(tq to RSR)
diff --git a/fastlane/metadata/android/en-US/full_description.txt b/fastlane/metadata/android/en-US/full_description.txt
new file mode 100644
index 00000000..ce0dfbb1
--- /dev/null
+++ b/fastlane/metadata/android/en-US/full_description.txt
@@ -0,0 +1,9 @@
+PurrfectSnap is an open-source module for Snapchat that adds advanced customization and utility features for rooted and non-rooted setups.
+
+It includes media download options, privacy controls, interface tweaks, automation tools, and quality-of-life improvements designed to give users more control over their Snapchat experience.
+
+Highlights:
+- Media and download utilities
+- Privacy and visibility controls
+- UI customization and theming
+- Rooted and non-rooted support
\ No newline at end of file
diff --git a/fastlane/metadata/android/en-US/images/featureGraphic.png b/fastlane/metadata/android/en-US/images/featureGraphic.png
new file mode 100644
index 00000000..9abe590f
Binary files /dev/null and b/fastlane/metadata/android/en-US/images/featureGraphic.png differ
diff --git a/fastlane/metadata/android/en-US/images/icon.png b/fastlane/metadata/android/en-US/images/icon.png
new file mode 100644
index 00000000..782d331b
Binary files /dev/null and b/fastlane/metadata/android/en-US/images/icon.png differ
diff --git a/fastlane/metadata/android/en-US/images/phoneScreenshots/6030798506894756928.jpg b/fastlane/metadata/android/en-US/images/phoneScreenshots/6030798506894756928.jpg
new file mode 100644
index 00000000..dfbced8f
Binary files /dev/null and b/fastlane/metadata/android/en-US/images/phoneScreenshots/6030798506894756928.jpg differ
diff --git a/fastlane/metadata/android/en-US/images/phoneScreenshots/6030798506894756929.jpg b/fastlane/metadata/android/en-US/images/phoneScreenshots/6030798506894756929.jpg
new file mode 100644
index 00000000..b778dfb2
Binary files /dev/null and b/fastlane/metadata/android/en-US/images/phoneScreenshots/6030798506894756929.jpg differ
diff --git a/fastlane/metadata/android/en-US/images/phoneScreenshots/6030798506894756930.jpg b/fastlane/metadata/android/en-US/images/phoneScreenshots/6030798506894756930.jpg
new file mode 100644
index 00000000..f035eeec
Binary files /dev/null and b/fastlane/metadata/android/en-US/images/phoneScreenshots/6030798506894756930.jpg differ
diff --git a/fastlane/metadata/android/en-US/images/phoneScreenshots/6030798506894756931.jpg b/fastlane/metadata/android/en-US/images/phoneScreenshots/6030798506894756931.jpg
new file mode 100644
index 00000000..5d640689
Binary files /dev/null and b/fastlane/metadata/android/en-US/images/phoneScreenshots/6030798506894756931.jpg differ
diff --git a/fastlane/metadata/android/en-US/images/phoneScreenshots/6030798506894756932.jpg b/fastlane/metadata/android/en-US/images/phoneScreenshots/6030798506894756932.jpg
new file mode 100644
index 00000000..fc355fb0
Binary files /dev/null and b/fastlane/metadata/android/en-US/images/phoneScreenshots/6030798506894756932.jpg differ
diff --git a/fastlane/metadata/android/en-US/images/phoneScreenshots/6030798506894756933.jpg b/fastlane/metadata/android/en-US/images/phoneScreenshots/6030798506894756933.jpg
new file mode 100644
index 00000000..6915b8ef
Binary files /dev/null and b/fastlane/metadata/android/en-US/images/phoneScreenshots/6030798506894756933.jpg differ
diff --git a/fastlane/metadata/android/en-US/images/phoneScreenshots/6030798506894756934.jpg b/fastlane/metadata/android/en-US/images/phoneScreenshots/6030798506894756934.jpg
new file mode 100644
index 00000000..3a27fef6
Binary files /dev/null and b/fastlane/metadata/android/en-US/images/phoneScreenshots/6030798506894756934.jpg differ
diff --git a/fastlane/metadata/android/en-US/short_description.txt b/fastlane/metadata/android/en-US/short_description.txt
new file mode 100644
index 00000000..e68ee94f
--- /dev/null
+++ b/fastlane/metadata/android/en-US/short_description.txt
@@ -0,0 +1 @@
+Advanced Snapchat customization, privacy, and automation tools for rooted and non-rooted devices.
\ No newline at end of file
diff --git a/fastlane/metadata/android/en-US/title.txt b/fastlane/metadata/android/en-US/title.txt
new file mode 100644
index 00000000..a3cbaa38
--- /dev/null
+++ b/fastlane/metadata/android/en-US/title.txt
@@ -0,0 +1 @@
+PurrfectSnap
\ No newline at end of file
diff --git a/gradle.properties b/gradle.properties
index ec216424..41d1d151 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/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties
index 2dcec856..bc75e3bb 100644
--- a/gradle/wrapper/gradle-wrapper.properties
+++ b/gradle/wrapper/gradle-wrapper.properties
@@ -1,6 +1,7 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-9.0-bin.zip
+distributionSha256Sum=371cb9fbebbe9880d147f59bab36d61eee122854ef8c9ee1ecf12b82368bcf10
networkTimeout=10000
validateDistributionUrl=true
zipStoreBase=GRADLE_USER_HOME
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("/", ".")) }
+ }
+ }
+}