commit 72c9b92a3e07d6b60832204aac661c989fe841df parent 37becec35047bee5e4493cd03ea26fbac3263492 Author: rhunk <101876869+rhunk@users.noreply.github.com> Date: Mon, 25 Dec 2023 23:10:31 +0100 feat(scripting): integrated ui Diffstat:
22 files changed, 647 insertions(+), 581 deletions(-)
diff --git a/app/src/main/kotlin/me/rhunk/snapenhance/scripting/RemoteScriptManager.kt b/app/src/main/kotlin/me/rhunk/snapenhance/scripting/RemoteScriptManager.kt @@ -14,7 +14,6 @@ import me.rhunk.snapenhance.common.scripting.type.ModuleInfo import me.rhunk.snapenhance.scripting.impl.IPCListeners import me.rhunk.snapenhance.scripting.impl.ManagerIPC import me.rhunk.snapenhance.scripting.impl.ManagerScriptConfig -import me.rhunk.snapenhance.scripting.impl.ui.InterfaceManager import java.io.File import java.io.InputStream import kotlin.system.exitProcess @@ -67,7 +66,6 @@ class RemoteScriptManager( putConst("currentSide", this, BindingSide.MANAGER.key) module.registerBindings( ManagerIPC(ipcListeners), - InterfaceManager(), ManagerScriptConfig(this@RemoteScriptManager) ) } diff --git a/app/src/main/kotlin/me/rhunk/snapenhance/scripting/impl/ui/InterfaceManager.kt b/app/src/main/kotlin/me/rhunk/snapenhance/scripting/impl/ui/InterfaceManager.kt @@ -1,103 +0,0 @@ -package me.rhunk.snapenhance.scripting.impl.ui - -import me.rhunk.snapenhance.common.scripting.bindings.AbstractBinding -import me.rhunk.snapenhance.common.scripting.bindings.BindingSide -import me.rhunk.snapenhance.common.scripting.ktx.contextScope -import me.rhunk.snapenhance.scripting.impl.ui.components.Node -import me.rhunk.snapenhance.scripting.impl.ui.components.NodeType -import me.rhunk.snapenhance.scripting.impl.ui.components.impl.ActionNode -import me.rhunk.snapenhance.scripting.impl.ui.components.impl.ActionType -import me.rhunk.snapenhance.scripting.impl.ui.components.impl.RowColumnNode -import org.mozilla.javascript.Function -import org.mozilla.javascript.annotations.JSFunction - - -class InterfaceBuilder { - val nodes = mutableListOf<Node>() - var onDisposeCallback: (() -> Unit)? = null - - private fun createNode(type: NodeType, block: Node.() -> Unit): Node { - return Node(type).apply(block).also { nodes.add(it) } - } - - fun onDispose(block: () -> Unit) { - nodes.add(ActionNode(ActionType.DISPOSE, callback = block)) - } - - fun onLaunched(block: () -> Unit) { - onLaunched(Unit, block) - } - - fun onLaunched(key: Any, block: () -> Unit) { - nodes.add(ActionNode(ActionType.LAUNCHED, key, block)) - } - - fun row(block: (InterfaceBuilder) -> Unit) = RowColumnNode(NodeType.ROW).apply { - children.addAll(InterfaceBuilder().apply(block).nodes) - }.also { nodes.add(it) } - - fun column(block: (InterfaceBuilder) -> Unit) = RowColumnNode(NodeType.COLUMN).apply { - children.addAll(InterfaceBuilder().apply(block).nodes) - }.also { nodes.add(it) } - - fun text(text: String) = createNode(NodeType.TEXT) { - label(text) - } - - fun switch(state: Boolean?, callback: (Boolean) -> Unit) = createNode(NodeType.SWITCH) { - attributes["state"] = state - attributes["callback"] = callback - } - - fun button(label: String, callback: () -> Unit) = createNode(NodeType.BUTTON) { - label(label) - attributes["callback"] = callback - } - - fun slider(min: Int, max: Int, step: Int, value: Int, callback: (Int) -> Unit) = createNode( - NodeType.SLIDER - ) { - attributes["value"] = value - attributes["min"] = min - attributes["max"] = max - attributes["step"] = step - attributes["callback"] = callback - } - - fun list(label: String, items: List<String>, callback: (String) -> Unit) = createNode(NodeType.LIST) { - label(label) - attributes["items"] = items - attributes["callback"] = callback - } -} - - - -class InterfaceManager : AbstractBinding("interface-manager", BindingSide.MANAGER) { - private val interfaces = mutableMapOf<String, () -> InterfaceBuilder?>() - - fun buildInterface(name: String): InterfaceBuilder? { - return interfaces[name]?.invoke() - } - - override fun onDispose() { - interfaces.clear() - } - - @Suppress("unused") - @JSFunction fun create(name: String, callback: Function) { - interfaces[name] = { - val interfaceBuilder = InterfaceBuilder() - runCatching { - contextScope { - callback.call(this, callback, callback, arrayOf(interfaceBuilder)) - } - interfaceBuilder - }.onFailure { - context.runtime.logger.error("Failed to create interface $name for ${context.moduleInfo.name}", it) - }.getOrNull() - } - } - - override fun getObject() = this -}- \ No newline at end of file diff --git a/app/src/main/kotlin/me/rhunk/snapenhance/scripting/impl/ui/components/Node.kt b/app/src/main/kotlin/me/rhunk/snapenhance/scripting/impl/ui/components/Node.kt @@ -1,52 +0,0 @@ -package me.rhunk.snapenhance.scripting.impl.ui.components - -open class Node( - val type: NodeType, -) { - lateinit var uiChangeDetection: (key: String, value: Any?) -> Unit - - val children = mutableListOf<Node>() - val attributes = object: HashMap<String, Any?>() { - override fun put(key: String, value: Any?): Any? { - return super.put(key, value).also { - if (::uiChangeDetection.isInitialized) { - uiChangeDetection(key, value) - } - } - } - } - - fun setAttribute(key: String, value: Any?) { - attributes[key] = value - } - - fun fillMaxWidth(): Node { - attributes["fillMaxWidth"] = true - return this - } - - fun fillMaxHeight(): Node { - attributes["fillMaxHeight"] = true - return this - } - - fun label(text: String): Node { - attributes["label"] = text - return this - } - - fun padding(padding: Int): Node { - attributes["padding"] = padding - return this - } - - fun fontSize(size: Int): Node { - attributes["fontSize"] = size - return this - } - - fun color(color: Long): Node { - attributes["color"] = color - return this - } -} diff --git a/app/src/main/kotlin/me/rhunk/snapenhance/scripting/impl/ui/components/NodeType.kt b/app/src/main/kotlin/me/rhunk/snapenhance/scripting/impl/ui/components/NodeType.kt @@ -1,11 +0,0 @@ -package me.rhunk.snapenhance.scripting.impl.ui.components -enum class NodeType { - ROW, - COLUMN, - TEXT, - SWITCH, - BUTTON, - SLIDER, - LIST, - ACTION -}- \ No newline at end of file diff --git a/app/src/main/kotlin/me/rhunk/snapenhance/scripting/impl/ui/components/impl/ActionNode.kt b/app/src/main/kotlin/me/rhunk/snapenhance/scripting/impl/ui/components/impl/ActionNode.kt @@ -1,15 +0,0 @@ -package me.rhunk.snapenhance.scripting.impl.ui.components.impl - -import me.rhunk.snapenhance.scripting.impl.ui.components.Node -import me.rhunk.snapenhance.scripting.impl.ui.components.NodeType - -enum class ActionType { - LAUNCHED, - DISPOSE -} - -class ActionNode( - val actionType: ActionType, - val key: Any = Unit, - val callback: () -> Unit -): Node(NodeType.ACTION)- \ No newline at end of file diff --git a/app/src/main/kotlin/me/rhunk/snapenhance/scripting/impl/ui/components/impl/RowColumnNode.kt b/app/src/main/kotlin/me/rhunk/snapenhance/scripting/impl/ui/components/impl/RowColumnNode.kt @@ -1,47 +0,0 @@ -package me.rhunk.snapenhance.scripting.impl.ui.components.impl - -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.ui.Alignment -import me.rhunk.snapenhance.scripting.impl.ui.components.Node -import me.rhunk.snapenhance.scripting.impl.ui.components.NodeType - - -class RowColumnNode( - type: NodeType, -) : Node(type) { - companion object { - private val arrangements = mapOf( - "start" to Arrangement.Start, - "end" to Arrangement.End, - "top" to Arrangement.Top, - "bottom" to Arrangement.Bottom, - "center" to Arrangement.Center, - "spaceBetween" to Arrangement.SpaceBetween, - "spaceAround" to Arrangement.SpaceAround, - "spaceEvenly" to Arrangement.SpaceEvenly, - ) - private val alignments = mapOf( - "start" to Alignment.Start, - "end" to Alignment.End, - "top" to Alignment.Top, - "bottom" to Alignment.Bottom, - "centerVertically" to Alignment.CenterVertically, - "centerHorizontally" to Alignment.CenterHorizontally, - ) - } - - fun arrangement(arrangement: String): RowColumnNode { - attributes["arrangement"] = arrangements[arrangement] ?: throw IllegalArgumentException("Invalid arrangement") - return this - } - - fun alignment(alignment: String): RowColumnNode { - attributes["alignment"] = alignments[alignment] ?: throw IllegalArgumentException("Invalid alignment") - return this - } - - fun spacedBy(spacing: Int): RowColumnNode { - attributes["spacing"] = spacing - return this - } -}- \ No newline at end of file diff --git a/app/src/main/kotlin/me/rhunk/snapenhance/ui/manager/sections/scripting/ScriptInterface.kt b/app/src/main/kotlin/me/rhunk/snapenhance/ui/manager/sections/scripting/ScriptInterface.kt @@ -1,182 +0,0 @@ -package me.rhunk.snapenhance.ui.manager.sections.scripting - -import androidx.compose.foundation.layout.* -import androidx.compose.material3.OutlinedButton -import androidx.compose.material3.Slider -import androidx.compose.material3.Switch -import androidx.compose.material3.Text -import androidx.compose.runtime.* -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.graphics.Color -import androidx.compose.ui.unit.dp -import androidx.compose.ui.unit.sp -import kotlinx.coroutines.launch -import me.rhunk.snapenhance.common.logger.AbstractLogger -import me.rhunk.snapenhance.scripting.impl.ui.InterfaceBuilder -import me.rhunk.snapenhance.scripting.impl.ui.components.Node -import me.rhunk.snapenhance.scripting.impl.ui.components.NodeType -import me.rhunk.snapenhance.scripting.impl.ui.components.impl.ActionNode -import me.rhunk.snapenhance.scripting.impl.ui.components.impl.ActionType -import kotlin.math.abs - - -@Composable -@Suppress("UNCHECKED_CAST") -private fun DrawNode(node: Node) { - val coroutineScope = rememberCoroutineScope() - val cachedAttributes = remember { mutableStateMapOf(*node.attributes.toList().toTypedArray()) } - - node.uiChangeDetection = { key, value -> - coroutineScope.launch { - cachedAttributes[key] = value - } - } - - DisposableEffect(Unit) { - onDispose { - node.uiChangeDetection = { _, _ -> } - } - } - - val arrangement = cachedAttributes["arrangement"] - val alignment = cachedAttributes["alignment"] - val spacing = cachedAttributes["spacing"]?.toString()?.toInt()?.let { abs(it) } - - val rowColumnModifier = Modifier - .then(if (cachedAttributes["fillMaxWidth"] as? Boolean == true) Modifier.fillMaxWidth() else Modifier) - .then(if (cachedAttributes["fillMaxHeight"] as? Boolean == true) Modifier.fillMaxHeight() else Modifier) - .padding( - (cachedAttributes["padding"] - ?.toString() - ?.toInt() - ?.let { abs(it) } ?: 2).dp) - - fun runCallbackSafe(callback: () -> Unit) { - runCatching { - callback() - }.onFailure { - AbstractLogger.directError("Error running callback", it) - } - } - - @Composable - fun NodeLabel() { - Text( - text = cachedAttributes["label"] as String, - fontSize = (cachedAttributes["fontSize"]?.toString()?.toInt() ?: 14).sp, - color = (cachedAttributes["color"] as? Long)?.let { Color(it) } ?: Color.Unspecified - ) - } - - when (node.type) { - NodeType.ACTION -> { - when ((node as ActionNode).actionType) { - ActionType.LAUNCHED -> { - LaunchedEffect(node.key) { - runCallbackSafe { - node.callback() - } - } - } - ActionType.DISPOSE -> { - DisposableEffect(Unit) { - onDispose { - runCallbackSafe { - node.callback() - } - } - } - } - } - } - NodeType.COLUMN -> { - Column( - verticalArrangement = arrangement as? Arrangement.Vertical ?: spacing?.let { Arrangement.spacedBy(it.dp) } ?: Arrangement.Top, - horizontalAlignment = alignment as? Alignment.Horizontal ?: Alignment.Start, - modifier = rowColumnModifier - ) { - node.children.forEach { child -> - DrawNode(child) - } - } - } - NodeType.ROW -> { - Row( - horizontalArrangement = arrangement as? Arrangement.Horizontal ?: spacing?.let { Arrangement.spacedBy(it.dp) } ?: Arrangement.SpaceBetween, - verticalAlignment = alignment as? Alignment.Vertical ?: Alignment.CenterVertically, - modifier = rowColumnModifier - ) { - node.children.forEach { child -> - DrawNode(child) - } - } - } - NodeType.TEXT -> NodeLabel() - NodeType.SWITCH -> { - var switchState by remember { - mutableStateOf(cachedAttributes["state"] as Boolean) - } - Switch( - checked = switchState, - onCheckedChange = { state -> - runCallbackSafe { - switchState = state - node.setAttribute("state", state) - (cachedAttributes["callback"] as? (Boolean) -> Unit)?.let { it(state) } - } - } - ) - } - NodeType.SLIDER -> { - var sliderValue by remember { - mutableFloatStateOf((cachedAttributes["value"] as Int).toFloat()) - } - Slider( - value = sliderValue, - onValueChange = { value -> - runCallbackSafe { - sliderValue = value - node.setAttribute("value", value.toInt()) - (cachedAttributes["callback"] as? (Int) -> Unit)?.let { it(value.toInt()) } - } - }, - valueRange = (cachedAttributes["min"] as Int).toFloat()..(cachedAttributes["max"] as Int).toFloat(), - steps = cachedAttributes["step"] as Int, - ) - } - NodeType.BUTTON -> { - OutlinedButton(onClick = { - runCallbackSafe { - (cachedAttributes["callback"] as? () -> Unit)?.let { it() } - } - }) { - NodeLabel() - } - } - else -> {} - } -} - -@Composable -fun ScriptInterface(interfaceBuilder: InterfaceBuilder) { - Column( - modifier = Modifier - .fillMaxWidth() - .padding(8.dp) - ) { - interfaceBuilder.nodes.forEach { node -> - DrawNode(node) - } - - DisposableEffect(Unit) { - onDispose { - runCatching { - interfaceBuilder.onDisposeCallback?.invoke() - }.onFailure { - AbstractLogger.directError("Error running onDisposed callback", it) - } - } - } - } -}- \ No newline at end of file diff --git a/app/src/main/kotlin/me/rhunk/snapenhance/ui/manager/sections/scripting/ScriptsSection.kt b/app/src/main/kotlin/me/rhunk/snapenhance/ui/manager/sections/scripting/ScriptsSection.kt @@ -20,7 +20,9 @@ import androidx.documentfile.provider.DocumentFile import kotlinx.coroutines.delay import kotlinx.coroutines.launch import me.rhunk.snapenhance.common.scripting.type.ModuleInfo -import me.rhunk.snapenhance.scripting.impl.ui.InterfaceManager +import me.rhunk.snapenhance.common.scripting.ui.EnumScriptInterface +import me.rhunk.snapenhance.common.scripting.ui.InterfaceManager +import me.rhunk.snapenhance.common.scripting.ui.ScriptInterface import me.rhunk.snapenhance.ui.manager.Section import me.rhunk.snapenhance.ui.util.ActivityLauncherHelper import me.rhunk.snapenhance.ui.util.chooseFolder @@ -140,22 +142,14 @@ class ScriptsSection : Section() { @Composable fun ScriptSettings(script: ModuleInfo) { - var settingsError by remember { - mutableStateOf(null as Throwable?) - } - val settingsInterface = remember { val module = context.scriptManager.runtime.getModuleByName(script.name) ?: return@remember null - runCatching { - (module.getBinding(InterfaceManager::class))?.buildInterface("settings") - }.onFailure { - settingsError = it - }.getOrNull() + (module.getBinding(InterfaceManager::class))?.buildInterface(EnumScriptInterface.SETTINGS) } if (settingsInterface == null) { Text( - text = settingsError?.message ?: "This module does not have any settings", + text = "This module does not have any settings", style = MaterialTheme.typography.bodySmall, modifier = Modifier.padding(8.dp) ) diff --git a/common/src/main/assets/lang/en_US.json b/common/src/main/assets/lang/en_US.json @@ -703,6 +703,10 @@ "name": "Auto Reload", "description": "Automatically reloads scripts when they change" }, + "integrated_ui": { + "name": "Integrated UI", + "description": "Allows scripts to add custom UI components to Snapchat" + }, "disable_log_anonymization": { "name": "Disable Log Anonymization", "description": "Disables the anonymization of logs" diff --git a/common/src/main/kotlin/me/rhunk/snapenhance/common/config/impl/Scripting.kt b/common/src/main/kotlin/me/rhunk/snapenhance/common/config/impl/Scripting.kt @@ -7,5 +7,6 @@ class Scripting : ConfigContainer() { val developerMode = boolean("developer_mode", false) { requireRestart() } val moduleFolder = string("module_folder", "modules") { addFlags(ConfigFlag.FOLDER); requireRestart() } val autoReload = unique("auto_reload", "snapchat_only", "all") + val integratedUI = boolean("integrated_ui", false) { requireRestart() } val disableLogAnonymization = boolean("disable_log_anonymization", false) { requireRestart() } } \ No newline at end of file diff --git a/common/src/main/kotlin/me/rhunk/snapenhance/common/scripting/JSModule.kt b/common/src/main/kotlin/me/rhunk/snapenhance/common/scripting/JSModule.kt @@ -11,6 +11,7 @@ import me.rhunk.snapenhance.common.scripting.ktx.scriptable import me.rhunk.snapenhance.common.scripting.ktx.scriptableObject import me.rhunk.snapenhance.common.scripting.type.ModuleInfo import me.rhunk.snapenhance.common.scripting.type.Permissions +import me.rhunk.snapenhance.common.scripting.ui.InterfaceManager import org.mozilla.javascript.Function import org.mozilla.javascript.NativeJavaObject import org.mozilla.javascript.ScriptableObject @@ -53,6 +54,7 @@ class JSModule( registerBindings( JavaInterfaces(), + InterfaceManager(), ) moduleObject.putFunction("setField") { args -> diff --git a/common/src/main/kotlin/me/rhunk/snapenhance/common/scripting/ui/EnumScriptInterface.kt b/common/src/main/kotlin/me/rhunk/snapenhance/common/scripting/ui/EnumScriptInterface.kt @@ -0,0 +1,11 @@ +package me.rhunk.snapenhance.common.scripting.ui + +import me.rhunk.snapenhance.common.scripting.bindings.BindingSide + +enum class EnumScriptInterface( + val key: String, + val side: BindingSide +) { + SETTINGS("settings", BindingSide.MANAGER), + FRIEND_FEED_CONTEXT_MENU("friendFeedContextMenu", BindingSide.CORE), +}+ \ No newline at end of file diff --git a/common/src/main/kotlin/me/rhunk/snapenhance/common/scripting/ui/InterfaceManager.kt b/common/src/main/kotlin/me/rhunk/snapenhance/common/scripting/ui/InterfaceManager.kt @@ -0,0 +1,116 @@ +package me.rhunk.snapenhance.common.scripting.ui + +import me.rhunk.snapenhance.common.scripting.bindings.AbstractBinding +import me.rhunk.snapenhance.common.scripting.bindings.BindingSide +import me.rhunk.snapenhance.common.scripting.ktx.contextScope +import me.rhunk.snapenhance.common.scripting.ktx.scriptableObject +import me.rhunk.snapenhance.common.scripting.ui.components.Node +import me.rhunk.snapenhance.common.scripting.ui.components.NodeType +import me.rhunk.snapenhance.common.scripting.ui.components.impl.ActionNode +import me.rhunk.snapenhance.common.scripting.ui.components.impl.ActionType +import me.rhunk.snapenhance.common.scripting.ui.components.impl.RowColumnNode +import org.mozilla.javascript.Function +import org.mozilla.javascript.annotations.JSFunction + + +class InterfaceBuilder { + val nodes = mutableListOf<Node>() + var onDisposeCallback: (() -> Unit)? = null + + private fun createNode(type: NodeType, block: Node.() -> Unit): Node { + return Node(type).apply(block).also { nodes.add(it) } + } + + fun onDispose(block: () -> Unit) { + nodes.add(ActionNode(ActionType.DISPOSE, callback = block)) + } + + fun onLaunched(block: () -> Unit) { + onLaunched(Unit, block) + } + + fun onLaunched(key: Any, block: () -> Unit) { + nodes.add(ActionNode(ActionType.LAUNCHED, key, block)) + } + + fun row(block: (InterfaceBuilder) -> Unit) = RowColumnNode(NodeType.ROW).apply { + children.addAll(InterfaceBuilder().apply(block).nodes) + }.also { nodes.add(it) } + + fun column(block: (InterfaceBuilder) -> Unit) = RowColumnNode(NodeType.COLUMN).apply { + children.addAll(InterfaceBuilder().apply(block).nodes) + }.also { nodes.add(it) } + + fun text(text: String) = createNode(NodeType.TEXT) { + label(text) + } + + fun switch(state: Boolean?, callback: (Boolean) -> Unit) = createNode(NodeType.SWITCH) { + attributes["state"] = state + attributes["callback"] = callback + } + + fun button(label: String, callback: () -> Unit) = createNode(NodeType.BUTTON) { + label(label) + attributes["callback"] = callback + } + + fun slider(min: Int, max: Int, step: Int, value: Int, callback: (Int) -> Unit) = createNode( + NodeType.SLIDER + ) { + attributes["value"] = value + attributes["min"] = min + attributes["max"] = max + attributes["step"] = step + attributes["callback"] = callback + } + + fun list(label: String, items: List<String>, callback: (String) -> Unit) = createNode(NodeType.LIST) { + label(label) + attributes["items"] = items + attributes["callback"] = callback + } +} + + + +class InterfaceManager : AbstractBinding("interface-manager", BindingSide.COMMON) { + private val interfaces = mutableMapOf<String, (args: Map<String, Any?>) -> InterfaceBuilder?>() + + fun buildInterface(scriptInterface: EnumScriptInterface, args: Map<String, Any?> = emptyMap()): InterfaceBuilder? { + return runCatching { + interfaces[scriptInterface.key]?.invoke(args) + }.onFailure { + context.runtime.logger.error("Failed to build interface ${scriptInterface.key} for ${context.moduleInfo.name}", it) + }.getOrNull() + } + + override fun onDispose() { + interfaces.clear() + } + + fun hasInterface(scriptInterfaces: EnumScriptInterface): Boolean { + return interfaces.containsKey(scriptInterfaces.key) + } + + @Suppress("unused") + @JSFunction fun create(name: String, callback: Function) { + interfaces[name] = { args -> + val interfaceBuilder = InterfaceBuilder() + runCatching { + contextScope { + callback.call(this, callback, callback, arrayOf(interfaceBuilder, scriptableObject { + args.forEach { (key, value) -> + putConst(key,this, value) + } + })) + } + interfaceBuilder + }.onFailure { + context.runtime.logger.error("Failed to create interface $name for ${context.moduleInfo.name}", it) + }.getOrNull() + } + } + + override fun getObject() = this +}+ \ No newline at end of file diff --git a/common/src/main/kotlin/me/rhunk/snapenhance/common/scripting/ui/ScriptInterface.kt b/common/src/main/kotlin/me/rhunk/snapenhance/common/scripting/ui/ScriptInterface.kt @@ -0,0 +1,181 @@ +package me.rhunk.snapenhance.common.scripting.ui + +import androidx.compose.foundation.layout.* +import androidx.compose.material3.OutlinedButton +import androidx.compose.material3.Slider +import androidx.compose.material3.Switch +import androidx.compose.material3.Text +import androidx.compose.runtime.* +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import kotlinx.coroutines.launch +import me.rhunk.snapenhance.common.logger.AbstractLogger +import me.rhunk.snapenhance.common.scripting.ui.components.Node +import me.rhunk.snapenhance.common.scripting.ui.components.NodeType +import me.rhunk.snapenhance.common.scripting.ui.components.impl.ActionNode +import me.rhunk.snapenhance.common.scripting.ui.components.impl.ActionType +import kotlin.math.abs + + +@Composable +@Suppress("UNCHECKED_CAST") +private fun DrawNode(node: Node) { + val coroutineScope = rememberCoroutineScope() + val cachedAttributes = remember { mutableStateMapOf(*node.attributes.toList().toTypedArray()) } + + node.uiChangeDetection = { key, value -> + coroutineScope.launch { + cachedAttributes[key] = value + } + } + + DisposableEffect(Unit) { + onDispose { + node.uiChangeDetection = { _, _ -> } + } + } + + val arrangement = cachedAttributes["arrangement"] + val alignment = cachedAttributes["alignment"] + val spacing = cachedAttributes["spacing"]?.toString()?.toInt()?.let { abs(it) } + + val rowColumnModifier = Modifier + .then(if (cachedAttributes["fillMaxWidth"] as? Boolean == true) Modifier.fillMaxWidth() else Modifier) + .then(if (cachedAttributes["fillMaxHeight"] as? Boolean == true) Modifier.fillMaxHeight() else Modifier) + .padding( + (cachedAttributes["padding"] + ?.toString() + ?.toInt() + ?.let { abs(it) } ?: 2).dp) + + fun runCallbackSafe(callback: () -> Unit) { + runCatching { + callback() + }.onFailure { + AbstractLogger.directError("Error running callback", it) + } + } + + @Composable + fun NodeLabel() { + Text( + text = cachedAttributes["label"] as String, + fontSize = (cachedAttributes["fontSize"]?.toString()?.toInt() ?: 14).sp, + color = (cachedAttributes["color"] as? Long)?.let { Color(it) } ?: Color.Unspecified + ) + } + + when (node.type) { + NodeType.ACTION -> { + when ((node as ActionNode).actionType) { + ActionType.LAUNCHED -> { + LaunchedEffect(node.key) { + runCallbackSafe { + node.callback() + } + } + } + ActionType.DISPOSE -> { + DisposableEffect(Unit) { + onDispose { + runCallbackSafe { + node.callback() + } + } + } + } + } + } + NodeType.COLUMN -> { + Column( + verticalArrangement = arrangement as? Arrangement.Vertical ?: spacing?.let { Arrangement.spacedBy(it.dp) } ?: Arrangement.Top, + horizontalAlignment = alignment as? Alignment.Horizontal ?: Alignment.Start, + modifier = rowColumnModifier + ) { + node.children.forEach { child -> + DrawNode(child) + } + } + } + NodeType.ROW -> { + Row( + horizontalArrangement = arrangement as? Arrangement.Horizontal ?: spacing?.let { Arrangement.spacedBy(it.dp) } ?: Arrangement.SpaceBetween, + verticalAlignment = alignment as? Alignment.Vertical ?: Alignment.CenterVertically, + modifier = rowColumnModifier + ) { + node.children.forEach { child -> + DrawNode(child) + } + } + } + NodeType.TEXT -> NodeLabel() + NodeType.SWITCH -> { + var switchState by remember { + mutableStateOf(cachedAttributes["state"] as Boolean) + } + Switch( + checked = switchState, + onCheckedChange = { state -> + runCallbackSafe { + switchState = state + node.setAttribute("state", state) + (cachedAttributes["callback"] as? (Boolean) -> Unit)?.let { it(state) } + } + } + ) + } + NodeType.SLIDER -> { + var sliderValue by remember { + mutableFloatStateOf((cachedAttributes["value"] as Int).toFloat()) + } + Slider( + value = sliderValue, + onValueChange = { value -> + runCallbackSafe { + sliderValue = value + node.setAttribute("value", value.toInt()) + (cachedAttributes["callback"] as? (Int) -> Unit)?.let { it(value.toInt()) } + } + }, + valueRange = (cachedAttributes["min"] as Int).toFloat()..(cachedAttributes["max"] as Int).toFloat(), + steps = cachedAttributes["step"] as Int, + ) + } + NodeType.BUTTON -> { + OutlinedButton(onClick = { + runCallbackSafe { + (cachedAttributes["callback"] as? () -> Unit)?.let { it() } + } + }) { + NodeLabel() + } + } + else -> {} + } +} + +@Composable +fun ScriptInterface(interfaceBuilder: InterfaceBuilder) { + Column( + modifier = Modifier + .fillMaxWidth() + .padding(8.dp) + ) { + interfaceBuilder.nodes.forEach { node -> + DrawNode(node) + } + + DisposableEffect(Unit) { + onDispose { + runCatching { + interfaceBuilder.onDisposeCallback?.invoke() + }.onFailure { + AbstractLogger.directError("Error running onDisposed callback", it) + } + } + } + } +}+ \ No newline at end of file diff --git a/common/src/main/kotlin/me/rhunk/snapenhance/common/scripting/ui/components/Node.kt b/common/src/main/kotlin/me/rhunk/snapenhance/common/scripting/ui/components/Node.kt @@ -0,0 +1,52 @@ +package me.rhunk.snapenhance.common.scripting.ui.components + +open class Node( + val type: NodeType, +) { + lateinit var uiChangeDetection: (key: String, value: Any?) -> Unit + + val children = mutableListOf<Node>() + val attributes = object: HashMap<String, Any?>() { + override fun put(key: String, value: Any?): Any? { + return super.put(key, value).also { + if (::uiChangeDetection.isInitialized) { + uiChangeDetection(key, value) + } + } + } + } + + fun setAttribute(key: String, value: Any?) { + attributes[key] = value + } + + fun fillMaxWidth(): Node { + attributes["fillMaxWidth"] = true + return this + } + + fun fillMaxHeight(): Node { + attributes["fillMaxHeight"] = true + return this + } + + fun label(text: String): Node { + attributes["label"] = text + return this + } + + fun padding(padding: Int): Node { + attributes["padding"] = padding + return this + } + + fun fontSize(size: Int): Node { + attributes["fontSize"] = size + return this + } + + fun color(color: Long): Node { + attributes["color"] = color + return this + } +} diff --git a/common/src/main/kotlin/me/rhunk/snapenhance/common/scripting/ui/components/NodeType.kt b/common/src/main/kotlin/me/rhunk/snapenhance/common/scripting/ui/components/NodeType.kt @@ -0,0 +1,12 @@ +package me.rhunk.snapenhance.common.scripting.ui.components + +enum class NodeType { + ROW, + COLUMN, + TEXT, + SWITCH, + BUTTON, + SLIDER, + LIST, + ACTION +}+ \ No newline at end of file diff --git a/common/src/main/kotlin/me/rhunk/snapenhance/common/scripting/ui/components/impl/ActionNode.kt b/common/src/main/kotlin/me/rhunk/snapenhance/common/scripting/ui/components/impl/ActionNode.kt @@ -0,0 +1,15 @@ +package me.rhunk.snapenhance.common.scripting.ui.components.impl + +import me.rhunk.snapenhance.common.scripting.ui.components.Node +import me.rhunk.snapenhance.common.scripting.ui.components.NodeType + +enum class ActionType { + LAUNCHED, + DISPOSE +} + +class ActionNode( + val actionType: ActionType, + val key: Any = Unit, + val callback: () -> Unit +): Node(NodeType.ACTION)+ \ No newline at end of file diff --git a/common/src/main/kotlin/me/rhunk/snapenhance/common/scripting/ui/components/impl/RowColumnNode.kt b/common/src/main/kotlin/me/rhunk/snapenhance/common/scripting/ui/components/impl/RowColumnNode.kt @@ -0,0 +1,47 @@ +package me.rhunk.snapenhance.common.scripting.ui.components.impl + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.ui.Alignment +import me.rhunk.snapenhance.common.scripting.ui.components.Node +import me.rhunk.snapenhance.common.scripting.ui.components.NodeType + + +class RowColumnNode( + type: NodeType, +) : Node(type) { + companion object { + private val arrangements = mapOf( + "start" to Arrangement.Start, + "end" to Arrangement.End, + "top" to Arrangement.Top, + "bottom" to Arrangement.Bottom, + "center" to Arrangement.Center, + "spaceBetween" to Arrangement.SpaceBetween, + "spaceAround" to Arrangement.SpaceAround, + "spaceEvenly" to Arrangement.SpaceEvenly, + ) + private val alignments = mapOf( + "start" to Alignment.Start, + "end" to Alignment.End, + "top" to Alignment.Top, + "bottom" to Alignment.Bottom, + "centerVertically" to Alignment.CenterVertically, + "centerHorizontally" to Alignment.CenterHorizontally, + ) + } + + fun arrangement(arrangement: String): RowColumnNode { + attributes["arrangement"] = arrangements[arrangement] ?: throw IllegalArgumentException("Invalid arrangement") + return this + } + + fun alignment(alignment: String): RowColumnNode { + attributes["alignment"] = alignments[alignment] ?: throw IllegalArgumentException("Invalid alignment") + return this + } + + fun spacedBy(spacing: Int): RowColumnNode { + attributes["spacing"] = spacing + return this + } +}+ \ No newline at end of file diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/core/manager/impl/FeatureManager.kt b/core/src/main/kotlin/me/rhunk/snapenhance/core/manager/impl/FeatureManager.kt @@ -25,7 +25,7 @@ import me.rhunk.snapenhance.core.features.impl.tweaks.UnsaveableMessages import me.rhunk.snapenhance.core.features.impl.ui.* import me.rhunk.snapenhance.core.logger.CoreLogger import me.rhunk.snapenhance.core.manager.Manager -import me.rhunk.snapenhance.core.ui.menu.impl.MenuViewInjector +import me.rhunk.snapenhance.core.ui.menu.MenuViewInjector import kotlin.reflect.KClass import kotlin.system.measureTimeMillis diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/core/ui/menu/MenuViewInjector.kt b/core/src/main/kotlin/me/rhunk/snapenhance/core/ui/menu/MenuViewInjector.kt @@ -0,0 +1,159 @@ +package me.rhunk.snapenhance.core.ui.menu + +import android.annotation.SuppressLint +import android.view.Gravity +import android.view.View +import android.view.ViewGroup +import android.widget.FrameLayout +import android.widget.LinearLayout +import android.widget.ScrollView +import me.rhunk.snapenhance.core.event.events.impl.AddViewEvent +import me.rhunk.snapenhance.core.features.Feature +import me.rhunk.snapenhance.core.features.FeatureLoadParams +import me.rhunk.snapenhance.core.features.impl.messaging.Messaging +import me.rhunk.snapenhance.core.ui.ViewTagState +import me.rhunk.snapenhance.core.ui.menu.impl.* +import me.rhunk.snapenhance.core.util.ktx.getIdentifier +import java.lang.reflect.Modifier +import kotlin.reflect.KClass + +@SuppressLint("DiscouragedApi") +class MenuViewInjector : Feature("MenuViewInjector", loadParams = FeatureLoadParams.ACTIVITY_CREATE_ASYNC) { + private val viewTagState = ViewTagState() + + private val menuMap = mutableMapOf<KClass<*>, AbstractMenu>() + private val newChatString by lazy { + context.resources.getString(context.resources.getIdentifier("new_chat", "string")) + } + + @SuppressLint("ResourceType") + override fun asyncOnActivityCreate() { + arrayOf( + OperaContextActionMenu(), + OperaDownloadIconMenu(), + SettingsGearInjector(), + FriendFeedInfoMenu(), + ChatActionMenu(), + SettingsMenu() + ).forEach { + menuMap[it::class] = it.also { + it.context = context; it.init() + } + } + + val messaging = context.feature(Messaging::class) + + val actionSheetItemsContainerLayoutId = context.resources.getIdentifier("action_sheet_items_container", "id") + val actionSheetContainer = context.resources.getIdentifier("action_sheet_container", "id") + val actionMenu = context.resources.getIdentifier("action_menu", "id") + val componentsHolder = context.resources.getIdentifier("components_holder", "id") + val feedNewChat = context.resources.getIdentifier("feed_new_chat", "id") + val contextMenuButtonIconView = context.resources.getIdentifier("context_menu_button_icon_view", "id") + + context.event.subscribe(AddViewEvent::class) { event -> + val originalAddView: (View) -> Unit = { + event.adapter.invokeOriginal(arrayOf(it, -1, + FrameLayout.LayoutParams( + ViewGroup.LayoutParams.MATCH_PARENT, + ViewGroup.LayoutParams.MATCH_PARENT + )) + ) + } + + val viewGroup: ViewGroup = event.parent + val childView: View = event.view + menuMap[OperaContextActionMenu::class]!!.inject(viewGroup, childView, originalAddView) + + if (childView.id == contextMenuButtonIconView) { + menuMap[OperaDownloadIconMenu::class]!!.inject(viewGroup, childView, originalAddView) + } + + if (event.parent.id == componentsHolder && childView.id == feedNewChat) { + menuMap[SettingsGearInjector::class]!!.inject(viewGroup, childView, originalAddView) + return@subscribe + } + + //download in chat snaps and notes from the chat action menu + if (viewGroup.javaClass.name.endsWith("ActionMenuChatItemContainer")) { + if (viewGroup.parent == null || viewGroup.parent.parent == null) return@subscribe + menuMap[ChatActionMenu::class]!!.inject(viewGroup, childView, originalAddView) + return@subscribe + } + + //TODO: inject in group chat menus + if (viewGroup.id == actionSheetContainer && childView.id == actionMenu && messaging.lastFetchGroupConversationUUID != null) { + val injectedLayout = LinearLayout(childView.context).apply { + orientation = LinearLayout.VERTICAL + gravity = Gravity.BOTTOM + layoutParams = ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT) + addView(childView) + addOnAttachStateChangeListener(object: View.OnAttachStateChangeListener { + override fun onViewAttachedToWindow(v: View) {} + override fun onViewDetachedFromWindow(v: View) { + messaging.lastFetchGroupConversationUUID = null + } + }) + } + + context.runOnUiThread { + injectedLayout.addView(ScrollView(injectedLayout.context).apply { + layoutParams = LinearLayout.LayoutParams( + ViewGroup.LayoutParams.MATCH_PARENT, + ViewGroup.LayoutParams.WRAP_CONTENT + ).apply { + weight = 1f; + setMargins(0, 100, 0, 0) + } + + addView(LinearLayout(context).apply { + orientation = LinearLayout.VERTICAL + menuMap[FriendFeedInfoMenu::class]?.inject(event.parent, injectedLayout) { view -> + view.layoutParams = LinearLayout.LayoutParams( + ViewGroup.LayoutParams.MATCH_PARENT, + ViewGroup.LayoutParams.WRAP_CONTENT + ).apply { + setMargins(0, 5, 0, 5) + } + addView(view) + } + }) + }, 0) + } + + event.view = injectedLayout + } + + if (viewGroup is LinearLayout && viewGroup.id == actionSheetItemsContainerLayoutId) { + val itemStringInterface by lazy { + childView.javaClass.declaredFields.filter { + !it.type.isPrimitive && Modifier.isAbstract(it.type.modifiers) + }.map { + runCatching { + it.isAccessible = true + it[childView] + }.getOrNull() + }.firstOrNull() + } + + //the 3 dot button shows a menu which contains the first item as a Plain object + if (viewGroup.getChildCount() == 0 && itemStringInterface != null && itemStringInterface.toString().startsWith("Plain(primaryText=$newChatString")) { + menuMap[SettingsMenu::class]!!.inject(viewGroup, childView, originalAddView) + viewGroup.addOnAttachStateChangeListener(object: View.OnAttachStateChangeListener { + override fun onViewAttachedToWindow(v: View) {} + override fun onViewDetachedFromWindow(v: View) { + viewTagState.removeState(viewGroup) + } + }) + viewTagState[viewGroup] + return@subscribe + } + if (messaging.lastFetchConversationUUID == null || messaging.lastFetchConversationUserUUID == null) return@subscribe + + //filter by the slot index + if (viewGroup.getChildCount() != context.config.userInterface.friendFeedMenuPosition.get()) return@subscribe + if (viewTagState[viewGroup]) return@subscribe + menuMap[FriendFeedInfoMenu::class]!!.inject(viewGroup, childView, originalAddView) + } + } + } +}+ \ No newline at end of file diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/core/ui/menu/impl/FriendFeedInfoMenu.kt b/core/src/main/kotlin/me/rhunk/snapenhance/core/ui/menu/impl/FriendFeedInfoMenu.kt @@ -9,8 +9,10 @@ import android.view.View import android.view.ViewGroup import android.widget.Button import android.widget.CompoundButton +import android.widget.LinearLayout import android.widget.ProgressBar import android.widget.Switch +import androidx.compose.runtime.remember import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Job import kotlinx.coroutines.delay @@ -21,6 +23,10 @@ import me.rhunk.snapenhance.common.data.MessageUpdate import me.rhunk.snapenhance.common.database.impl.ConversationMessage import me.rhunk.snapenhance.common.database.impl.FriendInfo import me.rhunk.snapenhance.common.database.impl.UserConversationLink +import me.rhunk.snapenhance.common.scripting.ui.EnumScriptInterface +import me.rhunk.snapenhance.common.scripting.ui.InterfaceManager +import me.rhunk.snapenhance.common.scripting.ui.ScriptInterface +import me.rhunk.snapenhance.common.ui.createComposeView import me.rhunk.snapenhance.common.util.protobuf.ProtoReader import me.rhunk.snapenhance.common.util.snap.BitmojiSelfie import me.rhunk.snapenhance.core.features.impl.messaging.Messaging @@ -326,5 +332,33 @@ class FriendFeedInfoMenu : AbstractMenu() { } }) } + + if (context.config.scripting.integratedUI.get()) { + context.scriptRuntime.eachModule { + val interfaceManager = getBinding(InterfaceManager::class) + ?.takeIf { + it.hasInterface(EnumScriptInterface.FRIEND_FEED_CONTEXT_MENU) + } ?: return@eachModule + + viewConsumer(LinearLayout(view.context).apply { + layoutParams = ViewGroup.LayoutParams( + ViewGroup.LayoutParams.MATCH_PARENT, + ViewGroup.LayoutParams.WRAP_CONTENT + ) + + applyTheme(view.width, hasRadius = true) + + orientation = LinearLayout.VERTICAL + addView(createComposeView(view.context) { + ScriptInterface(interfaceBuilder = remember { + interfaceManager.buildInterface(EnumScriptInterface.FRIEND_FEED_CONTEXT_MENU, mapOf( + "conversationId" to conversationId, + "userId" to targetUser + )) + } ?: return@createComposeView) + }) + }) + } + } } } \ No newline at end of file diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/core/ui/menu/impl/MenuViewInjector.kt b/core/src/main/kotlin/me/rhunk/snapenhance/core/ui/menu/impl/MenuViewInjector.kt @@ -1,151 +0,0 @@ -package me.rhunk.snapenhance.core.ui.menu.impl - -import android.annotation.SuppressLint -import android.view.Gravity -import android.view.View -import android.view.ViewGroup -import android.widget.FrameLayout -import android.widget.LinearLayout -import me.rhunk.snapenhance.core.event.events.impl.AddViewEvent -import me.rhunk.snapenhance.core.features.Feature -import me.rhunk.snapenhance.core.features.FeatureLoadParams -import me.rhunk.snapenhance.core.features.impl.messaging.Messaging -import me.rhunk.snapenhance.core.ui.ViewTagState -import me.rhunk.snapenhance.core.ui.menu.AbstractMenu -import me.rhunk.snapenhance.core.util.ktx.getIdentifier -import java.lang.reflect.Modifier -import kotlin.reflect.KClass - -@SuppressLint("DiscouragedApi") -class MenuViewInjector : Feature("MenuViewInjector", loadParams = FeatureLoadParams.ACTIVITY_CREATE_ASYNC) { - private val viewTagState = ViewTagState() - - private val menuMap = mutableMapOf<KClass<*>, AbstractMenu>() - private val newChatString by lazy { - context.resources.getString(context.resources.getIdentifier("new_chat", "string")) - } - - @SuppressLint("ResourceType") - override fun asyncOnActivityCreate() { - menuMap[OperaContextActionMenu::class] = OperaContextActionMenu() - menuMap[OperaDownloadIconMenu::class] = OperaDownloadIconMenu() - menuMap[SettingsGearInjector::class] = SettingsGearInjector() - menuMap[FriendFeedInfoMenu::class] = FriendFeedInfoMenu() - menuMap[ChatActionMenu::class] = ChatActionMenu() - menuMap[SettingsMenu::class] = SettingsMenu() - - menuMap.values.forEach { it.context = context; it.init() } - - val messaging = context.feature(Messaging::class) - - val actionSheetItemsContainerLayoutId = context.resources.getIdentifier("action_sheet_items_container", "id") - val actionSheetContainer = context.resources.getIdentifier("action_sheet_container", "id") - val actionMenu = context.resources.getIdentifier("action_menu", "id") - val componentsHolder = context.resources.getIdentifier("components_holder", "id") - val feedNewChat = context.resources.getIdentifier("feed_new_chat", "id") - val contextMenuButtonIconView = context.resources.getIdentifier("context_menu_button_icon_view", "id") - - context.event.subscribe(AddViewEvent::class) { event -> - val originalAddView: (View) -> Unit = { - event.adapter.invokeOriginal(arrayOf(it, -1, - FrameLayout.LayoutParams( - ViewGroup.LayoutParams.MATCH_PARENT, - ViewGroup.LayoutParams.MATCH_PARENT - )) - ) - } - - val viewGroup: ViewGroup = event.parent - val childView: View = event.view - menuMap[OperaContextActionMenu::class]!!.inject(viewGroup, childView, originalAddView) - - if (childView.id == contextMenuButtonIconView) { - menuMap[OperaDownloadIconMenu::class]!!.inject(viewGroup, childView, originalAddView) - } - - if (event.parent.id == componentsHolder && childView.id == feedNewChat) { - menuMap[SettingsGearInjector::class]!!.inject(viewGroup, childView, originalAddView) - return@subscribe - } - - //download in chat snaps and notes from the chat action menu - if (viewGroup.javaClass.name.endsWith("ActionMenuChatItemContainer")) { - if (viewGroup.parent == null || viewGroup.parent.parent == null) return@subscribe - menuMap[ChatActionMenu::class]!!.inject(viewGroup, childView, originalAddView) - return@subscribe - } - - //TODO: inject in group chat menus - if (viewGroup.id == actionSheetContainer && childView.id == actionMenu && messaging.lastFetchGroupConversationUUID != null) { - val injectedLayout = LinearLayout(childView.context).apply { - orientation = LinearLayout.VERTICAL - gravity = Gravity.BOTTOM - layoutParams = ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT) - addView(childView) - addOnAttachStateChangeListener(object: View.OnAttachStateChangeListener { - override fun onViewAttachedToWindow(v: View) {} - override fun onViewDetachedFromWindow(v: View) { - messaging.lastFetchGroupConversationUUID = null - } - }) - } - - val viewList = mutableListOf<View>() - context.runOnUiThread { - menuMap[FriendFeedInfoMenu::class]?.inject(event.parent, injectedLayout) { view -> - view.layoutParams = LinearLayout.LayoutParams( - ViewGroup.LayoutParams.MATCH_PARENT, - ViewGroup.LayoutParams.WRAP_CONTENT - ).apply { - setMargins(0, 5, 0, 5) - } - viewList.add(view) - } - - viewList.add(View(injectedLayout.context).apply { - layoutParams = LinearLayout.LayoutParams( - ViewGroup.LayoutParams.MATCH_PARENT, - 30 - ) - }) - - viewList.reversed().forEach { injectedLayout.addView(it, 0) } - } - - event.view = injectedLayout - } - - if (viewGroup is LinearLayout && viewGroup.id == actionSheetItemsContainerLayoutId) { - val itemStringInterface by lazy { - childView.javaClass.declaredFields.filter { - !it.type.isPrimitive && Modifier.isAbstract(it.type.modifiers) - }.map { - runCatching { - it.isAccessible = true - it[childView] - }.getOrNull() - }.firstOrNull() - } - - //the 3 dot button shows a menu which contains the first item as a Plain object - if (viewGroup.getChildCount() == 0 && itemStringInterface != null && itemStringInterface.toString().startsWith("Plain(primaryText=$newChatString")) { - menuMap[SettingsMenu::class]!!.inject(viewGroup, childView, originalAddView) - viewGroup.addOnAttachStateChangeListener(object: View.OnAttachStateChangeListener { - override fun onViewAttachedToWindow(v: View) {} - override fun onViewDetachedFromWindow(v: View) { - viewTagState.removeState(viewGroup) - } - }) - viewTagState[viewGroup] - return@subscribe - } - if (messaging.lastFetchConversationUUID == null || messaging.lastFetchConversationUserUUID == null) return@subscribe - - //filter by the slot index - if (viewGroup.getChildCount() != context.config.userInterface.friendFeedMenuPosition.get()) return@subscribe - if (viewTagState[viewGroup]) return@subscribe - menuMap[FriendFeedInfoMenu::class]!!.inject(viewGroup, childView, originalAddView) - } - } - } -}- \ No newline at end of file