feat(ui): experimental new chat action menu
This commit is contained in:
@@ -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"
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user