feat(core/ui): conversation toolbox

- add scripting support
This commit is contained in:
rhunk
2023-12-27 17:39:46 +01:00
parent 90d76c6412
commit 2a8fcacd2f
5 changed files with 216 additions and 54 deletions

View File

@ -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()
}

View File

@ -8,4 +8,5 @@ enum class EnumScriptInterface(
) {
SETTINGS("settings", BindingSide.MANAGER),
FRIEND_FEED_CONTEXT_MENU("friendFeedContextMenu", BindingSide.CORE),
CONVERSATION_TOOLBOX("conversationToolbox", BindingSide.CORE),
}

View File

@ -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)
}

View File

@ -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()
}
}

View File

@ -118,6 +118,7 @@ class FeatureManager(
EditTextOverride::class,
PreventForcedLogout::class,
SuspendLocationUpdates::class,
ConversationToolbox::class,
)
initializeFeatures()