mirror of
https://github.com/rhunk/SnapEnhance.git
synced 2025-06-12 05:07:46 +02:00
feat: rule system
This commit is contained in:
@ -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
|
||||
|
@ -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",
|
||||
|
@ -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)
|
||||
}
|
||||
|
@ -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))
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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 propertyTranslationPath(): String {
|
||||
return if (parentKey != null) {
|
||||
"${parentKey!!.propertyTranslationPath()}.properties.$name"
|
||||
} else {
|
||||
"features.properties.$name"
|
||||
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 {
|
||||
params.customTranslationPath?.let {
|
||||
return it
|
||||
}
|
||||
return parentKey?.let {
|
||||
"${it.propertyTranslationPath()}.properties.$name"
|
||||
} ?: "features.properties.$name"
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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")
|
||||
}
|
||||
|
@ -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",
|
||||
|
@ -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) }
|
||||
|
@ -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" }
|
||||
|
@ -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")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -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")
|
||||
}
|
||||
|
@ -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()
|
@ -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
|
||||
|
@ -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
|
||||
}
|
||||
}
|
@ -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)
|
||||
}
|
||||
|
@ -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))
|
||||
}
|
||||
}
|
@ -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!!
|
||||
|
@ -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)
|
||||
|
@ -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 ->
|
||||
|
@ -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)
|
@ -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
|
||||
}
|
||||
|
@ -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 ->
|
||||
|
@ -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)
|
||||
|
@ -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) }
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user