commit ce2ae6ff4588d6c36040e87390bef80d074a50f2
parent f29e3f37cdb473a804acc5872fd93a6c2db34760
Author: rhunk <101876869+rhunk@users.noreply.github.com>
Date: Mon, 10 Jun 2024 02:23:41 +0200
feat(core): compose friend feed menu
Signed-off-by: rhunk <101876869+rhunk@users.noreply.github.com>
Diffstat:
2 files changed, 202 insertions(+), 92 deletions(-)
diff --git a/common/src/main/kotlin/me/rhunk/snapenhance/common/data/MessagingCoreObjects.kt b/common/src/main/kotlin/me/rhunk/snapenhance/common/data/MessagingCoreObjects.kt
@@ -2,6 +2,9 @@ package me.rhunk.snapenhance.common.data
import android.database.Cursor
import android.os.Parcelable
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.outlined.*
+import androidx.compose.ui.graphics.vector.ImageVector
import kotlinx.parcelize.Parcelize
import me.rhunk.snapenhance.common.config.FeatureNotice
import me.rhunk.snapenhance.common.data.download.toKeyPair
@@ -38,18 +41,19 @@ enum class SocialScope(
enum class MessagingRuleType(
val key: String,
val listMode: Boolean,
+ val icon: ImageVector,
val showInFriendMenu: Boolean = true,
val defaultValue: String? = "whitelist",
val configNotices: Array<FeatureNotice> = emptyArray()
) {
- STEALTH("stealth", true),
- AUTO_DOWNLOAD("auto_download", true),
- AUTO_SAVE("auto_save", true, defaultValue = "blacklist"),
- AUTO_OPEN_SNAPS("auto_open_snaps", true, configNotices = arrayOf(FeatureNotice.BAN_RISK, FeatureNotice.UNSTABLE), defaultValue = null),
- UNSAVEABLE_MESSAGES("unsaveable_messages", true, configNotices = arrayOf(FeatureNotice.REQUIRE_NATIVE_HOOKS), defaultValue = null),
- HIDE_FRIEND_FEED("hide_friend_feed", false, showInFriendMenu = false),
- E2E_ENCRYPTION("e2e_encryption", false),
- PIN_CONVERSATION("pin_conversation", false, showInFriendMenu = false);
+ STEALTH("stealth", true, Icons.Outlined.TrackChanges),
+ AUTO_DOWNLOAD("auto_download", true, Icons.Outlined.DownloadForOffline),
+ AUTO_SAVE("auto_save", true, Icons.Outlined.Save, defaultValue = "blacklist"),
+ AUTO_OPEN_SNAPS("auto_open_snaps", true, Icons.Outlined.OpenInFull, configNotices = arrayOf(FeatureNotice.BAN_RISK, FeatureNotice.UNSTABLE), defaultValue = null),
+ UNSAVEABLE_MESSAGES("unsaveable_messages", true, Icons.Outlined.FolderOff, configNotices = arrayOf(FeatureNotice.REQUIRE_NATIVE_HOOKS), defaultValue = null),
+ HIDE_FRIEND_FEED("hide_friend_feed", false, Icons.Outlined.VisibilityOff, showInFriendMenu = false),
+ E2E_ENCRYPTION("e2e_encryption", false, Icons.Outlined.Lock),
+ PIN_CONVERSATION("pin_conversation", false, Icons.Outlined.PushPin, showInFriendMenu = false);
fun translateOptionKey(optionKey: String): String {
return if (listMode) "rules.properties.$key.options.$optionKey" else "rules.properties.$key.name"
diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/core/ui/menu/impl/FriendFeedInfoMenu.kt b/core/src/main/kotlin/me/rhunk/snapenhance/core/ui/menu/impl/FriendFeedInfoMenu.kt
@@ -7,18 +7,29 @@ import android.graphics.drawable.BitmapDrawable
import android.graphics.drawable.Drawable
import android.view.View
import android.view.ViewGroup
-import android.widget.Button
import android.widget.LinearLayout
-import android.widget.Switch
-import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.background
+import androidx.compose.foundation.gestures.detectTapGestures
+import androidx.compose.foundation.layout.*
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.CheckCircleOutline
import androidx.compose.material.icons.filled.Info
import androidx.compose.material.icons.filled.NotInterested
-import androidx.compose.material3.MaterialTheme
-import androidx.compose.material3.Surface
-import androidx.compose.runtime.remember
+import androidx.compose.material.icons.outlined.EditNote
+import androidx.compose.material.icons.outlined.RemoveRedEye
+import androidx.compose.material3.*
+import androidx.compose.runtime.*
+import androidx.compose.ui.Alignment
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.text.TextStyle
+import androidx.compose.ui.text.font.Font
+import androidx.compose.ui.text.font.FontFamily
+import androidx.compose.ui.text.font.FontWeight
+import androidx.compose.ui.unit.dp
+import androidx.compose.ui.unit.sp
import me.rhunk.snapenhance.common.data.ContentType
import me.rhunk.snapenhance.common.data.FriendLinkType
import me.rhunk.snapenhance.common.database.impl.ConversationMessage
@@ -37,7 +48,8 @@ import me.rhunk.snapenhance.core.ui.ViewAppearanceHelper
import me.rhunk.snapenhance.core.ui.applyTheme
import me.rhunk.snapenhance.core.ui.menu.AbstractMenu
import me.rhunk.snapenhance.core.ui.triggerRootCloseTouchEvent
-import me.rhunk.snapenhance.core.util.ktx.vibrateLongPress
+import me.rhunk.snapenhance.core.util.ktx.getIdentifier
+import me.rhunk.snapenhance.core.util.ktx.isDarkTheme
import java.net.HttpURLConnection
import java.net.URL
import java.text.DateFormat
@@ -47,6 +59,22 @@ import java.util.Date
import java.util.Locale
class FriendFeedInfoMenu : AbstractMenu() {
+ private val avenirNextMediumFont by lazy {
+ FontFamily(
+ Font(context.resources.getIdentifier("avenir_next_medium", "font"), FontWeight.Medium)
+ )
+ }
+ private val sigColorTextPrimary by lazy {
+ context.androidContext.theme.obtainStyledAttributes(
+ intArrayOf(context.resources.getIdentifier("sigColorTextPrimary", "attr"))
+ ).getColor(0, 0)
+ }
+ private val sigColorBackgroundSurface by lazy {
+ context.androidContext.theme.obtainStyledAttributes(
+ intArrayOf(context.resources.getIdentifier("sigColorBackgroundSurface", "attr"))
+ ).getColor(0, 0)
+ }
+
private fun getImageDrawable(url: String): Drawable {
val connection = URL(url).openConnection() as HttpURLConnection
connection.connect()
@@ -208,16 +236,54 @@ class FriendFeedInfoMenu : AbstractMenu() {
builder.show()
}
- private fun createToggleFeature(viewConsumer: ((View) -> Unit), value: String, checked: () -> Boolean, toggle: (Boolean) -> Unit) {
- viewConsumer(Switch(context.androidContext).apply {
- text = this@FriendFeedInfoMenu.context.translation[value]
- isChecked = checked()
- applyTheme(hasRadius = true)
- isSoundEffectsEnabled = false
- setOnCheckedChangeListener { _, checked ->
- toggle(checked)
+ @Composable
+ private fun MenuElement(
+ index: Int,
+ icon: ImageVector,
+ text: String,
+ onClick: () -> Unit,
+ onLongClick: (() -> Unit)? = null,
+ content: @Composable RowScope.() -> Unit = {}
+ ) {
+ if (index > 0) {
+ Spacer(modifier = Modifier
+ .height(1.dp)
+ .background(remember { if (context.androidContext.isDarkTheme()) Color(0x1affffff) else Color(0xffeeeeee) })
+ .fillMaxWidth())
+ }
+ Surface(
+ color = Color(sigColorBackgroundSurface),
+ contentColor = Color(sigColorTextPrimary),
+ ) {
+ Row(
+ modifier = Modifier
+ .fillMaxWidth()
+ .pointerInput(Unit) {
+ detectTapGestures(
+ onLongPress = {
+ onLongClick?.invoke()
+ },
+ onTap = {
+ onClick()
+ }
+ )
+ }
+ .heightIn(min = 55.dp)
+ .padding(start = 16.dp, end = 16.dp),
+ verticalAlignment = Alignment.CenterVertically
+ ) {
+ Icon(icon, contentDescription = null, modifier = Modifier
+ .size(32.dp)
+ .padding(end = 8.dp))
+ Text(
+ text = text,
+ modifier = Modifier.weight(1f),
+ lineHeight = 18.sp,
+ fontSize = 16.sp,
+ )
+ content()
}
- })
+ }
}
override fun inject(parent: ViewGroup, view: View, viewConsumer: ((View) -> Unit)) {
@@ -228,89 +294,129 @@ class FriendFeedInfoMenu : AbstractMenu() {
val messaging = context.feature(Messaging::class)
val conversationId = messaging.lastFocusedConversationId ?: return
- val targetUser = context.database.getDMOtherParticipant(conversationId)
+ val targetUser by lazy { context.database.getDMOtherParticipant(conversationId) }
messaging.resetLastFocusedConversation()
val translation = context.translation.getCategory("friend_menu_option")
- if (friendFeedMenuOptions.contains("conversation_info")) {
- viewConsumer(Button(view.context).apply {
- text = translation["preview"]
- applyTheme(view.width, hasRadius = true)
- setOnClickListener {
- showPreview(
- targetUser,
- conversationId
- )
- }
- })
- }
- modContext.features.getRuleFeatures().forEach { ruleFeature ->
- if (!friendFeedMenuOptions.contains(ruleFeature.ruleType.key)) return@forEach
-
- val ruleState = ruleFeature.getRuleState() ?: return@forEach
- createToggleFeature(viewConsumer,
- ruleFeature.ruleType.translateOptionKey(ruleState.key),
- { ruleFeature.getState(conversationId) },
- {
- ruleFeature.setState(conversationId, it)
- context.inAppOverlay.showStatusToast(
- if (it) Icons.Default.CheckCircleOutline else Icons.Default.NotInterested,
- context.translation.format("rules.toasts.${if (it) "enabled" else "disabled"}", "ruleName" to context.translation[ruleFeature.ruleType.translateOptionKey(ruleState.key)]),
- durationMs = 1500
+ @Composable
+ fun ComposeFriendFeedMenu() {
+ Column(
+ modifier = Modifier.fillMaxWidth(),
+ ) {
+ var elementIndex by remember { mutableIntStateOf(0) }
+
+ if (friendFeedMenuOptions.contains("conversation_info")) {
+ MenuElement(
+ remember { elementIndex++ },
+ Icons.Outlined.RemoveRedEye,
+ translation["preview"],
+ onClick = {
+ showPreview(targetUser, conversationId)
+ }
)
- context.mainActivity?.triggerRootCloseTouchEvent()
}
- )
- }
- if (friendFeedMenuOptions.contains("mark_snaps_as_seen")) {
- viewConsumer(Button(view.context).apply {
- text = translation["mark_snaps_as_seen"]
- isSoundEffectsEnabled = false
- applyTheme(view.width, hasRadius = true)
- setOnClickListener {
- this@FriendFeedInfoMenu.context.apply {
- mainActivity?.triggerRootCloseTouchEvent()
- feature(AutoMarkAsRead::class).markSnapsAsSeen(conversationId)
- }
- }
- })
- }
+ modContext.features.getRuleFeatures().forEach { ruleFeature ->
+ if (!friendFeedMenuOptions.contains(ruleFeature.ruleType.key)) return@forEach
- if (targetUser != null && friendFeedMenuOptions.contains("mark_stories_as_seen_locally")) {
- viewConsumer(Button(view.context).apply {
- text = translation["mark_stories_as_seen_locally"]
- applyTheme(view.width, hasRadius = true)
- isSoundEffectsEnabled = false
-
- val translations = this@FriendFeedInfoMenu.context.translation.getCategory("mark_as_seen")
-
- this@FriendFeedInfoMenu.context.apply {
- setOnClickListener {
- mainActivity?.triggerRootCloseTouchEvent()
- this@FriendFeedInfoMenu.context.inAppOverlay.showStatusToast(
- Icons.Default.Info,
- if (database.setStoriesViewedState(targetUser, true)) translations["seen_toast"]
- else translations["already_seen_toast"],
- durationMs = 2500
+ val ruleState = ruleFeature.getRuleState() ?: return@forEach
+ var state by remember { mutableStateOf(ruleFeature.getState(conversationId)) }
+
+ fun toggle() {
+ state = !ruleFeature.getState(conversationId)
+ ruleFeature.setState(conversationId, state)
+ context.inAppOverlay.showStatusToast(
+ if (ruleFeature.getState(conversationId)) Icons.Default.CheckCircleOutline else Icons.Default.NotInterested,
+ context.translation.format("rules.toasts.${if (ruleFeature.getState(conversationId)) "enabled" else "disabled"}", "ruleName" to context.translation[ruleFeature.ruleType.translateOptionKey(ruleState.key)]),
+ durationMs = 1500
)
+ context.mainActivity?.triggerRootCloseTouchEvent()
}
- setOnLongClickListener {
- context.vibrateLongPress()
- mainActivity?.triggerRootCloseTouchEvent()
- this@FriendFeedInfoMenu.context.inAppOverlay.showStatusToast(
- Icons.Default.Info,
- if (database.setStoriesViewedState(targetUser, false)) translations["unseen_toast"]
- else translations["already_unseen_toast"],
- durationMs = 2500
+
+ MenuElement(
+ remember { elementIndex++ },
+ icon = ruleFeature.ruleType.icon,
+ text = context.translation[ruleFeature.ruleType.translateOptionKey(ruleState.key)],
+ onClick = {
+ toggle()
+ }
+ ) {
+ Switch(
+ checked = state,
+ onCheckedChange = {
+ state = it
+ toggle()
+ }
)
- true
}
}
- })
+
+ if (friendFeedMenuOptions.contains("mark_snaps_as_seen")) {
+ MenuElement(
+ remember { elementIndex++ },
+ Icons.Outlined.EditNote,
+ translation["mark_snaps_as_seen"],
+ onClick = {
+ context.apply {
+ mainActivity?.triggerRootCloseTouchEvent()
+ feature(AutoMarkAsRead::class).markSnapsAsSeen(conversationId)
+ }
+ }
+ )
+ }
+
+ if (targetUser != null && friendFeedMenuOptions.contains("mark_stories_as_seen_locally")) {
+ val markAsSeenTranslation = remember { context.translation.getCategory("mark_as_seen") }
+
+ MenuElement(
+ remember { elementIndex++ },
+ Icons.Outlined.RemoveRedEye,
+ translation["mark_stories_as_seen_locally"],
+ onClick = {
+ context.apply {
+ mainActivity?.triggerRootCloseTouchEvent()
+ inAppOverlay.showStatusToast(
+ Icons.Default.Info,
+ if (database.setStoriesViewedState(targetUser!!, true)) markAsSeenTranslation["seen_toast"]
+ else markAsSeenTranslation["already_seen_toast"],
+ durationMs = 2500
+ )
+ }
+ },
+ onLongClick = {
+ view.post {
+ context.apply {
+ mainActivity?.triggerRootCloseTouchEvent()
+ inAppOverlay.showStatusToast(
+ Icons.Default.Info,
+ if (database.setStoriesViewedState(targetUser!!, false)) markAsSeenTranslation["unseen_toast"]
+ else markAsSeenTranslation["already_unseen_toast"],
+ durationMs = 2500
+ )
+ }
+ }
+ }
+ )
+ }
+ }
}
+ viewConsumer(
+ createComposeView(view.context) {
+ CompositionLocalProvider(
+ LocalTextStyle provides LocalTextStyle.current.merge(TextStyle(fontFamily = avenirNextMediumFont))
+ ) {
+ ComposeFriendFeedMenu()
+ }
+ }.apply {
+ layoutParams = ViewGroup.LayoutParams(
+ ViewGroup.LayoutParams.MATCH_PARENT,
+ ViewGroup.LayoutParams.WRAP_CONTENT
+ )
+ }
+ )
+
if (context.config.scripting.integratedUI.get()) {
context.scriptRuntime.eachModule {
val interfaceManager = getBinding(InterfaceManager::class)