feat(core): compose friend feed menu

Signed-off-by: rhunk <101876869+rhunk@users.noreply.github.com>
This commit is contained in:
rhunk 2024-06-10 02:23:41 +02:00
parent f29e3f37cd
commit ce2ae6ff45
2 changed files with 204 additions and 94 deletions

View File

@ -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"

View File

@ -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,88 +294,128 @@ 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
@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)
}
)
}
})
}
modContext.features.getRuleFeatures().forEach { ruleFeature ->
if (!friendFeedMenuOptions.contains(ruleFeature.ruleType.key)) return@forEach
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
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()
}
MenuElement(
remember { elementIndex++ },
icon = ruleFeature.ruleType.icon,
text = context.translation[ruleFeature.ruleType.translateOptionKey(ruleState.key)],
onClick = {
toggle()
}
) {
Switch(
checked = state,
onCheckedChange = {
state = it
toggle()
}
)
}
}
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)
}
}
)
context.mainActivity?.triggerRootCloseTouchEvent()
}
)
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
)
}
}
}
)
}
}
}
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)
}
viewConsumer(
createComposeView(view.context) {
CompositionLocalProvider(
LocalTextStyle provides LocalTextStyle.current.merge(TextStyle(fontFamily = avenirNextMediumFont))
) {
ComposeFriendFeedMenu()
}
})
}
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
)
}
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
)
true
}
}
})
}
}.apply {
layoutParams = ViewGroup.LayoutParams(
ViewGroup.LayoutParams.MATCH_PARENT,
ViewGroup.LayoutParams.WRAP_CONTENT
)
}
)
if (context.config.scripting.integratedUI.get()) {
context.scriptRuntime.eachModule {