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:
Mcommon/src/main/assets/lang/en_US.json | 4++++
Mcommon/src/main/kotlin/me/rhunk/snapenhance/common/config/impl/Experimental.kt | 1+
Mcore/src/main/kotlin/me/rhunk/snapenhance/core/ui/menu/MenuViewInjector.kt | 10+++++++---
Acore/src/main/kotlin/me/rhunk/snapenhance/core/ui/menu/impl/NewChatActionMenu.kt | 164+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mcore/src/main/kotlin/me/rhunk/snapenhance/core/util/ktx/AndroidExt.kt | 10++++++++++
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