From f6d9800f15fa724fc36a44ce342d83e99a3b1100 Mon Sep 17 00:00:00 2001 From: Infi Date: Wed, 18 Sep 2024 17:36:36 +0200 Subject: [PATCH] feat(jbm): remove jbm switches on every message Signed-off-by: Infi --- .../chat/revolt/activities/MainActivity.kt | 14 ++- .../chat/revolt/api/settings/Experiments.kt | 39 +++++++++ .../chat/revolt/api/settings/GlobalState.kt | 1 + .../chat/revolt/components/chat/Message.kt | 42 ++++----- .../settings/ExperimentsSettingsScreen.kt | 81 +++++++++++++++++ .../revolt/screens/settings/SettingsScreen.kt | 21 +++++ .../chat/revolt/sheets/ReactionInfoSheet.kt | 87 ++++++++++++++++++- app/src/main/res/drawable/ic_flask_24dp.xml | 9 ++ 8 files changed, 266 insertions(+), 28 deletions(-) create mode 100644 app/src/main/java/chat/revolt/api/settings/Experiments.kt create mode 100644 app/src/main/java/chat/revolt/screens/settings/ExperimentsSettingsScreen.kt create mode 100644 app/src/main/res/drawable/ic_flask_24dp.xml diff --git a/app/src/main/java/chat/revolt/activities/MainActivity.kt b/app/src/main/java/chat/revolt/activities/MainActivity.kt index ecc25bf1..ddd00304 100644 --- a/app/src/main/java/chat/revolt/activities/MainActivity.kt +++ b/app/src/main/java/chat/revolt/activities/MainActivity.kt @@ -42,10 +42,11 @@ import chat.revolt.RevoltApplication import chat.revolt.api.HitRateLimitException import chat.revolt.api.RevoltAPI import chat.revolt.api.RevoltHttp +import chat.revolt.api.api import chat.revolt.api.routes.onboard.needsOnboarding +import chat.revolt.api.settings.Experiments import chat.revolt.api.settings.GlobalState import chat.revolt.api.settings.SyncedSettings -import chat.revolt.api.api import chat.revolt.ndk.NativeLibraries import chat.revolt.persistence.KVStorage import chat.revolt.screens.DefaultDestinationScreen @@ -67,6 +68,7 @@ import chat.revolt.screens.settings.AppearanceSettingsScreen import chat.revolt.screens.settings.ChangelogsSettingsScreen import chat.revolt.screens.settings.ChatSettingsScreen import chat.revolt.screens.settings.DebugSettingsScreen +import chat.revolt.screens.settings.ExperimentsSettingsScreen import chat.revolt.screens.settings.ProfileSettingsScreen import chat.revolt.screens.settings.SessionSettingsScreen import chat.revolt.screens.settings.SettingsScreen @@ -125,6 +127,14 @@ class MainActivityViewModel @Inject constructor( isReady.emit(true) } + private fun doPreStartupTasks() { + Log.d("MainActivity", "Performing pre-startup tasks") + viewModelScope.launch { + Log.d("MainActivity", "Hydrating Experiments from KV") + Experiments.hydrateWithKv() + } + } + fun checkLoggedInState() { viewModelScope.launch { Log.d("MainActivity", "Checking logged in state") @@ -211,6 +221,7 @@ class MainActivityViewModel @Inject constructor( init { Log.d("MainActivity", "Starting up") + doPreStartupTasks() checkLoggedInState() } } @@ -400,6 +411,7 @@ fun AppEntrypoint( composable("settings/appearance") { AppearanceSettingsScreen(navController) } composable("settings/chat") { ChatSettingsScreen(navController) } composable("settings/debug") { DebugSettingsScreen(navController) } + composable("settings/experiments") { ExperimentsSettingsScreen(navController) } composable("settings/changelogs") { ChangelogsSettingsScreen(navController) } composable("settings/channel/{channelId}") { backStackEntry -> diff --git a/app/src/main/java/chat/revolt/api/settings/Experiments.kt b/app/src/main/java/chat/revolt/api/settings/Experiments.kt new file mode 100644 index 00000000..bb4b0e96 --- /dev/null +++ b/app/src/main/java/chat/revolt/api/settings/Experiments.kt @@ -0,0 +1,39 @@ +package chat.revolt.api.settings + +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.setValue +import chat.revolt.RevoltApplication +import chat.revolt.persistence.KVStorage + +class ExperimentInstance(default: Boolean) { + private var _isEnabled by mutableStateOf(default) + val isEnabled: Boolean + get() = GlobalState.experimentsEnabled && _isEnabled + + fun setEnabled(enabled: Boolean) { + _isEnabled = enabled + } +} + +/** + * Experiments are boolean feature flags that can be toggled by the user in a self-service manner. + * Unlike regular feature flags they are created with the goal of going live in the future. + * They come with multiple safeguards: + * - Users must first enable experiments in the settings by performing a hidden action. They are then warned about potential instability. + * - Experiment states are not persisted across devices or uninstalls. + * - All experiments can be disabled at once with a single toggle. + */ +object Experiments { + val useKotlinBasedMarkdownRenderer = ExperimentInstance(false) + + suspend fun hydrateWithKv() { + val kvStorage = KVStorage(RevoltApplication.instance) + + GlobalState.experimentsEnabled = kvStorage.getBoolean("experimentsEnabled") ?: false + + useKotlinBasedMarkdownRenderer.setEnabled( + kvStorage.getBoolean("exp/useKotlinBasedMarkdownRenderer") ?: false + ) + } +} \ No newline at end of file diff --git a/app/src/main/java/chat/revolt/api/settings/GlobalState.kt b/app/src/main/java/chat/revolt/api/settings/GlobalState.kt index 6a4ce030..a0fbf790 100644 --- a/app/src/main/java/chat/revolt/api/settings/GlobalState.kt +++ b/app/src/main/java/chat/revolt/api/settings/GlobalState.kt @@ -17,6 +17,7 @@ object GlobalState { var theme by mutableStateOf(getDefaultTheme()) var messageReplyStyle by mutableStateOf(MessageReplyStyle.SwipeFromEnd) var avatarRadius by mutableIntStateOf(50) + var experimentsEnabled by mutableStateOf(false) fun hydrateWithSettings(settings: SyncedSettings) { this.theme = settings.android.theme?.let { Theme.valueOf(it) } ?: getDefaultTheme() diff --git a/app/src/main/java/chat/revolt/components/chat/Message.kt b/app/src/main/java/chat/revolt/components/chat/Message.kt index f4898583..91b18d3c 100644 --- a/app/src/main/java/chat/revolt/components/chat/Message.kt +++ b/app/src/main/java/chat/revolt/components/chat/Message.kt @@ -34,17 +34,13 @@ import androidx.compose.material3.Icon import androidx.compose.material3.LocalContentColor import androidx.compose.material3.LocalTextStyle import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.Switch import androidx.compose.material3.Text import androidx.compose.material3.surfaceColorAtElevation import androidx.compose.runtime.Composable import androidx.compose.runtime.CompositionLocalProvider -import androidx.compose.runtime.getValue import androidx.compose.runtime.key -import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope -import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip @@ -73,6 +69,7 @@ import chat.revolt.api.routes.channel.unreact import chat.revolt.api.routes.microservices.january.asJanuaryProxyUrl import chat.revolt.api.schemas.AutumnResource import chat.revolt.api.schemas.User +import chat.revolt.api.settings.Experiments import chat.revolt.api.settings.GlobalState import chat.revolt.api.settings.MessageReplyStyle import chat.revolt.callbacks.Action @@ -199,8 +196,6 @@ fun Message( val authorIsBlocked = remember(author) { author.relationship == "Blocked" } - var __TEMPORARY_useJbm by remember { mutableStateOf(false) } - Column(Modifier.animateContentSize()) { if (message.tail == false) { Spacer(modifier = Modifier.height(10.dp)) @@ -370,12 +365,21 @@ fun Message( message.content?.let { if (message.content.isBlank()) return@let // if only an attachment is sent - Switch( - checked = __TEMPORARY_useJbm, - onCheckedChange = { __TEMPORARY_useJbm = it }, - ) - - if (__TEMPORARY_useJbm == false) { + if (Experiments.useKotlinBasedMarkdownRenderer.isEnabled) { + CompositionLocalProvider( + LocalJBMarkdownTreeState provides LocalJBMarkdownTreeState.current.copy( + fontSizeMultiplier = Gigamoji.useGigamojiForMessage( + message.content + ) + .let { + if (it) 2f else 1f + } + ) + ) { + Spacer(modifier = Modifier.height(2.dp)) + JBMRenderer(message.content) + } + } else { CompositionLocalProvider( LocalMarkdownTreeConfig provides LocalMarkdownTreeConfig.current.copy( currentServer = RevoltAPI.channelCache[message.channel]?.server, @@ -390,20 +394,6 @@ fun Message( Spacer(modifier = Modifier.height(2.dp)) RichMarkdown(input = message.content) } - } else { - CompositionLocalProvider( - LocalJBMarkdownTreeState provides LocalJBMarkdownTreeState.current.copy( - fontSizeMultiplier = Gigamoji.useGigamojiForMessage( - message.content - ) - .let { - if (it) 2f else 1f - } - ) - ) { - Spacer(modifier = Modifier.height(2.dp)) - JBMRenderer(message.content) - } } } } diff --git a/app/src/main/java/chat/revolt/screens/settings/ExperimentsSettingsScreen.kt b/app/src/main/java/chat/revolt/screens/settings/ExperimentsSettingsScreen.kt new file mode 100644 index 00000000..0b5ef11c --- /dev/null +++ b/app/src/main/java/chat/revolt/screens/settings/ExperimentsSettingsScreen.kt @@ -0,0 +1,81 @@ +package chat.revolt.screens.settings + +import androidx.compose.material3.ElevatedButton +import androidx.compose.material3.ListItem +import androidx.compose.material3.Switch +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.runtime.setValue +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.text.style.TextOverflow +import androidx.navigation.NavController +import chat.revolt.api.settings.Experiments +import chat.revolt.api.settings.GlobalState +import chat.revolt.persistence.KVStorage +import chat.revolt.settings.dsl.SettingsPage +import chat.revolt.settings.dsl.SubcategoryContentInsets +import kotlinx.coroutines.launch + +@Composable +fun ExperimentsSettingsScreen(navController: NavController) { + val context = LocalContext.current + val kv = remember { KVStorage(context) } + val scope = rememberCoroutineScope() + + var useKotlinMdRendererChecked by remember { mutableStateOf(false) } + LaunchedEffect(Unit) { + useKotlinMdRendererChecked = kv.getBoolean("exp/useKotlinBasedMarkdownRenderer") ?: false + } + + SettingsPage( + navController, + title = { + Text("Experiments", maxLines = 1, overflow = TextOverflow.Ellipsis) + } + ) { + ListItem( + headlineContent = { + Text("New Message Markdown Renderer") + }, + supportingContent = { + Text("Use a Kotlin-based Markdown renderer for messages rather than the C++ one. Missing features may be present.") + }, + trailingContent = { + Switch( + checked = useKotlinMdRendererChecked, + onCheckedChange = { isChecked -> + scope.launch { + kv.set("exp/useKotlinBasedMarkdownRenderer", isChecked) + Experiments.useKotlinBasedMarkdownRenderer.setEnabled(isChecked) + useKotlinMdRendererChecked = isChecked + } + } + ) + } + ) + + Subcategory( + title = { + Text("Disable experiments") + }, + contentInsets = SubcategoryContentInsets + ) { + ElevatedButton( + onClick = { + scope.launch { + kv.remove("experimentsEnabled") + GlobalState.experimentsEnabled = false + navController.popBackStack() + } + } + ) { + Text("Disable") + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/chat/revolt/screens/settings/SettingsScreen.kt b/app/src/main/java/chat/revolt/screens/settings/SettingsScreen.kt index 495d4147..e84d39d0 100644 --- a/app/src/main/java/chat/revolt/screens/settings/SettingsScreen.kt +++ b/app/src/main/java/chat/revolt/screens/settings/SettingsScreen.kt @@ -261,6 +261,27 @@ fun SettingsScreen( ) } + if (GlobalState.experimentsEnabled) { + ListItem( + headlineContent = { + Text( + text = "Experiments" + ) + }, + leadingContent = { + Icon( + painter = painterResource(R.drawable.ic_flask_24dp), + contentDescription = null, + ) + }, + modifier = Modifier + .testTag("settings_view_experiments") + .clickable { + navController.navigate("settings/experiments") + } + ) + } + ListHeader { Text( stringResource( diff --git a/app/src/main/java/chat/revolt/sheets/ReactionInfoSheet.kt b/app/src/main/java/chat/revolt/sheets/ReactionInfoSheet.kt index c5d2c506..f96c5b03 100644 --- a/app/src/main/java/chat/revolt/sheets/ReactionInfoSheet.kt +++ b/app/src/main/java/chat/revolt/sheets/ReactionInfoSheet.kt @@ -1,6 +1,8 @@ package chat.revolt.sheets import androidx.compose.foundation.ExperimentalFoundationApi +import androidx.compose.foundation.clickable +import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column @@ -12,22 +14,28 @@ import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.width import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.material3.AlertDialog +import androidx.compose.material3.Button import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.LocalTextStyle import androidx.compose.material3.MaterialTheme import androidx.compose.material3.ScrollableTabRow import androidx.compose.material3.Tab import androidx.compose.material3.Text +import androidx.compose.material3.TextButton import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableIntStateOf import androidx.compose.runtime.mutableStateListOf +import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.PlatformTextStyle import androidx.compose.ui.text.font.FontWeight @@ -37,15 +45,18 @@ import androidx.compose.ui.unit.sp import chat.revolt.R import chat.revolt.api.REVOLT_FILES import chat.revolt.api.RevoltAPI -import chat.revolt.internals.text.MessageProcessor import chat.revolt.api.internals.isUlid import chat.revolt.api.routes.custom.fetchEmoji import chat.revolt.api.routes.user.fetchUser import chat.revolt.api.schemas.Emoji import chat.revolt.api.schemas.User +import chat.revolt.api.settings.GlobalState import chat.revolt.components.chat.MemberListItem import chat.revolt.components.generic.RemoteImage import chat.revolt.components.generic.SheetEnd +import chat.revolt.internals.text.MessageProcessor +import chat.revolt.persistence.KVStorage +import kotlinx.coroutines.launch @OptIn(ExperimentalFoundationApi::class) @Composable @@ -55,6 +66,9 @@ fun ReactionInfoSheet(messageId: String, emoji: String, onDismiss: () -> Unit) { val reactions = message.reactions val reactionEmoji = reactions?.keys?.toList() + val context = LocalContext.current + val scope = rememberCoroutineScope() + val extendedEmojiInfo = remember(emoji) { mutableStateListOf() } LaunchedEffect(reactionEmoji) { @@ -120,6 +134,71 @@ fun ReactionInfoSheet(messageId: String, emoji: String, onDismiss: () -> Unit) { item("info") { val current = reactionEmoji[selectedReactionIndex] + // Code related to enabling of experimental features + val interactionSource = remember { MutableInteractionSource() } + val canBeUsedForTapCountIncrement = + remember(selectedReactionIndex) { + MessageProcessor.emoji.unicodeAsShortcode( + current + ) == ":trolleybus:" + } + var tapCount by remember { mutableIntStateOf(0) } + var showEnabledConfirmAlert by remember { mutableStateOf(false) } + var showEnabledAlreadyAlert by remember { mutableStateOf(false) } + val incrementTapCount = remember { + { + if (canBeUsedForTapCountIncrement) { + tapCount++ + if (tapCount > 9) { + tapCount = 0 + if (GlobalState.experimentsEnabled) { + showEnabledAlreadyAlert = true + } else { + showEnabledConfirmAlert = true + } + } + } + } + } + + if (showEnabledAlreadyAlert) { + AlertDialog( + onDismissRequest = {}, + title = { Text("Traveller, you may not unsee your knowledge...") }, + text = { Text("Experimental features are already unlocked.") }, + confirmButton = { + TextButton(onClick = { showEnabledAlreadyAlert = false }) { + Text("OK") + } + } + ) + } + + if (showEnabledConfirmAlert) { + AlertDialog( + onDismissRequest = {}, + title = { Text("You hear a faint whisper in the wind...") }, + text = { Text("Would you like to enable experimental features? They may be unstable.") }, + confirmButton = { + TextButton(onClick = { + showEnabledConfirmAlert = false + GlobalState.experimentsEnabled = true + scope.launch { + KVStorage(context).set("experimentsEnabled", true) + } + }) { + Text("I dare to try!") + } + }, + dismissButton = { + Button(onClick = { showEnabledConfirmAlert = false }) { + Text("I shall not risk it.") + } + } + ) + } + // End of code related to enabling of experimental features + Column( verticalArrangement = Arrangement.spacedBy(16.dp), modifier = Modifier.padding( @@ -158,6 +237,12 @@ fun ReactionInfoSheet(messageId: String, emoji: String, onDismiss: () -> Unit) { ) ), modifier = Modifier + .clickable( + interactionSource = interactionSource, + indication = null, + ) { + incrementTapCount() + } .size(64.dp) ) } diff --git a/app/src/main/res/drawable/ic_flask_24dp.xml b/app/src/main/res/drawable/ic_flask_24dp.xml new file mode 100644 index 00000000..3a45d27f --- /dev/null +++ b/app/src/main/res/drawable/ic_flask_24dp.xml @@ -0,0 +1,9 @@ + + + \ No newline at end of file