mirror of
https://github.com/rhunk/SnapEnhance.git
synced 2025-06-13 05:37:48 +02:00
feat(core/ui): conversation toolbox
- add scripting support
This commit is contained in:
@ -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()
|
||||
}
|
||||
|
@ -8,4 +8,5 @@ enum class EnumScriptInterface(
|
||||
) {
|
||||
SETTINGS("settings", BindingSide.MANAGER),
|
||||
FRIEND_FEED_CONTEXT_MENU("friendFeedContextMenu", BindingSide.CORE),
|
||||
CONVERSATION_TOOLBOX("conversationToolbox", BindingSide.CORE),
|
||||
}
|
@ -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)
|
||||
}
|
||||
|
@ -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()
|
||||
}
|
||||
}
|
@ -118,6 +118,7 @@ class FeatureManager(
|
||||
EditTextOverride::class,
|
||||
PreventForcedLogout::class,
|
||||
SuspendLocationUpdates::class,
|
||||
ConversationToolbox::class,
|
||||
)
|
||||
|
||||
initializeFeatures()
|
||||
|
Reference in New Issue
Block a user