commit 5c3ad5d5880ee4d1b4fef769e42381bd38312c3f
parent 2b90ee2deb3711b023a2b7c256f328731e90eda6
Author: rhunk <101876869+rhunk@users.noreply.github.com>
Date:   Tue, 22 Aug 2023 18:41:23 +0200

feat: rule system

Diffstat:
Mapp/src/main/kotlin/me/rhunk/snapenhance/bridge/BridgeService.kt | 11++++++-----
Mapp/src/main/kotlin/me/rhunk/snapenhance/messaging/ModDatabase.kt | 36++++++++++++++----------------------
Mapp/src/main/kotlin/me/rhunk/snapenhance/ui/manager/sections/features/Dialogs.kt | 5+----
Mapp/src/main/kotlin/me/rhunk/snapenhance/ui/manager/sections/features/FeaturesSection.kt | 20++++++++++----------
Mapp/src/main/kotlin/me/rhunk/snapenhance/ui/manager/sections/social/ScopeContent.kt | 34++++++++++++++++++++++------------
Mcore/src/main/aidl/me/rhunk/snapenhance/bridge/BridgeInterface.aidl | 6+++++-
Mcore/src/main/assets/lang/en_US.json | 40++++++++++++++++++++++++++++++++++++++++
Mcore/src/main/kotlin/me/rhunk/snapenhance/bridge/BridgeClient.kt | 13++++++-------
Mcore/src/main/kotlin/me/rhunk/snapenhance/core/config/ConfigContainer.kt | 5++---
Mcore/src/main/kotlin/me/rhunk/snapenhance/core/config/ConfigObjects.kt | 63+++++++++++++++++++++++++++++++++++++++++++++++++++++----------
Mcore/src/main/kotlin/me/rhunk/snapenhance/core/config/impl/Camera.kt | 5+++--
Mcore/src/main/kotlin/me/rhunk/snapenhance/core/config/impl/DownloaderConfig.kt | 3++-
Mcore/src/main/kotlin/me/rhunk/snapenhance/core/config/impl/Global.kt | 2+-
Mcore/src/main/kotlin/me/rhunk/snapenhance/core/config/impl/RootConfig.kt | 1+
Acore/src/main/kotlin/me/rhunk/snapenhance/core/config/impl/Rules.kt | 27+++++++++++++++++++++++++++
Mcore/src/main/kotlin/me/rhunk/snapenhance/core/config/impl/UserInterfaceTweaks.kt | 16++++++++++------
Mcore/src/main/kotlin/me/rhunk/snapenhance/core/messaging/MessagingCoreObjects.kt | 31+++++++++++++++++++++++--------
Mcore/src/main/kotlin/me/rhunk/snapenhance/database/DatabaseAccess.kt | 52++++++++++++++++++++++++++++++++++------------------
Acore/src/main/kotlin/me/rhunk/snapenhance/features/MessagingRuleFeature.kt | 34++++++++++++++++++++++++++++++++++
Mcore/src/main/kotlin/me/rhunk/snapenhance/features/impl/Messaging.kt | 4++--
Dcore/src/main/kotlin/me/rhunk/snapenhance/features/impl/downloader/AntiAutoDownload.kt | 20--------------------
Mcore/src/main/kotlin/me/rhunk/snapenhance/features/impl/downloader/MediaDownloader.kt | 9+++++----
Mcore/src/main/kotlin/me/rhunk/snapenhance/features/impl/spying/MessageLogger.kt | 4+---
Mcore/src/main/kotlin/me/rhunk/snapenhance/features/impl/spying/PreventReadReceipts.kt | 2+-
Mcore/src/main/kotlin/me/rhunk/snapenhance/features/impl/spying/StealthMode.kt | 20++++----------------
Mcore/src/main/kotlin/me/rhunk/snapenhance/features/impl/tweaks/AutoSave.kt | 13++++++-------
Mcore/src/main/kotlin/me/rhunk/snapenhance/features/impl/tweaks/Notifications.kt | 2+-
Mcore/src/main/kotlin/me/rhunk/snapenhance/manager/impl/FeatureManager.kt | 2--
Mcore/src/main/kotlin/me/rhunk/snapenhance/ui/menu/impl/FriendFeedInfoMenu.kt | 201+++++++++++++------------------------------------------------------------------
29 files changed, 347 insertions(+), 334 deletions(-)

