commit b0434f44f437162979785dc1fc0bc1c033825812
parent c1d3e0736a3633760b237abed7b3c87b5a39933a
Author: rhunk <101876869+rhunk@users.noreply.github.com>
Date: Sun, 3 Mar 2024 19:48:44 +0100
feat(ui): experimental new chat action menu
Diffstat:
5 files changed, 186 insertions(+), 3 deletions(-)
diff --git a/common/src/main/assets/lang/en_US.json b/common/src/main/assets/lang/en_US.json
@@ -678,6 +678,10 @@
"name": "Convert Message Locally",
"description": "Converts snaps to chat external media locally. This appears in chat download context menu"
},
+ "new_chat_action_menu": {
+ "name": "New Chat Action Menu",
+ "description": "Use the new chat action menu drawer"
+ },
"story_logger": {
"name": "Story Logger",
"description": "Provides a history of friends stories"
diff --git a/common/src/main/kotlin/me/rhunk/snapenhance/common/config/impl/Experimental.kt b/common/src/main/kotlin/me/rhunk/snapenhance/common/config/impl/Experimental.kt
@@ -22,6 +22,7 @@ class Experimental : ConfigContainer() {
val sessionEvents = container("session_events", SessionEventsConfig()) { requireRestart(); nativeHooks() }
val spoof = container("spoof", Spoof()) { icon = "Fingerprint" ; addNotices(FeatureNotice.BAN_RISK); requireRestart() }
val convertMessageLocally = boolean("convert_message_locally") { requireRestart() }
+ val newChatActionMenu = boolean("new_chat_action_menu") { requireRestart() }
val storyLogger = boolean("story_logger") { requireRestart(); addNotices(FeatureNotice.UNSTABLE); }
val appPasscode = string("app_passcode")
val appLockOnResume = boolean("app_lock_on_resume")
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
@@ -16,9 +16,9 @@ import me.rhunk.snapenhance.core.util.ktx.getIdentifier
@SuppressLint("DiscouragedApi")
class MenuViewInjector : Feature("MenuViewInjector", loadParams = FeatureLoadParams.ACTIVITY_CREATE_ASYNC) {
- @SuppressLint("ResourceType")
override fun asyncOnActivityCreate() {
val menuMap = arrayOf(
+ NewChatActionMenu(),
OperaContextActionMenu(),
OperaDownloadIconMenu(),
SettingsGearInjector(),
@@ -75,8 +75,12 @@ class MenuViewInjector : Feature("MenuViewInjector", loadParams = FeatureLoadPar
return@subscribe
}
- //download in chat snaps and notes from the chat action menu
- if (viewGroup.javaClass.name.endsWith("ActionMenuChatItemContainer")) {
+ if (childView.javaClass.name.endsWith("ChatActionMenuComponent") && context.config.experimental.newChatActionMenu.get()) {
+ (menuMap[NewChatActionMenu::class]!! as NewChatActionMenu).handle(event)
+ return@subscribe
+ }
+
+ if (viewGroup.javaClass.name.endsWith("ActionMenuChatItemContainer") && !context.config.experimental.newChatActionMenu.get()) {
if (viewGroup.parent == null || viewGroup.parent.parent == null) return@subscribe
menuMap[ChatActionMenu::class]!!.inject(viewGroup, childView, originalAddView)
return@subscribe
diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/core/ui/menu/impl/NewChatActionMenu.kt b/core/src/main/kotlin/me/rhunk/snapenhance/core/ui/menu/impl/NewChatActionMenu.kt
@@ -0,0 +1,163 @@
+package me.rhunk.snapenhance.core.ui.menu.impl
+
+import android.view.ViewGroup
+import android.widget.LinearLayout
+import android.widget.ScrollView
+import androidx.compose.foundation.border
+import androidx.compose.foundation.clickable
+import androidx.compose.foundation.gestures.detectTapGestures
+import androidx.compose.foundation.layout.*
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.filled.Download
+import androidx.compose.material.icons.filled.RemoveRedEye
+import androidx.compose.material.icons.outlined.Image
+import androidx.compose.material.icons.rounded.BookmarkRemove
+import androidx.compose.material3.Icon
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.remember
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.graphics.vector.ImageVector
+import androidx.compose.ui.input.pointer.pointerInput
+import androidx.compose.ui.unit.dp
+import me.rhunk.snapenhance.common.ui.createComposeView
+import me.rhunk.snapenhance.core.event.events.impl.AddViewEvent
+import me.rhunk.snapenhance.core.features.impl.downloader.MediaDownloader
+import me.rhunk.snapenhance.core.features.impl.experiments.ConvertMessageLocally
+import me.rhunk.snapenhance.core.features.impl.messaging.Messaging
+import me.rhunk.snapenhance.core.features.impl.spying.MessageLogger
+import me.rhunk.snapenhance.core.ui.iterateParent
+import me.rhunk.snapenhance.core.ui.menu.AbstractMenu
+import me.rhunk.snapenhance.core.ui.triggerCloseTouchEvent
+import me.rhunk.snapenhance.core.util.ktx.isDarkTheme
+import me.rhunk.snapenhance.core.util.ktx.setObjectField
+import me.rhunk.snapenhance.core.util.ktx.vibrateLongPress
+
+class NewChatActionMenu : AbstractMenu() {
+ fun handle(event: AddViewEvent) {
+ if (event.parent is LinearLayout) return
+ val closeActionMenu = { event.parent.iterateParent {
+ it.triggerCloseTouchEvent()
+ false
+ } }
+
+ val mediaDownloader = context.feature(MediaDownloader::class)
+ val messageLogger = context.feature(MessageLogger::class)
+ val messaging = context.feature(Messaging::class)
+
+ val composeView = createComposeView(event.view.context) {
+ val primaryColor = remember { if (event.view.context.isDarkTheme()) Color.White else Color.Black }
+
+ @Composable
+ fun ListButton(
+ modifier: Modifier = Modifier,
+ icon: ImageVector,
+ text: String,
+ ) {
+ Row(
+ modifier = Modifier
+ .fillMaxWidth()
+ .then(modifier)
+ .padding(top = 11.dp, bottom = 11.dp),
+ horizontalArrangement = Arrangement.spacedBy(16.dp)
+ ) {
+ Icon(
+ modifier = Modifier
+ .padding(start = 16.dp),
+ imageVector = icon,
+ tint = primaryColor,
+ contentDescription = text
+ )
+ Text(text, color = primaryColor)
+ }
+ Spacer(modifier = Modifier
+ .height(1.dp)
+ .fillMaxWidth()
+ .border(1.dp, MaterialTheme.colorScheme.onSurface.copy(alpha = 0.05f)))
+ }
+
+ Column(
+ modifier = Modifier.fillMaxWidth(),
+ ) {
+ if (context.config.downloader.downloadContextMenu.get()) {
+ ListButton(icon = Icons.Default.RemoveRedEye, text = context.translation["chat_action_menu.preview_button"], modifier = Modifier.clickable {
+ closeActionMenu()
+ mediaDownloader.onMessageActionMenu(true)
+ })
+ ListButton(icon = Icons.Default.Download, text = context.translation["chat_action_menu.download_button"], modifier = Modifier.pointerInput(Unit) {
+ detectTapGestures(
+ onTap = {
+ closeActionMenu()
+ mediaDownloader.onMessageActionMenu(false)
+ },
+ onLongPress = {
+ context.androidContext.vibrateLongPress()
+ mediaDownloader.onMessageActionMenu(isPreviewMode = false, forceAllowDuplicate = true)
+ }
+ )
+ })
+ }
+
+ if (context.config.messaging.messageLogger.globalState == true) {
+ ListButton(icon = Icons.Rounded.BookmarkRemove, text = context.translation["chat_action_menu.delete_logged_message_button"], modifier = Modifier.clickable {
+ closeActionMenu()
+ context.executeAsync {
+ messageLogger.deleteMessage(messaging.openedConversationUUID.toString(), messaging.lastFocusedMessageId)
+ }
+ })
+ }
+
+ if (context.config.experimental.convertMessageLocally.get()) {
+ ListButton(icon = Icons.Outlined.Image, text = context.translation["chat_action_menu.convert_message"], modifier = Modifier.clickable {
+ closeActionMenu()
+ messaging.conversationManager?.fetchMessage(
+ messaging.openedConversationUUID.toString(),
+ messaging.lastFocusedMessageId,
+ onSuccess = {
+ context.runOnUiThread {
+ runCatching {
+ context.feature(ConvertMessageLocally::class)
+ .convertMessageInterface(it)
+ }.onFailure {
+ context.log.verbose("Failed to convert message: $it")
+ context.shortToast("Failed to edit message: $it")
+ }
+ }
+ },
+ onError = {
+ context.shortToast("Failed to fetch message: $it")
+ }
+ )
+ })
+ }
+ }
+ }.apply {
+ layoutParams = LinearLayout.LayoutParams(
+ ViewGroup.LayoutParams.MATCH_PARENT,
+ ViewGroup.LayoutParams.WRAP_CONTENT
+ )
+ }
+ event.view = ScrollView(event.view.context).apply {
+ addView(LinearLayout(context).apply {
+ orientation = LinearLayout.VERTICAL
+ layoutParams = ViewGroup.LayoutParams(
+ ViewGroup.LayoutParams.MATCH_PARENT,
+ ViewGroup.LayoutParams.MATCH_PARENT
+ )
+ addView(composeView)
+ composeView.post {
+ (event.parent.layoutParams as ViewGroup.MarginLayoutParams).apply {
+ setObjectField("a", null) // remove drag callback
+ if (height < composeView.measuredHeight) {
+ height += composeView.measuredHeight
+ }
+ }
+ event.parent.requestLayout()
+ }
+ addView(event.view)
+ })
+ }
+ }
+}+
\ No newline at end of file
diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/core/util/ktx/AndroidExt.kt b/core/src/main/kotlin/me/rhunk/snapenhance/core/util/ktx/AndroidExt.kt
@@ -8,6 +8,7 @@ import android.content.res.TypedArray
import android.graphics.drawable.Drawable
import android.os.VibrationEffect
import android.os.Vibrator
+import androidx.core.graphics.ColorUtils
import me.rhunk.snapenhance.common.Constants
@@ -42,4 +43,13 @@ fun Resources.getDrawable(name: String, theme: Theme): Drawable {
@SuppressLint("MissingPermission")
fun Context.vibrateLongPress() {
getSystemService(Vibrator::class.java).vibrate(VibrationEffect.createOneShot(50, VibrationEffect.DEFAULT_AMPLITUDE))
+}
+
+@SuppressLint("DiscouragedApi")
+fun Context.isDarkTheme(): Boolean {
+ return theme.obtainStyledAttributes(
+ intArrayOf(resources.getIdentifier("sigColorTextPrimary", "attr", packageName))
+ ).getColor(0, 0).let {
+ ColorUtils.calculateLuminance(it) > 0.5
+ }
}
\ No newline at end of file