feat(jbm): remove jbm switches on every message

Signed-off-by: Infi <infi@infi.sh>
This commit is contained in:
Infi 2024-09-18 17:36:36 +02:00
parent f4085c1c93
commit f6d9800f15
8 changed files with 266 additions and 28 deletions

View File

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

View File

@ -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
)
}
}

View File

@ -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()

View File

@ -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)
}
}
}
}

View File

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

View File

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

View File

@ -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<Emoji>() }
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)
)
}

View File

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:height="24dp"
android:width="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="#ffffff"
android:pathData="M6,22A3,3 0 0,1 3,19C3,18.4 3.18,17.84 3.5,17.37L9,7.81V6A1,1 0 0,1 8,5V4A2,2 0 0,1 10,2H14A2,2 0 0,1 16,4V5A1,1 0 0,1 15,6V7.81L20.5,17.37C20.82,17.84 21,18.4 21,19A3,3 0 0,1 18,22H6M5,19A1,1 0 0,0 6,20H18A1,1 0 0,0 19,19C19,18.79 18.93,18.59 18.82,18.43L16.53,14.47L14,17L8.93,11.93L5.18,18.43C5.07,18.59 5,18.79 5,19M13,10A1,1 0 0,0 12,11A1,1 0 0,0 13,12A1,1 0 0,0 14,11A1,1 0 0,0 13,10Z" />
</vector>