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? {
|
override fun acceptPairingRequest(friendId: String, publicKey: ByteArray): ByteArray? {
|
||||||
val kemGen = KyberKEMGenerator(secureRandom)
|
val kemGen = KyberKEMGenerator(secureRandom)
|
||||||
val encapsulatedSecret = runCatching {
|
val encapsulatedSecret = runCatching {
|
||||||
kemGen.generateEncapsulated(
|
kemGen.generateEncapsulated(
|
||||||
KyberPublicKeyParameters(
|
KyberPublicKeyParameters(
|
||||||
kyberDefaultParameters,
|
kyberDefaultParameters,
|
||||||
@ -164,7 +164,7 @@ class E2EEImplementation (
|
|||||||
cipher.init(Cipher.DECRYPT_MODE, SecretKeySpec(encryptionKey, "AES"), IvParameterSpec(iv))
|
cipher.init(Cipher.DECRYPT_MODE, SecretKeySpec(encryptionKey, "AES"), IvParameterSpec(iv))
|
||||||
cipher.doFinal(message)
|
cipher.doFinal(message)
|
||||||
}.onFailure {
|
}.onFailure {
|
||||||
context.log.error("Failed to decrypt message from $friendId", it)
|
context.log.warn("Failed to decrypt message for $friendId")
|
||||||
return null
|
return null
|
||||||
}.getOrNull()
|
}.getOrNull()
|
||||||
}
|
}
|
||||||
|
@ -8,4 +8,5 @@ enum class EnumScriptInterface(
|
|||||||
) {
|
) {
|
||||||
SETTINGS("settings", BindingSide.MANAGER),
|
SETTINGS("settings", BindingSide.MANAGER),
|
||||||
FRIEND_FEED_CONTEXT_MENU("friendFeedContextMenu", BindingSide.CORE),
|
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.graphics.drawable.shapes.Shape
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
import android.view.ViewGroup.LayoutParams
|
|
||||||
import android.view.ViewGroup.MarginLayoutParams
|
|
||||||
import android.widget.Button
|
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.ContentType
|
||||||
import me.rhunk.snapenhance.common.data.MessageState
|
import me.rhunk.snapenhance.common.data.MessageState
|
||||||
import me.rhunk.snapenhance.common.data.MessagingRuleType
|
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.ProtoEditor
|
||||||
import me.rhunk.snapenhance.common.util.protobuf.ProtoReader
|
import me.rhunk.snapenhance.common.util.protobuf.ProtoReader
|
||||||
import me.rhunk.snapenhance.common.util.protobuf.ProtoWriter
|
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.BindViewEvent
|
||||||
import me.rhunk.snapenhance.core.event.events.impl.BuildMessageEvent
|
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.NativeUnaryCallEvent
|
||||||
import me.rhunk.snapenhance.core.event.events.impl.SendMessageWithContentEvent
|
import me.rhunk.snapenhance.core.event.events.impl.SendMessageWithContentEvent
|
||||||
import me.rhunk.snapenhance.core.features.FeatureLoadParams
|
import me.rhunk.snapenhance.core.features.FeatureLoadParams
|
||||||
import me.rhunk.snapenhance.core.features.MessagingRuleFeature
|
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.ViewAppearanceHelper
|
||||||
import me.rhunk.snapenhance.core.ui.addForegroundDrawable
|
import me.rhunk.snapenhance.core.ui.addForegroundDrawable
|
||||||
import me.rhunk.snapenhance.core.ui.removeForegroundDrawable
|
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")
|
@SuppressLint("SetTextI18n", "DiscouragedApi")
|
||||||
override fun onActivityCreate() {
|
override fun onActivityCreate() {
|
||||||
if (!isEnabled) return
|
if (!isEnabled) return
|
||||||
// add button to input bar
|
|
||||||
context.event.subscribe(AddViewEvent::class) { param ->
|
context.feature(ConversationToolbox::class).addComposable("End-to-end Encryption", filter = {
|
||||||
if (param.view.toString().contains("default_input_bar")) {
|
context.database.getDMOtherParticipant(it) != null
|
||||||
(param.view as ViewGroup).addView(TextView(param.view.context).apply {
|
}) { dialog, conversationId ->
|
||||||
layoutParams = MarginLayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.MATCH_PARENT)
|
val friendId = remember {
|
||||||
setOnClickListener { openManagementPopup() }
|
context.database.getDMOtherParticipant(conversationId)
|
||||||
setPadding(20, 20, 20, 20)
|
} ?: return@addComposable
|
||||||
textSize = 23f
|
val fingerprint = remember {
|
||||||
text = "\uD83D\uDD12"
|
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 {
|
viewGroup.addView(Button(context.mainActivity!!).apply {
|
||||||
text = "Accept secret"
|
text = "Accept secret"
|
||||||
tag = receiveSecretTag
|
tag = receiveSecretTag
|
||||||
|
layoutParams = ViewGroup.LayoutParams(
|
||||||
|
ViewGroup.LayoutParams.MATCH_PARENT,
|
||||||
|
ViewGroup.LayoutParams.WRAP_CONTENT,
|
||||||
|
)
|
||||||
setOnClickListener {
|
setOnClickListener {
|
||||||
handleSecretResponse(conversationId, secret)
|
handleSecretResponse(conversationId, secret)
|
||||||
}
|
}
|
||||||
@ -255,6 +240,10 @@ class EndToEndEncryption : MessagingRuleFeature(
|
|||||||
viewGroup.addView(Button(context.mainActivity!!).apply {
|
viewGroup.addView(Button(context.mainActivity!!).apply {
|
||||||
text = "Receive public key"
|
text = "Receive public key"
|
||||||
tag = receivePublicKeyTag
|
tag = receivePublicKeyTag
|
||||||
|
layoutParams = ViewGroup.LayoutParams(
|
||||||
|
ViewGroup.LayoutParams.MATCH_PARENT,
|
||||||
|
ViewGroup.LayoutParams.WRAP_CONTENT,
|
||||||
|
)
|
||||||
setOnClickListener {
|
setOnClickListener {
|
||||||
handlePublicKeyRequest(conversationId, publicKey)
|
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,
|
EditTextOverride::class,
|
||||||
PreventForcedLogout::class,
|
PreventForcedLogout::class,
|
||||||
SuspendLocationUpdates::class,
|
SuspendLocationUpdates::class,
|
||||||
|
ConversationToolbox::class,
|
||||||
)
|
)
|
||||||
|
|
||||||
initializeFeatures()
|
initializeFeatures()
|
||||||
|
Reference in New Issue
Block a user