diff --git a/app/src/main/kotlin/me/rhunk/snapenhance/bridge/BridgeService.kt b/app/src/main/kotlin/me/rhunk/snapenhance/bridge/BridgeService.kt @@ -123,11 +123,12 @@ class BridgeService : Service() { ).onReceive(intent) } - override fun getRules(objectType: String, uuid: String): MutableList<String> { - remoteSideContext.modDatabase.getRulesFromId(SocialScope.valueOf(objectType), uuid) - .let { rules -> - return rules.map { it.toJson() }.toMutableList() - } + override fun getRules(uuid: String): List<String> { + return remoteSideContext.modDatabase.getRules(uuid).map { it.key } + } + + override fun setRule(uuid: String, rule: String, state: Boolean) { + remoteSideContext.modDatabase.setRule(uuid, rule, state) } override fun sync(callback: SyncCallback) { diff --git a/app/src/main/kotlin/me/rhunk/snapenhance/messaging/ModDatabase.kt b/app/src/main/kotlin/me/rhunk/snapenhance/messaging/ModDatabase.kt @@ -6,8 +6,7 @@ import me.rhunk.snapenhance.RemoteSideContext import me.rhunk.snapenhance.core.messaging.FriendStreaks import me.rhunk.snapenhance.core.messaging.MessagingFriendInfo import me.rhunk.snapenhance.core.messaging.MessagingGroupInfo -import me.rhunk.snapenhance.core.messaging.MessagingRule -import me.rhunk.snapenhance.core.messaging.SocialScope +import me.rhunk.snapenhance.core.messaging.MessagingRuleType import me.rhunk.snapenhance.database.objects.FriendInfo import me.rhunk.snapenhance.util.SQLiteDatabaseHelper import me.rhunk.snapenhance.util.ktx.getInteger @@ -53,10 +52,8 @@ class ModDatabase( ), "rules" to listOf( "id INTEGER PRIMARY KEY AUTOINCREMENT", - "scope VARCHAR", - "targetUuid VARCHAR", - //"mode VARCHAR", - "subject VARCHAR" + "type VARCHAR", + "targetUuid VARCHAR" ), "streaks" to listOf( "userId VARCHAR PRIMARY KEY", @@ -157,34 +154,29 @@ class ModDatabase( } } - fun getRulesFromId(type: SocialScope, targetUuid: String): List<MessagingRule> { - return database.rawQuery("SELECT * FROM rules WHERE scope = ? AND targetUuid = ?", arrayOf(type.name, targetUuid)).use { cursor -> - val rules = mutableListOf<MessagingRule>() + fun getRules(targetUuid: String): List<MessagingRuleType> { + return database.rawQuery("SELECT type FROM rules WHERE targetUuid = ?", arrayOf( + targetUuid + )).use { cursor -> + val rules = mutableListOf<MessagingRuleType>() while (cursor.moveToNext()) { - rules.add(MessagingRule( - id = cursor.getInteger("id"), - socialScope = SocialScope.valueOf(cursor.getStringOrNull("scope")!!), - targetUuid = cursor.getStringOrNull("targetUuid")!!, - subject = cursor.getStringOrNull("subject")!! - )) + rules.add(MessagingRuleType.getByName(cursor.getStringOrNull("type")!!)) } rules } } - fun toggleRuleFor(type: SocialScope, targetUuid: String, subject: String, enabled: Boolean) { + fun setRule(targetUuid: String, type: String, enabled: Boolean) { executeAsync { if (enabled) { - database.execSQL("INSERT OR REPLACE INTO rules (scope, targetUuid, subject) VALUES (?, ?, ?)", arrayOf( - type.name, + database.execSQL("INSERT OR REPLACE INTO rules (targetUuid, type) VALUES (?, ?)", arrayOf( targetUuid, - subject + type )) } else { - database.execSQL("DELETE FROM rules WHERE scope = ? AND targetUuid = ? AND subject = ?", arrayOf( - type.name, + database.execSQL("DELETE FROM rules WHERE targetUuid = ? AND type = ?", arrayOf( targetUuid, - subject + type )) } } diff --git a/app/src/main/kotlin/me/rhunk/snapenhance/ui/manager/sections/features/Dialogs.kt b/app/src/main/kotlin/me/rhunk/snapenhance/ui/manager/sections/features/Dialogs.kt @@ -57,10 +57,7 @@ class Dialogs( @Composable fun TranslatedText(property: PropertyPair<*>, key: String, modifier: Modifier = Modifier) { Text( - text = when (key) { - "null" -> translation["manager.features.disabled"] - else -> if (property.key.params.shouldTranslate) translation["features.options.${property.key.name}.$key"] else key - }, + text = property.key.propertyOption(translation, key), modifier = Modifier .padding(10.dp, 10.dp, 10.dp, 10.dp) .then(modifier) diff --git a/app/src/main/kotlin/me/rhunk/snapenhance/ui/manager/sections/features/FeaturesSection.kt b/app/src/main/kotlin/me/rhunk/snapenhance/ui/manager/sections/features/FeaturesSection.kt @@ -28,6 +28,7 @@ import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.Close import androidx.compose.material.icons.filled.FolderOpen import androidx.compose.material.icons.filled.OpenInNew +import androidx.compose.material.icons.filled.Rule import androidx.compose.material.icons.filled.Search import androidx.compose.material.icons.rounded.Save import androidx.compose.material3.BottomSheetScaffoldState @@ -71,6 +72,7 @@ import kotlinx.coroutines.Job import kotlinx.coroutines.delay import kotlinx.coroutines.launch import me.rhunk.snapenhance.core.config.ConfigContainer +import me.rhunk.snapenhance.core.config.ConfigFlag import me.rhunk.snapenhance.core.config.DataProcessors import me.rhunk.snapenhance.core.config.PropertyKey import me.rhunk.snapenhance.core.config.PropertyPair @@ -172,8 +174,8 @@ class FeaturesSection : Section() { backStackEntry.arguments?.getString("keyword")?.let { keyword -> val properties = allProperties.filter { it.key.name.contains(keyword, ignoreCase = true) || - context.translation["${it.key.propertyTranslationPath()}.name"].contains(keyword, ignoreCase = true) || - context.translation["${it.key.propertyTranslationPath()}.description"].contains(keyword, ignoreCase = true) + context.translation[it.key.propertyName()].contains(keyword, ignoreCase = true) || + context.translation[it.key.propertyDescription()].contains(keyword, ignoreCase = true) }.map { PropertyPair(it.key, it.value) } PropertiesView(properties) @@ -199,7 +201,7 @@ class FeaturesSection : Section() { val propertyValue = property.value - if (property.key.params.isFolder) { + if (property.key.params.flags.contains(ConfigFlag.FOLDER)) { IconButton(onClick = registerClickCallback { openFolderCallback = { uri -> propertyValue.setAny(uri) @@ -234,11 +236,9 @@ class FeaturesSection : Section() { overflow = TextOverflow.Ellipsis, maxLines = 1, modifier = Modifier.widthIn(0.dp, 120.dp), - text = (propertyValue.getNullable() as? String)?.let{ - if (property.key.params.shouldTranslate) { - context.translation["features.options.${property.name}.$it"] - } else it - } ?: context.translation["manager.features.disabled"], + text = (propertyValue.getNullable() as? String ?: "null").let { + property.key.propertyOption(context.translation, it) + } ) } @@ -353,12 +353,12 @@ class FeaturesSection : Section() { .padding(all = 10.dp) ) { Text( - text = context.translation["${property.key.propertyTranslationPath()}.name"], + text = context.translation[property.key.propertyName()], fontSize = 16.sp, fontWeight = FontWeight.Bold ) Text( - text = context.translation["${property.key.propertyTranslationPath()}.description"], + text = context.translation[property.key.propertyDescription()], fontSize = 12.sp, lineHeight = 15.sp ) diff --git a/app/src/main/kotlin/me/rhunk/snapenhance/ui/manager/sections/social/ScopeContent.kt b/app/src/main/kotlin/me/rhunk/snapenhance/ui/manager/sections/social/ScopeContent.kt @@ -71,23 +71,33 @@ class ScopeContent( Spacer(modifier = Modifier.height(16.dp)) - val scopeRules = context.modDatabase.getRulesFromId(scope, id) + val rules = context.modDatabase.getRules(id) SectionTitle("Rules") ContentCard { //manager anti features etc - MessagingRuleType.values().forEach { feature -> - var featureEnabled by remember { - mutableStateOf(scopeRules.any { it.subject == feature.key }) + MessagingRuleType.values().forEach { ruleType -> + var ruleEnabled by remember { + mutableStateOf(rules.any { it.key == ruleType.key }) } - val featureEnabledText = if (featureEnabled) "Enabled" else "Disabled" - Row { - Text(text = "${feature.key}: $featureEnabledText", maxLines = 1) - Switch(checked = featureEnabled, onCheckedChange = { - context.modDatabase.toggleRuleFor(scope, id, feature.key, it) - featureEnabled = it + val ruleState = context.config.root.rules.getRuleState(ruleType) + + Row( + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier.padding(all = 4.dp) + ) { + Text( + text = if (ruleType.listMode && ruleState != null) { + context.translation["rules.properties.${ruleType.key}.options.${ruleState.key}"] + } else context.translation["rules.properties.${ruleType.key}.name"], + maxLines = 1, + modifier = Modifier.weight(1f) + ) + Switch(checked = ruleEnabled, enabled = if (ruleType.listMode) ruleState != null else true, onCheckedChange = { + context.modDatabase.setRule(id, ruleType.key, it) + ruleEnabled = it }) } } @@ -195,9 +205,9 @@ class ScopeContent( fontSize = 12.sp, fontWeight = FontWeight.Light ) - Spacer(modifier = Modifier.height(16.dp)) + // Spacer(modifier = Modifier.height(16.dp)) - DeleteScopeEntityButton() + //DeleteScopeEntityButton() } Spacer(modifier = Modifier.height(16.dp)) diff --git a/core/src/main/aidl/me/rhunk/snapenhance/bridge/BridgeInterface.aidl b/core/src/main/aidl/me/rhunk/snapenhance/bridge/BridgeInterface.aidl @@ -63,8 +63,12 @@ interface BridgeInterface { /** * Get rules for a given user or conversation */ + List<String> getRules(String uuid); - List<String> getRules(String objectType, String uuid); + /** + * Update rule for a giver user or conversation + */ + void setRule(String uuid, String type, boolean state); /** * Sync groups and friends diff --git a/core/src/main/assets/lang/en_US.json b/core/src/main/assets/lang/en_US.json @@ -28,6 +28,42 @@ } }, + "rules": { + "modes": { + "blacklist": "Blacklist mode", + "whitelist": "Whitelist mode" + }, + "properties": { + "auto_download": { + "name": "Auto download", + "description": "Auto download snaps when viewed", + "options": { + "blacklist": "Exclude from Auto Download", + "whitelist": "Auto Download" + } + }, + "stealth": { + "name": "Stealth Mode", + "description": "Prevents anyone from knowing you've opened their Snaps/Chats and conversations", + "options": { + "blacklist": "Exclude from stealth mode", + "whitelist": "Stealth mode" + } + }, + "auto_save": { + "name": "Auto Save", + "description": "Saves chat messages when viewed", + "options": { + "blacklist": "Exclude from auto save", + "whitelist": "Auto save" + } + }, + "hide_chat_feed": { + "name": "Hide from chat feed" + } + } + }, + "action": { "clean_cache": "Clean Cache", "clear_message_logger": "Clear Message Logger", @@ -239,6 +275,10 @@ } } }, + "rules": { + "name": "Rules", + "description": "Manage automatic features\nThe social tab lets you assign a rule to an object" + }, "camera": { "name": "Camera", "description": "Adjust the right settings for the perfect snap", diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/bridge/BridgeClient.kt b/core/src/main/kotlin/me/rhunk/snapenhance/bridge/BridgeClient.kt @@ -15,10 +15,8 @@ import me.rhunk.snapenhance.ModContext import me.rhunk.snapenhance.bridge.types.BridgeFileType import me.rhunk.snapenhance.bridge.types.FileActionType import me.rhunk.snapenhance.core.BuildConfig -import me.rhunk.snapenhance.core.messaging.MessagingRule -import me.rhunk.snapenhance.core.messaging.SocialScope +import me.rhunk.snapenhance.core.messaging.MessagingRuleType import me.rhunk.snapenhance.data.LocalePair -import me.rhunk.snapenhance.util.SerializableDataObject import java.util.concurrent.CompletableFuture import java.util.concurrent.Executors import kotlin.system.exitProcess @@ -138,9 +136,10 @@ class BridgeClient( fun passGroupsAndFriends(groups: List<String>, friends: List<String>) = service.passGroupsAndFriends(groups, friends) - fun getRulesFromId(type: SocialScope, targetUuid: String): List<MessagingRule> { - return service.getRules(type.name, targetUuid).map { - SerializableDataObject.fromJson(it, MessagingRule::class.java) - }.toList() + fun getRules(targetUuid: String): List<MessagingRuleType> { + return service.getRules(targetUuid).map { MessagingRuleType.getByName(it) } } + + fun setRule(targetUuid: String, type: MessagingRuleType, state: Boolean) + = service.setRule(targetUuid, type.key, state) } diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/core/config/ConfigContainer.kt b/core/src/main/kotlin/me/rhunk/snapenhance/core/config/ConfigContainer.kt @@ -71,9 +71,8 @@ open class ConfigContainer( fun fromJson(json: JsonObject) { properties.forEach { (key, _) -> val jsonElement = json.get(key.name) ?: return@forEach - key.dataType.deserializeAny(jsonElement)?.let { - properties[key]?.setAny(it) - } + //TODO: check incoming values + properties[key]?.setAny(key.dataType.deserializeAny(jsonElement)) } } diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/core/config/ConfigObjects.kt b/core/src/main/kotlin/me/rhunk/snapenhance/core/config/ConfigObjects.kt @@ -1,5 +1,6 @@ package me.rhunk.snapenhance.core.config +import me.rhunk.snapenhance.bridge.wrapper.LocaleWrapper import kotlin.reflect.KProperty @@ -10,17 +11,43 @@ data class PropertyPair<T>( val name get() = key.name } +enum class FeatureNotice( + val id: Int, + val key: String +) { + UNSTABLE(0b0001, "unstable"), + MAY_BAN(0b0010, "may_ban"), + MAY_BREAK_INTERNAL_BEHAVIOR(0b0100, "may_break_internal_behavior"), + MAY_CAUSE_CRASHES(0b1000, "may_cause_crashes"); +} + +enum class ConfigFlag( + val id: Int +) { + NO_TRANSLATE(0b0001), + HIDDEN(0b0010), + FOLDER(0b0100), + NO_DISABLE_KEY(0b1000) +} + class ConfigParams( + private var _flags: Int? = null, private var _notices: Int? = null, - var shouldTranslate: Boolean = true, - var isHidden: Boolean = false, - var isFolder: Boolean = false, + + var icon: String? = null, var disabledKey: String? = null, - var icon: String? = null + var customTranslationPath: String? = null, + var customOptionTranslationPath: String? = null ) { val notices get() = _notices?.let { FeatureNotice.values().filter { flag -> it and flag.id != 0 } } ?: emptyList() - fun addNotices(vararg flags: FeatureNotice) { - this._notices = (this._notices ?: 0) or flags.fold(0) { acc, featureNotice -> acc or featureNotice.id } + val flags get() = _flags?.let { ConfigFlag.values().filter { flag -> it and flag.id != 0 } } ?: emptyList() + + fun addNotices(vararg values: FeatureNotice) { + this._notices = (this._notices ?: 0) or values.fold(0) { acc, featureNotice -> acc or featureNotice.id } + } + + fun addFlags(vararg values: ConfigFlag) { + this._flags = (this._flags ?: 0) or values.fold(0) { acc, flag -> acc or flag.id } } } @@ -55,12 +82,28 @@ data class PropertyKey<T>( ) { val parentKey by lazy { _parent() } + fun propertyOption(translation: LocaleWrapper, key: String): String { + if (key == "null") { + return translation[params.disabledKey ?: "manager.features.disabled"] + } + + return if (!params.flags.contains(ConfigFlag.NO_TRANSLATE)) + translation[params.customOptionTranslationPath?.let { + "$it.$key" + } ?: "features.options.${name}.$key"] + else key + } + + fun propertyName() = propertyTranslationPath() + ".name" + fun propertyDescription() = propertyTranslationPath() + ".description" + fun propertyTranslationPath(): String { - return if (parentKey != null) { - "${parentKey!!.propertyTranslationPath()}.properties.$name" - } else { - "features.properties.$name" + params.customTranslationPath?.let { + return it } + return parentKey?.let { + "${it.propertyTranslationPath()}.properties.$name" + } ?: "features.properties.$name" } } diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/core/config/impl/Camera.kt b/core/src/main/kotlin/me/rhunk/snapenhance/core/config/impl/Camera.kt @@ -1,6 +1,7 @@ package me.rhunk.snapenhance.core.config.impl import me.rhunk.snapenhance.core.config.ConfigContainer +import me.rhunk.snapenhance.core.config.ConfigFlag import me.rhunk.snapenhance.core.config.FeatureNotice import me.rhunk.snapenhance.features.impl.tweaks.CameraTweaks @@ -8,9 +9,9 @@ class Camera : ConfigContainer() { val disable = boolean("disable_camera") val immersiveCameraPreview = boolean("immersive_camera_preview") { addNotices(FeatureNotice.MAY_CAUSE_CRASHES) } val overridePreviewResolution = unique("override_preview_resolution", *CameraTweaks.resolutions.toTypedArray()) - { shouldTranslate = false } + { addFlags(ConfigFlag.NO_TRANSLATE) } val overridePictureResolution = unique("override_picture_resolution", *CameraTweaks.resolutions.toTypedArray()) - { shouldTranslate = false } + { addFlags(ConfigFlag.NO_TRANSLATE) } val forceHighestFrameRate = boolean("force_highest_frame_rate") { addNotices(FeatureNotice.MAY_BREAK_INTERNAL_BEHAVIOR) } val forceCameraSourceEncoding = boolean("force_camera_source_encoding") } diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/core/config/impl/DownloaderConfig.kt b/core/src/main/kotlin/me/rhunk/snapenhance/core/config/impl/DownloaderConfig.kt @@ -1,10 +1,11 @@ package me.rhunk.snapenhance.core.config.impl import me.rhunk.snapenhance.core.config.ConfigContainer +import me.rhunk.snapenhance.core.config.ConfigFlag import me.rhunk.snapenhance.core.config.FeatureNotice class DownloaderConfig : ConfigContainer() { - val saveFolder = string("save_folder") { isFolder = true } + val saveFolder = string("save_folder") { addFlags(ConfigFlag.FOLDER) } val autoDownloadOptions = multiple("auto_download_options", "friend_snaps", "friend_stories", diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/core/config/impl/Global.kt b/core/src/main/kotlin/me/rhunk/snapenhance/core/config/impl/Global.kt @@ -6,7 +6,7 @@ import me.rhunk.snapenhance.data.NotificationType class Global : ConfigContainer() { val snapchatPlus = boolean("snapchat_plus") { addNotices(FeatureNotice.MAY_BAN) } - val autoUpdater = unique("auto_updater", "EVERY_LAUNCH", "DAILY", "WEEKLY") + val autoUpdater = unique("auto_updater", "EVERY_LAUNCH", "DAILY", "WEEKLY").apply { set("DAILY") } val disableMetrics = boolean("disable_metrics") val blockAds = boolean("block_ads") val disableVideoLengthRestrictions = boolean("disable_video_length_restrictions") { addNotices(FeatureNotice.MAY_BAN) } diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/core/config/impl/RootConfig.kt b/core/src/main/kotlin/me/rhunk/snapenhance/core/config/impl/RootConfig.kt @@ -7,6 +7,7 @@ class RootConfig : ConfigContainer() { val userInterface = container("user_interface", UserInterfaceTweaks()) { icon = "RemoveRedEye"} val messaging = container("messaging", MessagingTweaks()) { icon = "Send" } val global = container("global", Global()) { icon = "MiscellaneousServices" } + val rules = container("rules", Rules()) { icon = "Rule" } val camera = container("camera", Camera()) { icon = "Camera"} val experimental = container("experimental", Experimental()) { icon = "Science" } val spoof = container("spoof", Spoof()) { icon = "Fingerprint" } diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/core/config/impl/Rules.kt b/core/src/main/kotlin/me/rhunk/snapenhance/core/config/impl/Rules.kt @@ -0,0 +1,26 @@ +package me.rhunk.snapenhance.core.config.impl + +import me.rhunk.snapenhance.core.config.ConfigContainer +import me.rhunk.snapenhance.core.config.PropertyValue +import me.rhunk.snapenhance.core.messaging.MessagingRuleType +import me.rhunk.snapenhance.core.messaging.RuleState + + +class Rules : ConfigContainer() { + private val rules = mutableMapOf<MessagingRuleType, PropertyValue<String>>() + + fun getRuleState(ruleType: MessagingRuleType): RuleState? { + return rules[ruleType]?.getNullable()?.let { RuleState.getByName(it) } + } + + init { + MessagingRuleType.values().filter { it.listMode }.forEach { ruleType -> + rules[ruleType] = unique(ruleType.key,"whitelist", "blacklist") { + customTranslationPath = "rules.properties.${ruleType.key}" + customOptionTranslationPath = "rules.modes" + }.apply { + set("whitelist") + } + } + } +}+ \ No newline at end of file diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/core/config/impl/UserInterfaceTweaks.kt b/core/src/main/kotlin/me/rhunk/snapenhance/core/config/impl/UserInterfaceTweaks.kt @@ -2,15 +2,22 @@ package me.rhunk.snapenhance.core.config.impl import me.rhunk.snapenhance.core.config.ConfigContainer import me.rhunk.snapenhance.core.config.FeatureNotice +import me.rhunk.snapenhance.core.messaging.MessagingRuleType class UserInterfaceTweaks : ConfigContainer() { val enableAppAppearance = boolean("enable_app_appearance") + val friendFeedMenuButtons = multiple( + "friend_feed_menu_buttons","conversation_info", *MessagingRuleType.values().toList().filter { it.listMode }.map { it.key }.toTypedArray() + ).apply { + set(mutableListOf("conversation_info", MessagingRuleType.STEALTH.key)) + } + val friendFeedMenuPosition = integer("friend_feed_menu_position", defaultValue = 1) val amoledDarkMode = boolean("amoled_dark_mode") { addNotices(FeatureNotice.UNSTABLE) } val mapFriendNameTags = boolean("map_friend_nametags") val streakExpirationInfo = boolean("streak_expiration_info") - val hideStorySections = multiple("hide_story_sections", "hide_friend_suggestions", "hide_friends", "hide_following", "hide_for_you") - val hideUiComponents = multiple( - "hide_ui_components", + val hideStorySections = multiple("hide_story_sections", + "hide_friend_suggestions", "hide_friends", "hide_following", "hide_for_you") + val hideUiComponents = multiple("hide_ui_components", "hide_voice_record_button", "hide_stickers_button", "hide_cognac_button", @@ -27,7 +34,4 @@ class UserInterfaceTweaks : ConfigContainer() { "ngs_search_icon_container" ) val storyViewerOverride = unique("story_viewer_override", "DISCOVER_PLAYBACK_SEEKBAR", "VERTICAL_STORY_VIEWER") { addNotices(FeatureNotice.UNSTABLE) } - val friendFeedMenuButtons = multiple("friend_feed_menu_buttons", "auto_download_blacklist", "anti_auto_save", "stealth_mode", "conversation_info") - val friendFeedMenuPosition = integer("friend_feed_menu_position", defaultValue = 1) - val enableFriendFeedMenuBar = boolean("enable_friend_feed_menu_bar") } diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/core/messaging/MessagingCoreObjects.kt b/core/src/main/kotlin/me/rhunk/snapenhance/core/messaging/MessagingCoreObjects.kt @@ -3,9 +3,15 @@ package me.rhunk.snapenhance.core.messaging import me.rhunk.snapenhance.util.SerializableDataObject -enum class Mode { - BLACKLIST, - WHITELIST +enum class RuleState( + val key: String +) { + BLACKLIST("blacklist"), + WHITELIST("whitelist"); + + companion object { + fun getByName(name: String) = values().first { it.key == name } + } } enum class SocialScope( @@ -18,11 +24,20 @@ enum class SocialScope( enum class MessagingRuleType( val key: String, - val socialScope: SocialScope, + val listMode: Boolean ) { - DOWNLOAD("download", SocialScope.FRIEND), - STEALTH("stealth", SocialScope.GROUP), - AUTO_SAVE("auto_save", SocialScope.GROUP); + AUTO_DOWNLOAD("auto_download", true), + STEALTH("stealth", true), + AUTO_SAVE("auto_save", true), + HIDE_CHAT_FEED("hide_chat_feed", false); + + fun translateOptionKey(optionKey: String): String { + return "rules.properties.${key}.options.${optionKey}" + } + + companion object { + fun getByName(name: String) = values().first { it.key == name } + } } data class FriendStreaks( @@ -50,8 +65,8 @@ data class MessagingFriendInfo( data class MessagingRule( val id: Int, + val type: MessagingRuleType, val socialScope: SocialScope, val targetUuid: String, //val mode: Mode?, - val subject: String ) : SerializableDataObject() \ No newline at end of file diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/database/DatabaseAccess.kt b/core/src/main/kotlin/me/rhunk/snapenhance/database/DatabaseAccess.kt @@ -86,6 +86,23 @@ class DatabaseAccess(private val context: ModContext) : Manager { } } + val myUserId by lazy { + safeDatabaseOperation(openArroyo()) { arroyoDatabase: SQLiteDatabase -> + val cursor = arroyoDatabase.rawQuery(buildString { + append("SELECT * FROM required_values WHERE key = 'USERID'") + }, null) + + if (!cursor.moveToFirst()) { + cursor.close() + return@safeDatabaseOperation null + } + + val userId = cursor.getString(cursor.getColumnIndex("value")) + cursor.close() + userId + }!! + } + fun getFeedEntryByConversationId(conversationId: String): FriendFeedEntry? { return safeDatabaseOperation(openMain()) { readDatabaseObject( @@ -157,7 +174,7 @@ class DatabaseAccess(private val context: ModContext) : Manager { } } - fun getDMConversationIdFromUserId(userId: String): UserConversationLink? { + fun getConversationLinkFromUserId(userId: String): UserConversationLink? { return safeDatabaseOperation(openArroyo()) { readDatabaseObject( UserConversationLink(), @@ -169,6 +186,22 @@ class DatabaseAccess(private val context: ModContext) : Manager { } } + fun getDMOtherParticipant(conversationId: String): String? { + return safeDatabaseOperation(openArroyo()) { cursor -> + val query = cursor.rawQuery( + "SELECT * FROM user_conversation WHERE client_conversation_id = ? AND conversation_type = 0", + arrayOf(conversationId) + ) + val participants = mutableListOf<String>() + while (query.moveToNext()) { + participants.add(query.getString(query.getColumnIndex("user_id"))) + } + query.close() + participants.firstOrNull { it != myUserId } + } + } + + fun getStoryEntryFromId(storyId: String): StoryEntry? { return safeDatabaseOperation(openMain()) { readDatabaseObject(StoryEntry(), it, "Story", "storyId = ?", arrayOf(storyId)) @@ -194,23 +227,6 @@ class DatabaseAccess(private val context: ModContext) : Manager { } } - fun getMyUserId(): String? { - return safeDatabaseOperation(openArroyo()) { arroyoDatabase: SQLiteDatabase -> - val cursor = arroyoDatabase.rawQuery(buildString { - append("SELECT * FROM required_values WHERE key = 'USERID'") - }, null) - - if (!cursor.moveToFirst()) { - cursor.close() - return@safeDatabaseOperation null - } - - val userId = cursor.getString(cursor.getColumnIndex("value")) - cursor.close() - userId - } - } - fun getMessagesFromConversationId( conversationId: String, limit: Int diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/features/MessagingRuleFeature.kt b/core/src/main/kotlin/me/rhunk/snapenhance/features/MessagingRuleFeature.kt @@ -0,0 +1,33 @@ +package me.rhunk.snapenhance.features + +import me.rhunk.snapenhance.core.messaging.MessagingRuleType +import me.rhunk.snapenhance.core.messaging.RuleState + +abstract class MessagingRuleFeature(name: String, val ruleType: MessagingRuleType, loadParams: Int = 0) : Feature(name, loadParams) { + init { + if (!ruleType.listMode) throw IllegalArgumentException("Rule type must be a list mode") + } + + fun getRuleState() = context.config.rules.getRuleState(ruleType) + + fun setState(conversationId: String, state: Boolean) { + context.bridgeClient.setRule( + context.database.getDMOtherParticipant(conversationId) ?: conversationId, + ruleType, + state + ) + } + + fun getState(conversationId: String) = + context.bridgeClient.getRules( + context.database.getDMOtherParticipant(conversationId) ?: conversationId + ).contains(ruleType) && getRuleState() != null + + fun canUseRule(conversationId: String): Boolean { + val state = getState(conversationId) + if (context.config.rules.getRuleState(ruleType) == RuleState.BLACKLIST) { + return !state + } + return state + } +}+ \ No newline at end of file diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/features/impl/Messaging.kt b/core/src/main/kotlin/me/rhunk/snapenhance/features/impl/Messaging.kt @@ -68,7 +68,7 @@ class Messaging : Feature("Messaging", loadParams = FeatureLoadParams.ACTIVITY_C arrayOf("activate", "deactivate", "processTypingActivity").forEach { hook -> Hooker.hook(context.classCache.presenceSession, hook, HookStage.BEFORE, { - hideBitmojiPresence || stealthMode.isStealth(openedConversationUUID.toString()) + hideBitmojiPresence || stealthMode.canUseRule(openedConversationUUID.toString()) }) { it.setResult(null) } @@ -86,7 +86,7 @@ class Messaging : Feature("Messaging", loadParams = FeatureLoadParams.ACTIVITY_C } Hooker.hook(context.classCache.conversationManager, "sendTypingNotification", HookStage.BEFORE, { - hideTypingNotification || stealthMode.isStealth(openedConversationUUID.toString()) + hideTypingNotification || stealthMode.canUseRule(openedConversationUUID.toString()) }) { it.setResult(null) } diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/features/impl/downloader/AntiAutoDownload.kt b/core/src/main/kotlin/me/rhunk/snapenhance/features/impl/downloader/AntiAutoDownload.kt @@ -1,19 +0,0 @@ -package me.rhunk.snapenhance.features.impl.downloader - -import me.rhunk.snapenhance.bridge.types.BridgeFileType -import me.rhunk.snapenhance.features.BridgeFileFeature -import me.rhunk.snapenhance.features.FeatureLoadParams - -class AntiAutoDownload : BridgeFileFeature("AntiAutoDownload", BridgeFileType.ANTI_AUTO_DOWNLOAD, loadParams = FeatureLoadParams.ACTIVITY_CREATE_SYNC) { - override fun onActivityCreate() { - readFile() - } - - fun setUserIgnored(userId: String, state: Boolean) { - setState(userId.hashCode().toLong().toString(16), state) - } - - fun isUserIgnored(userId: String): Boolean { - return exists(userId.hashCode().toLong().toString(16)) - } -}- \ No newline at end of file diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/features/impl/downloader/MediaDownloader.kt b/core/src/main/kotlin/me/rhunk/snapenhance/features/impl/downloader/MediaDownloader.kt @@ -9,6 +9,7 @@ import kotlinx.coroutines.runBlocking import me.rhunk.snapenhance.Logger import me.rhunk.snapenhance.Logger.xposedLog import me.rhunk.snapenhance.bridge.DownloadCallback +import me.rhunk.snapenhance.core.messaging.MessagingRuleType import me.rhunk.snapenhance.data.ContentType import me.rhunk.snapenhance.data.FileType import me.rhunk.snapenhance.data.wrapper.impl.media.MediaInfo @@ -24,8 +25,8 @@ import me.rhunk.snapenhance.download.data.InputMedia import me.rhunk.snapenhance.download.data.MediaFilter import me.rhunk.snapenhance.download.data.SplitMediaAssetType import me.rhunk.snapenhance.download.data.toKeyPair -import me.rhunk.snapenhance.features.Feature import me.rhunk.snapenhance.features.FeatureLoadParams +import me.rhunk.snapenhance.features.MessagingRuleFeature import me.rhunk.snapenhance.features.impl.Messaging import me.rhunk.snapenhance.features.impl.spying.MessageLogger import me.rhunk.snapenhance.hook.HookAdapter @@ -47,7 +48,7 @@ import kotlin.io.encoding.Base64 import kotlin.io.encoding.ExperimentalEncodingApi @OptIn(ExperimentalEncodingApi::class) -class MediaDownloader : Feature("MediaDownloader", loadParams = FeatureLoadParams.ACTIVITY_CREATE_ASYNC) { +class MediaDownloader : MessagingRuleFeature("MediaDownloader", MessagingRuleType.AUTO_DOWNLOAD, loadParams = FeatureLoadParams.ACTIVITY_CREATE_ASYNC) { private var lastSeenMediaInfoMap: MutableMap<SplitMediaAssetType, MediaInfo>? = null private var lastSeenMapParams: ParamMap? = null @@ -230,7 +231,7 @@ class MediaDownloader : Feature("MediaDownloader", loadParams = FeatureLoadParam val senderId = conversationMessage.senderId!! val conversationId = conversationMessage.clientConversationId!! - if (!forceDownload && context.feature(AntiAutoDownload::class).isUserIgnored(senderId)) { + if (!forceDownload && canUseRule(senderId)) { return } @@ -272,7 +273,7 @@ class MediaDownloader : Feature("MediaDownloader", loadParams = FeatureLoadParam val author = context.database.getFriendInfo( if (storyUserId == null || storyUserId == "null") - context.database.getMyUserId()!! + context.database.myUserId else storyUserId ) ?: throw Exception("Friend not found in database") val authorName = author.usernameForSorting!! diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/features/impl/spying/MessageLogger.kt b/core/src/main/kotlin/me/rhunk/snapenhance/features/impl/spying/MessageLogger.kt @@ -37,8 +37,6 @@ class MessageLogger : Feature("MessageLogger", private val fetchedMessages = mutableListOf<Long>() private val deletedMessageCache = mutableMapOf<Long, JsonObject>() - private val myUserId by lazy { context.database.getMyUserId() } - fun isMessageRemoved(conversationId: String, orderKey: Long) = deletedMessageCache.containsKey(computeMessageIdentifier(conversationId, orderKey)) fun deleteMessage(conversationId: String, clientMessageId: Long) { @@ -83,7 +81,7 @@ class MessageLogger : Feature("MessageLogger", if (message.messageState != MessageState.COMMITTED) return //exclude messages sent by me - if (message.senderId.toString() == myUserId) return + if (message.senderId.toString() == context.database.myUserId) return val conversationId = message.messageDescriptor.conversationId.toString() val serverIdentifier = computeMessageIdentifier(conversationId, message.orderKey) diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/features/impl/spying/PreventReadReceipts.kt b/core/src/main/kotlin/me/rhunk/snapenhance/features/impl/spying/PreventReadReceipts.kt @@ -12,7 +12,7 @@ class PreventReadReceipts : Feature("PreventReadReceipts", loadParams = FeatureL val preventReadReceipts by context.config.messaging.preventReadReceipts val isConversationInStealthMode: (SnapUUID) -> Boolean = hook@{ if (preventReadReceipts) return@hook true - context.feature(StealthMode::class).isStealth(it.toString()) + context.feature(StealthMode::class).canUseRule(it.toString()) } arrayOf("mediaMessagesDisplayed", "displayedMessages").forEach { methodName: String -> diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/features/impl/spying/StealthMode.kt b/core/src/main/kotlin/me/rhunk/snapenhance/features/impl/spying/StealthMode.kt @@ -1,19 +1,6 @@ package me.rhunk.snapenhance.features.impl.spying -import me.rhunk.snapenhance.bridge.types.BridgeFileType -import me.rhunk.snapenhance.features.BridgeFileFeature -import me.rhunk.snapenhance.features.FeatureLoadParams +import me.rhunk.snapenhance.core.messaging.MessagingRuleType +import me.rhunk.snapenhance.features.MessagingRuleFeature -class StealthMode : BridgeFileFeature("StealthMode", BridgeFileType.STEALTH, loadParams = FeatureLoadParams.ACTIVITY_CREATE_SYNC) { - override fun onActivityCreate() { - readFile() - } - - fun setStealth(conversationId: String, stealth: Boolean) { - setState(conversationId.hashCode().toLong().toString(16), stealth) - } - - fun isStealth(conversationId: String): Boolean { - return exists(conversationId.hashCode().toLong().toString(16)) - } -} +class StealthMode : MessagingRuleFeature("StealthMode", MessagingRuleType.STEALTH)+ \ No newline at end of file diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/features/impl/tweaks/AutoSave.kt b/core/src/main/kotlin/me/rhunk/snapenhance/features/impl/tweaks/AutoSave.kt @@ -1,11 +1,12 @@ package me.rhunk.snapenhance.features.impl.tweaks import me.rhunk.snapenhance.Logger +import me.rhunk.snapenhance.core.messaging.MessagingRuleType import me.rhunk.snapenhance.data.MessageState import me.rhunk.snapenhance.data.wrapper.impl.Message import me.rhunk.snapenhance.data.wrapper.impl.SnapUUID -import me.rhunk.snapenhance.features.Feature import me.rhunk.snapenhance.features.FeatureLoadParams +import me.rhunk.snapenhance.features.MessagingRuleFeature import me.rhunk.snapenhance.features.impl.Messaging import me.rhunk.snapenhance.features.impl.spying.MessageLogger import me.rhunk.snapenhance.features.impl.spying.StealthMode @@ -15,14 +16,12 @@ import me.rhunk.snapenhance.util.CallbackBuilder import me.rhunk.snapenhance.util.ktx.getObjectField import java.util.concurrent.Executors -class AutoSave : Feature("Auto Save", loadParams = FeatureLoadParams.ACTIVITY_CREATE_ASYNC) { +class AutoSave : MessagingRuleFeature("Auto Save", MessagingRuleType.AUTO_SAVE, loadParams = FeatureLoadParams.ACTIVITY_CREATE_ASYNC) { private val asyncSaveExecutorService = Executors.newSingleThreadExecutor() private val messageLogger by lazy { context.feature(MessageLogger::class) } private val messaging by lazy { context.feature(Messaging::class) } - private val myUserId by lazy { context.database.getMyUserId() } - private val fetchConversationWithMessagesCallbackClass by lazy { context.mappings.getMappedClass("callbacks", "FetchConversationWithMessagesCallback") } private val callbackClass by lazy { context.mappings.getMappedClass("callbacks", "Callback") } @@ -62,7 +61,7 @@ class AutoSave : Feature("Auto Save", loadParams = FeatureLoadParams.ACTIVITY_CR } private fun canSaveMessage(message: Message): Boolean { - if (message.messageMetadata.savedBy.any { uuid -> uuid.toString() == myUserId }) return false + if (message.messageMetadata.savedBy.any { uuid -> uuid.toString() == context.database.myUserId }) return false val contentType = message.messageContent.contentType.toString() return autoSaveFilter.any { it == contentType } @@ -74,8 +73,8 @@ class AutoSave : Feature("Auto Save", loadParams = FeatureLoadParams.ACTIVITY_CR with(context.feature(Messaging::class)) { if (openedConversationUUID == null) return@canSave false val conversation = openedConversationUUID.toString() - if (context.feature(StealthMode::class).isStealth(conversation)) return@canSave false - if (context.feature(AntiAutoSave::class).isConversationIgnored(conversation)) return@canSave false + if (context.feature(StealthMode::class).canUseRule(conversation)) return@canSave false + if (canUseRule(conversation)) return@canSave false } return true } diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/features/impl/tweaks/Notifications.kt b/core/src/main/kotlin/me/rhunk/snapenhance/features/impl/tweaks/Notifications.kt @@ -154,7 +154,7 @@ class Notifications : Feature("Notifications", loadParams = FeatureLoadParams.IN val input = RemoteInput.getResultsFromIntent(intent).getCharSequence("chat_reply_input") .toString() - context.database.getMyUserId()?.let { context.database.getFriendInfo(it) }?.let { myUser -> + context.database.myUserId.let { context.database.getFriendInfo(it) }?.let { myUser -> cachedMessages.computeIfAbsent(conversationId) { mutableListOf() }.add("${myUser.displayName}: $input") updateNotification(notificationId) { notification -> diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/manager/impl/FeatureManager.kt b/core/src/main/kotlin/me/rhunk/snapenhance/manager/impl/FeatureManager.kt @@ -7,7 +7,6 @@ import me.rhunk.snapenhance.features.FeatureLoadParams import me.rhunk.snapenhance.features.impl.AutoUpdater import me.rhunk.snapenhance.features.impl.ConfigurationOverride import me.rhunk.snapenhance.features.impl.Messaging -import me.rhunk.snapenhance.features.impl.downloader.AntiAutoDownload import me.rhunk.snapenhance.features.impl.downloader.MediaDownloader import me.rhunk.snapenhance.features.impl.experiments.AmoledDarkMode import me.rhunk.snapenhance.features.impl.experiments.AppPasscode @@ -76,7 +75,6 @@ class FeatureManager(private val context: ModContext) : Manager { register(AutoSave::class) register(UITweaks::class) register(ConfigurationOverride::class) - register(AntiAutoDownload::class) register(GalleryMediaSendOverride::class) register(AntiAutoSave::class) register(UnlimitedSnapViewTime::class) diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/ui/menu/impl/FriendFeedInfoMenu.kt b/core/src/main/kotlin/me/rhunk/snapenhance/ui/menu/impl/FriendFeedInfoMenu.kt @@ -4,32 +4,23 @@ import android.annotation.SuppressLint import android.content.Context import android.content.DialogInterface import android.content.res.Resources -import android.graphics.Bitmap import android.graphics.BitmapFactory -import android.graphics.Canvas -import android.graphics.Color -import android.graphics.Paint import android.graphics.drawable.BitmapDrawable import android.graphics.drawable.Drawable -import android.view.Gravity -import android.view.MotionEvent import android.view.View -import android.view.ViewGroup import android.widget.Button import android.widget.CompoundButton -import android.widget.LinearLayout import android.widget.Switch -import android.widget.Toast import me.rhunk.snapenhance.Logger import me.rhunk.snapenhance.data.ContentType -import me.rhunk.snapenhance.data.wrapper.impl.FriendActionButton import me.rhunk.snapenhance.database.objects.ConversationMessage import me.rhunk.snapenhance.database.objects.FriendInfo import me.rhunk.snapenhance.database.objects.UserConversationLink +import me.rhunk.snapenhance.features.MessagingRuleFeature import me.rhunk.snapenhance.features.impl.Messaging -import me.rhunk.snapenhance.features.impl.downloader.AntiAutoDownload +import me.rhunk.snapenhance.features.impl.downloader.MediaDownloader import me.rhunk.snapenhance.features.impl.spying.StealthMode -import me.rhunk.snapenhance.features.impl.tweaks.AntiAutoSave +import me.rhunk.snapenhance.features.impl.tweaks.AutoSave import me.rhunk.snapenhance.ui.ViewAppearanceHelper import me.rhunk.snapenhance.ui.menu.AbstractMenu import me.rhunk.snapenhance.util.snap.BitmojiSelfie @@ -106,17 +97,18 @@ class FriendFeedInfoMenu : AbstractMenu() { context.config.messaging.messagePreviewLength.get() )?.reversed() - if (messages.isNullOrEmpty()) { - Toast.makeText(androidCtx, "No messages found", Toast.LENGTH_SHORT).show() + if (messages == null) { + context.longToast("Can't fetch messages") return } + val participants: Map<String, FriendInfo> = context.database.getConversationParticipants(conversationId)!! .map { context.database.getFriendInfo(it)!! } .associateBy { it.userId!! } val messageBuilder = StringBuilder() - messages.forEach{ message: ConversationMessage -> + messages.forEach { message -> val sender: FriendInfo? = participants[message.senderId] var messageString: String = message.getMessageAsString() ?: ContentType.fromId(message.contentType).name @@ -164,7 +156,6 @@ class FriendFeedInfoMenu : AbstractMenu() { .append("\n") } - //alert dialog val builder = ViewAppearanceHelper.newAlertDialogBuilder(context.mainActivity) builder.setTitle(context.translation["conversation_preview.title"]) @@ -182,22 +173,6 @@ class FriendFeedInfoMenu : AbstractMenu() { builder.show() } - private fun createEmojiDrawable(text: String, width: Int, height: Int, textSize: Float, disabled: Boolean = false): Drawable { - val bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888) - val canvas = Canvas(bitmap) - val paint = Paint() - paint.textSize = textSize - paint.color = Color.BLACK - paint.textAlign = Paint.Align.CENTER - canvas.drawText(text, width / 2f, height.toFloat() - paint.descent(), paint) - if (disabled) { - paint.color = Color.RED - paint.strokeWidth = 5f - canvas.drawLine(0f, 0f, width.toFloat(), height.toFloat(), paint) - } - return BitmapDrawable(context.resources, bitmap) - } - private fun getCurrentConversationInfo(): Pair<String, String?> { val messaging = context.feature(Messaging::class) val focusedConversationTargetUser: String? = messaging.lastFetchConversationUserUUID?.toString() @@ -213,7 +188,7 @@ class FriendFeedInfoMenu : AbstractMenu() { //old conversation fetch val conversationId = if (messaging.lastFetchConversationUUID == null && focusedConversationTargetUser != null) { - val conversation: UserConversationLink = context.database.getDMConversationIdFromUserId(focusedConversationTargetUser) ?: throw IllegalStateException("No conversation found") + val conversation: UserConversationLink = context.database.getConversationLinkFromUserId(focusedConversationTargetUser) ?: throw IllegalStateException("No conversation found") conversation.clientConversationId!!.trim().lowercase() } else { messaging.lastFetchConversationUUID.toString() @@ -244,148 +219,38 @@ class FriendFeedInfoMenu : AbstractMenu() { val (conversationId, targetUser) = getCurrentConversationInfo() - if (!context.config.userInterface.enableFriendFeedMenuBar.get()) { - //preview button - val previewButton = Button(viewModel.context).apply { - text = modContext.translation["friend_menu_option.preview"] - ViewAppearanceHelper.applyTheme(this, viewModel.width) - setOnClickListener { - showPreview( - targetUser, - conversationId, - context - ) - } - } - - //stealth switch - val stealthSwitch = Switch(viewModel.context).apply { - text = modContext.translation["friend_menu_option.stealth_mode"] - isChecked = modContext.feature(StealthMode::class).isStealth(conversationId) - ViewAppearanceHelper.applyTheme(this) - setOnCheckedChangeListener { _: CompoundButton?, isChecked: Boolean -> - modContext.feature(StealthMode::class).setStealth( - conversationId, - isChecked - ) - } - } - - if (friendFeedMenuOptions.contains("anti_auto_save")) { - createToggleFeature(viewConsumer, - "friend_menu_option.anti_auto_save", - { context.feature(AntiAutoSave::class).isConversationIgnored(conversationId) }, - { context.feature(AntiAutoSave::class).setConversationIgnored(conversationId, it) } - ) - } - - run { - val userId = context.database.getFeedEntryByConversationId(conversationId)?.friendUserId ?: return@run - if (friendFeedMenuOptions.contains("auto_download_blacklist")) { - createToggleFeature(viewConsumer, - "friend_menu_option.auto_download_blacklist", - { context.feature(AntiAutoDownload::class).isUserIgnored(userId) }, - { context.feature(AntiAutoDownload::class).setUserIgnored(userId, it) } - ) - } - } - - if (friendFeedMenuOptions.contains("stealth_mode")) { - viewConsumer(stealthSwitch) - } - if (friendFeedMenuOptions.contains("conversation_info")) { - viewConsumer(previewButton) - } - return - } - - val menuButtonBar = LinearLayout(viewModel.context).apply { - layoutParams = ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT) - orientation = LinearLayout.HORIZONTAL - gravity = Gravity.CENTER - } - - fun createActionButton(icon: String, isDisabled: Boolean? = null, onClick: (Boolean) -> Unit) { - //FIXME: hardcoded values - menuButtonBar.addView(LinearLayout(viewModel.context).apply { - layoutParams = LinearLayout.LayoutParams(0, ViewGroup.LayoutParams.MATCH_PARENT, 1f) - gravity = Gravity.CENTER - isClickable = false - - var isLineThrough = isDisabled ?: false - FriendActionButton.new(viewModel.context).apply { - fun setLineThrough(value: Boolean) { - setIconDrawable(createEmojiDrawable(icon, 60, 60, 50f, if (isDisabled == null) false else value)) - } - setLineThrough(isLineThrough) - (instanceNonNull() as View).apply { - layoutParams = LinearLayout.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT).apply { - setMargins(0, 40, 0, 40) - } - setOnTouchListener { _, event -> - if (event.action == MotionEvent.ACTION_UP) { - isLineThrough = !isLineThrough - onClick(isLineThrough) - setLineThrough(isLineThrough) - } - false - } - } - - }.also { addView(it.instanceNonNull() as View) } - - }) - } - - if (friendFeedMenuOptions.contains("auto_download_blacklist")) { - run { - val userId = - context.database.getFeedEntryByConversationId(conversationId)?.friendUserId - ?: return@run - createActionButton( - "\u2B07\uFE0F", - isDisabled = !context.feature(AntiAutoDownload::class).isUserIgnored(userId) - ) { - context.feature(AntiAutoDownload::class).setUserIgnored(userId, !it) - } - } - } - - if (friendFeedMenuOptions.contains("anti_auto_save")) { - //diskette - createActionButton("\uD83D\uDCAC", - isDisabled = !context.feature(AntiAutoSave::class) - .isConversationIgnored(conversationId) - ) { - context.feature(AntiAutoSave::class).setConversationIgnored(conversationId, !it) - } - } - - - if (friendFeedMenuOptions.contains("stealth_mode")) { - //eyes - createActionButton( - "\uD83D\uDC7B", - isDisabled = !context.feature(StealthMode::class).isStealth(conversationId) - ) { isChecked -> - context.feature(StealthMode::class).setStealth( + val previewButton = Button(viewModel.context).apply { + text = modContext.translation["friend_menu_option.preview"] + ViewAppearanceHelper.applyTheme(this, viewModel.width) + setOnClickListener { + showPreview( + targetUser, conversationId, - !isChecked + context ) } } if (friendFeedMenuOptions.contains("conversation_info")) { - //user - createActionButton("\uD83D\uDC64") { - showPreview( - targetUser, - conversationId, - viewModel.context - ) - } + viewConsumer(previewButton) } - viewConsumer(menuButtonBar) + val rules: Array<MessagingRuleFeature> = arrayOf( + StealthMode::class, + AutoSave::class, + MediaDownloader::class + ).map { modContext.feature(it) }.toTypedArray() + + rules.forEach { ruleFeature -> + if (!friendFeedMenuOptions.contains(ruleFeature.ruleType.key)) return@forEach + Logger.debug("${ruleFeature.ruleType.key} ${ruleFeature.getRuleState()}") + + val ruleState = ruleFeature.getRuleState() ?: return@forEach + createToggleFeature(viewConsumer, + ruleFeature.ruleType.translateOptionKey(ruleState.key), + { ruleFeature.getState(conversationId) }, + { ruleFeature.setState(conversationId, it) } + ) + } } } \ No newline at end of file