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:
Mcommon/src/main/kotlin/me/rhunk/snapenhance/common/data/MessagingCoreObjects.kt | 20++++++++++++--------
Mcore/src/main/kotlin/me/rhunk/snapenhance/core/ui/menu/impl/FriendFeedInfoMenu.kt | 274+++++++++++++++++++++++++++++++++++++++++++++++++++++++------------------------
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)