commit 2a8fcacd2fec0aeb6246b197d6ef758745bb66c3
parent 90d76c6412e994f1b0c7cfafe89270d0ededa7be
Author: rhunk <101876869+rhunk@users.noreply.github.com>
Date: Wed, 27 Dec 2023 17:39:46 +0100
feat(core/ui): conversation toolbox
- add scripting support
Diffstat:
5 files changed, 217 insertions(+), 54 deletions(-)
diff --git a/app/src/main/kotlin/me/rhunk/snapenhance/e2ee/E2EEImplementation.kt b/app/src/main/kotlin/me/rhunk/snapenhance/e2ee/E2EEImplementation.kt
@@ -62,7 +62,7 @@ class E2EEImplementation (
override fun acceptPairingRequest(friendId: String, publicKey: ByteArray): ByteArray? {
val kemGen = KyberKEMGenerator(secureRandom)
- val encapsulatedSecret = runCatching {
+ val encapsulatedSecret = runCatching {
kemGen.generateEncapsulated(
KyberPublicKeyParameters(
kyberDefaultParameters,
@@ -164,7 +164,7 @@ class E2EEImplementation (
cipher.init(Cipher.DECRYPT_MODE, SecretKeySpec(encryptionKey, "AES"), IvParameterSpec(iv))
cipher.doFinal(message)
}.onFailure {
- context.log.error("Failed to decrypt message from $friendId", it)
+ context.log.warn("Failed to decrypt message for $friendId")
return null
}.getOrNull()
}
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
@@ -8,4 +8,5 @@ enum class EnumScriptInterface(
) {
SETTINGS("settings", BindingSide.MANAGER),
FRIEND_FEED_CONTEXT_MENU("friendFeedContextMenu", BindingSide.CORE),
+ CONVERSATION_TOOLBOX("conversationToolbox", BindingSide.CORE),
}
\ No newline at end of file
diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/core/features/impl/experiments/EndToEndEncryption.kt b/core/src/main/kotlin/me/rhunk/snapenhance/core/features/impl/experiments/EndToEndEncryption.kt
@@ -7,10 +7,14 @@ import android.graphics.drawable.ShapeDrawable
import android.graphics.drawable.shapes.Shape
import android.view.View
import android.view.ViewGroup
-import android.view.ViewGroup.LayoutParams
-import android.view.ViewGroup.MarginLayoutParams
import android.widget.Button
-import android.widget.TextView
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.height
+import androidx.compose.material3.Button
+import androidx.compose.material3.Text
+import androidx.compose.runtime.remember
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.unit.dp
import me.rhunk.snapenhance.common.data.ContentType
import me.rhunk.snapenhance.common.data.MessageState
import me.rhunk.snapenhance.common.data.MessagingRuleType
@@ -18,14 +22,13 @@ import me.rhunk.snapenhance.common.data.RuleState
import me.rhunk.snapenhance.common.util.protobuf.ProtoEditor
import me.rhunk.snapenhance.common.util.protobuf.ProtoReader
import me.rhunk.snapenhance.common.util.protobuf.ProtoWriter
-import me.rhunk.snapenhance.core.event.events.impl.AddViewEvent
import me.rhunk.snapenhance.core.event.events.impl.BindViewEvent
import me.rhunk.snapenhance.core.event.events.impl.BuildMessageEvent
import me.rhunk.snapenhance.core.event.events.impl.NativeUnaryCallEvent
import me.rhunk.snapenhance.core.event.events.impl.SendMessageWithContentEvent
import me.rhunk.snapenhance.core.features.FeatureLoadParams
import me.rhunk.snapenhance.core.features.MessagingRuleFeature
-import me.rhunk.snapenhance.core.features.impl.messaging.Messaging
+import me.rhunk.snapenhance.core.features.impl.ui.ConversationToolbox
import me.rhunk.snapenhance.core.ui.ViewAppearanceHelper
import me.rhunk.snapenhance.core.ui.addForegroundDrawable
import me.rhunk.snapenhance.core.ui.removeForegroundDrawable
@@ -157,56 +160,34 @@ class EndToEndEncryption : MessagingRuleFeature(
}
}
- private fun openManagementPopup() {
- val conversationId = context.feature(Messaging::class).openedConversationUUID?.toString() ?: return
- val friendId = context.database.getDMOtherParticipant(conversationId)
-
- if (friendId == null) {
- context.shortToast("This menu is only available in direct messages.")
- return
- }
-
- val actions = listOf(
- "Initiate a new shared secret",
- "Show shared key fingerprint"
- )
-
- ViewAppearanceHelper.newAlertDialogBuilder(context.mainActivity!!).apply {
- setTitle("End-to-end encryption")
- setItems(actions.toTypedArray()) { _, which ->
- when (which) {
- 0 -> {
- warnKeyOverwrite(friendId) {
- askForKeys(conversationId)
- }
- }
- 1 -> {
- val fingerprint = e2eeInterface.getSecretFingerprint(friendId)
- ViewAppearanceHelper.newAlertDialogBuilder(context).apply {
- setTitle("End-to-end encryption")
- setMessage("Your fingerprint is:\n\n$fingerprint\n\nMake sure to check if it matches your friend's fingerprint!")
- setPositiveButton("OK") { _, _ -> }
- }.show()
- }
- }
- }
- setPositiveButton("OK") { _, _ -> }
- }.show()
- }
-
@SuppressLint("SetTextI18n", "DiscouragedApi")
override fun onActivityCreate() {
if (!isEnabled) return
- // add button to input bar
- context.event.subscribe(AddViewEvent::class) { param ->
- if (param.view.toString().contains("default_input_bar")) {
- (param.view as ViewGroup).addView(TextView(param.view.context).apply {
- layoutParams = MarginLayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.MATCH_PARENT)
- setOnClickListener { openManagementPopup() }
- setPadding(20, 20, 20, 20)
- textSize = 23f
- text = "\uD83D\uDD12"
- })
+
+ context.feature(ConversationToolbox::class).addComposable("End-to-end Encryption", filter = {
+ context.database.getDMOtherParticipant(it) != null
+ }) { dialog, conversationId ->
+ val friendId = remember {
+ context.database.getDMOtherParticipant(conversationId)
+ } ?: return@addComposable
+ val fingerprint = remember {
+ runCatching {
+ e2eeInterface.getSecretFingerprint(friendId)
+ }.getOrNull()
+ }
+ if (fingerprint != null) {
+ Text("Your fingerprint is:\n\n$fingerprint\n\nMake sure to check if it matches your friend's fingerprint!")
+ } else {
+ Text("You don't have a shared secret with this friend yet. Click below to initiate a new one.")
+ }
+ Spacer(modifier = Modifier.height(10.dp))
+ Button(onClick = {
+ dialog.dismiss()
+ warnKeyOverwrite(friendId) {
+ askForKeys(conversationId)
+ }
+ }) {
+ Text("Initiate new shared secret")
}
}
@@ -245,6 +226,10 @@ class EndToEndEncryption : MessagingRuleFeature(
viewGroup.addView(Button(context.mainActivity!!).apply {
text = "Accept secret"
tag = receiveSecretTag
+ layoutParams = ViewGroup.LayoutParams(
+ ViewGroup.LayoutParams.MATCH_PARENT,
+ ViewGroup.LayoutParams.WRAP_CONTENT,
+ )
setOnClickListener {
handleSecretResponse(conversationId, secret)
}
@@ -255,6 +240,10 @@ class EndToEndEncryption : MessagingRuleFeature(
viewGroup.addView(Button(context.mainActivity!!).apply {
text = "Receive public key"
tag = receivePublicKeyTag
+ layoutParams = ViewGroup.LayoutParams(
+ ViewGroup.LayoutParams.MATCH_PARENT,
+ ViewGroup.LayoutParams.WRAP_CONTENT,
+ )
setOnClickListener {
handlePublicKeyRequest(conversationId, publicKey)
}
diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/core/features/impl/ui/ConversationToolbox.kt b/core/src/main/kotlin/me/rhunk/snapenhance/core/features/impl/ui/ConversationToolbox.kt
@@ -0,0 +1,171 @@
+package me.rhunk.snapenhance.core.features.impl.ui
+
+import android.annotation.SuppressLint
+import android.app.AlertDialog
+import android.view.Gravity
+import android.view.ViewGroup
+import android.widget.FrameLayout
+import android.widget.LinearLayout
+import android.widget.TextView
+import androidx.compose.foundation.Image
+import androidx.compose.foundation.clickable
+import androidx.compose.foundation.layout.*
+import androidx.compose.foundation.rememberScrollState
+import androidx.compose.foundation.verticalScroll
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.filled.KeyboardArrowDown
+import androidx.compose.material.icons.filled.KeyboardArrowUp
+import androidx.compose.material3.Card
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.Text
+import androidx.compose.runtime.*
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.ColorFilter
+import androidx.compose.ui.platform.LocalConfiguration
+import androidx.compose.ui.text.font.FontStyle
+import androidx.compose.ui.text.style.TextAlign
+import androidx.compose.ui.unit.dp
+import androidx.compose.ui.unit.sp
+import androidx.compose.ui.unit.times
+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.createComposeAlertDialog
+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.util.ktx.getId
+
+
+data class ComposableMenu(
+ val title: String,
+ val filter: (conversationId: String) -> Boolean,
+ val composable: @Composable (alertDialog: AlertDialog, conversationId: String) -> Unit,
+)
+
+class ConversationToolbox : Feature("Conversation Toolbox", loadParams = FeatureLoadParams.ACTIVITY_CREATE_SYNC) {
+ private val composableList = mutableListOf<ComposableMenu>()
+ private val expandedComposableCache = mutableStateMapOf<String, Boolean>()
+
+ fun addComposable(title: String, filter: (conversationId: String) -> Boolean = { true }, composable: @Composable (alertDialog: AlertDialog, conversationId: String) -> Unit) {
+ composableList.add(
+ ComposableMenu(title, filter, composable)
+ )
+ }
+
+ @SuppressLint("SetTextI18n")
+ override fun onActivityCreate() {
+ val defaultInputBarId = context.resources.getId("default_input_bar")
+
+ context.event.subscribe(AddViewEvent::class) { event ->
+ if (event.view.id != defaultInputBarId) return@subscribe
+ if (composableList.isEmpty()) return@subscribe
+
+ (event.view as ViewGroup).addView(FrameLayout(event.view.context).apply {
+ layoutParams = LinearLayout.LayoutParams(
+ ViewGroup.LayoutParams.WRAP_CONTENT,
+ (52 * context.resources.displayMetrics.density).toInt(),
+ ).apply {
+ gravity = Gravity.BOTTOM
+ }
+ setPadding(25, 0, 25, 0)
+
+ addView(TextView(event.view.context).apply {
+ layoutParams = FrameLayout.LayoutParams(
+ ViewGroup.LayoutParams.WRAP_CONTENT,
+ ViewGroup.LayoutParams.WRAP_CONTENT,
+ ).apply {
+ gravity = Gravity.CENTER_VERTICAL
+ }
+ setOnClickListener {
+ openToolbox()
+ }
+ textSize = 21f
+ text = "\uD83E\uDDF0"
+ })
+ })
+ }
+
+ context.scriptRuntime.eachModule {
+ val interfaceManager = getBinding(InterfaceManager::class)?.takeIf {
+ it.hasInterface(EnumScriptInterface.CONVERSATION_TOOLBOX)
+ } ?: return@eachModule
+ addComposable("\uD83D\uDCDC ${moduleInfo.displayName}") { alertDialog, conversationId ->
+ ScriptInterface(remember {
+ interfaceManager.buildInterface(EnumScriptInterface.CONVERSATION_TOOLBOX, mapOf(
+ "alertDialog" to alertDialog,
+ "conversationId" to conversationId,
+ ))
+ } ?: return@addComposable)
+ }
+ }
+ }
+
+ private fun openToolbox() {
+ val openedConversationId = context.feature(Messaging::class).openedConversationUUID?.toString() ?: run {
+ context.shortToast("You must open a conversation first")
+ return
+ }
+
+ createComposeAlertDialog(context.mainActivity!!) { alertDialog ->
+ Column(
+ modifier = Modifier
+ .fillMaxWidth()
+ .heightIn(
+ min = 100.dp,
+ max = LocalConfiguration.current.screenHeightDp * 0.8f.dp
+ )
+ .verticalScroll(rememberScrollState())
+ ) {
+ Text("Conversation Toolbox", fontSize = 20.sp, modifier = Modifier
+ .fillMaxWidth()
+ .padding(10.dp), textAlign = TextAlign.Center)
+ Spacer(modifier = Modifier.height(10.dp))
+
+ composableList.reversed().forEach { (title, filter, composable) ->
+ if (!filter(openedConversationId)) return@forEach
+ Card(
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(5.dp),
+ shape = MaterialTheme.shapes.medium
+ ) {
+ Row(
+ modifier = Modifier
+ .clickable {
+ expandedComposableCache[title] = !(expandedComposableCache[title] ?: false)
+ }
+ .fillMaxWidth()
+ .padding(10.dp),
+ horizontalArrangement = Arrangement.spacedBy(10.dp),
+ verticalAlignment = Alignment.CenterVertically,
+ ) {
+ Image(
+ imageVector = if (expandedComposableCache[title] == true) Icons.Filled.KeyboardArrowDown else Icons.Filled.KeyboardArrowUp,
+ contentDescription = null,
+ colorFilter = ColorFilter.tint(MaterialTheme.colorScheme.onSurface),
+ )
+ Text(title, fontSize = 16.sp, fontStyle = FontStyle.Italic)
+ }
+ if (expandedComposableCache[title] == true) {
+ Column(
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(10.dp)
+ ) {
+ runCatching {
+ composable(alertDialog, openedConversationId)
+ }.onFailure { throwable ->
+ Text("Failed to load composable: ${throwable.message}")
+ context.log.error("Failed to load composable: ${throwable.message}", throwable)
+ }
+ }
+ }
+ }
+ }
+ }
+ }.show()
+ }
+}+
\ 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
@@ -118,6 +118,7 @@ class FeatureManager(
EditTextOverride::class,
PreventForcedLogout::class,
SuspendLocationUpdates::class,
+ ConversationToolbox::class,
)
initializeFeatures()