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.HitRateLimitException
import chat.revolt.api.RevoltAPI import chat.revolt.api.RevoltAPI
import chat.revolt.api.RevoltHttp import chat.revolt.api.RevoltHttp
import chat.revolt.api.api
import chat.revolt.api.routes.onboard.needsOnboarding import chat.revolt.api.routes.onboard.needsOnboarding
import chat.revolt.api.settings.Experiments
import chat.revolt.api.settings.GlobalState import chat.revolt.api.settings.GlobalState
import chat.revolt.api.settings.SyncedSettings import chat.revolt.api.settings.SyncedSettings
import chat.revolt.api.api
import chat.revolt.ndk.NativeLibraries import chat.revolt.ndk.NativeLibraries
import chat.revolt.persistence.KVStorage import chat.revolt.persistence.KVStorage
import chat.revolt.screens.DefaultDestinationScreen 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.ChangelogsSettingsScreen
import chat.revolt.screens.settings.ChatSettingsScreen import chat.revolt.screens.settings.ChatSettingsScreen
import chat.revolt.screens.settings.DebugSettingsScreen import chat.revolt.screens.settings.DebugSettingsScreen
import chat.revolt.screens.settings.ExperimentsSettingsScreen
import chat.revolt.screens.settings.ProfileSettingsScreen import chat.revolt.screens.settings.ProfileSettingsScreen
import chat.revolt.screens.settings.SessionSettingsScreen import chat.revolt.screens.settings.SessionSettingsScreen
import chat.revolt.screens.settings.SettingsScreen import chat.revolt.screens.settings.SettingsScreen
@ -125,6 +127,14 @@ class MainActivityViewModel @Inject constructor(
isReady.emit(true) 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() { fun checkLoggedInState() {
viewModelScope.launch { viewModelScope.launch {
Log.d("MainActivity", "Checking logged in state") Log.d("MainActivity", "Checking logged in state")
@ -211,6 +221,7 @@ class MainActivityViewModel @Inject constructor(
init { init {
Log.d("MainActivity", "Starting up") Log.d("MainActivity", "Starting up")
doPreStartupTasks()
checkLoggedInState() checkLoggedInState()
} }
} }
@ -400,6 +411,7 @@ fun AppEntrypoint(
composable("settings/appearance") { AppearanceSettingsScreen(navController) } composable("settings/appearance") { AppearanceSettingsScreen(navController) }
composable("settings/chat") { ChatSettingsScreen(navController) } composable("settings/chat") { ChatSettingsScreen(navController) }
composable("settings/debug") { DebugSettingsScreen(navController) } composable("settings/debug") { DebugSettingsScreen(navController) }
composable("settings/experiments") { ExperimentsSettingsScreen(navController) }
composable("settings/changelogs") { ChangelogsSettingsScreen(navController) } composable("settings/changelogs") { ChangelogsSettingsScreen(navController) }
composable("settings/channel/{channelId}") { backStackEntry -> 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 theme by mutableStateOf(getDefaultTheme())
var messageReplyStyle by mutableStateOf(MessageReplyStyle.SwipeFromEnd) var messageReplyStyle by mutableStateOf(MessageReplyStyle.SwipeFromEnd)
var avatarRadius by mutableIntStateOf(50) var avatarRadius by mutableIntStateOf(50)
var experimentsEnabled by mutableStateOf(false)
fun hydrateWithSettings(settings: SyncedSettings) { fun hydrateWithSettings(settings: SyncedSettings) {
this.theme = settings.android.theme?.let { Theme.valueOf(it) } ?: getDefaultTheme() 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.LocalContentColor
import androidx.compose.material3.LocalTextStyle import androidx.compose.material3.LocalTextStyle
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Switch
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.material3.surfaceColorAtElevation import androidx.compose.material3.surfaceColorAtElevation
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.getValue
import androidx.compose.runtime.key import androidx.compose.runtime.key
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip 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.routes.microservices.january.asJanuaryProxyUrl
import chat.revolt.api.schemas.AutumnResource import chat.revolt.api.schemas.AutumnResource
import chat.revolt.api.schemas.User import chat.revolt.api.schemas.User
import chat.revolt.api.settings.Experiments
import chat.revolt.api.settings.GlobalState import chat.revolt.api.settings.GlobalState
import chat.revolt.api.settings.MessageReplyStyle import chat.revolt.api.settings.MessageReplyStyle
import chat.revolt.callbacks.Action import chat.revolt.callbacks.Action
@ -199,8 +196,6 @@ fun Message(
val authorIsBlocked = remember(author) { author.relationship == "Blocked" } val authorIsBlocked = remember(author) { author.relationship == "Blocked" }
var __TEMPORARY_useJbm by remember { mutableStateOf(false) }
Column(Modifier.animateContentSize()) { Column(Modifier.animateContentSize()) {
if (message.tail == false) { if (message.tail == false) {
Spacer(modifier = Modifier.height(10.dp)) Spacer(modifier = Modifier.height(10.dp))
@ -370,12 +365,21 @@ fun Message(
message.content?.let { message.content?.let {
if (message.content.isBlank()) return@let // if only an attachment is sent if (message.content.isBlank()) return@let // if only an attachment is sent
Switch( if (Experiments.useKotlinBasedMarkdownRenderer.isEnabled) {
checked = __TEMPORARY_useJbm, CompositionLocalProvider(
onCheckedChange = { __TEMPORARY_useJbm = it }, LocalJBMarkdownTreeState provides LocalJBMarkdownTreeState.current.copy(
) fontSizeMultiplier = Gigamoji.useGigamojiForMessage(
message.content
if (__TEMPORARY_useJbm == false) { )
.let {
if (it) 2f else 1f
}
)
) {
Spacer(modifier = Modifier.height(2.dp))
JBMRenderer(message.content)
}
} else {
CompositionLocalProvider( CompositionLocalProvider(
LocalMarkdownTreeConfig provides LocalMarkdownTreeConfig.current.copy( LocalMarkdownTreeConfig provides LocalMarkdownTreeConfig.current.copy(
currentServer = RevoltAPI.channelCache[message.channel]?.server, currentServer = RevoltAPI.channelCache[message.channel]?.server,
@ -390,20 +394,6 @@ fun Message(
Spacer(modifier = Modifier.height(2.dp)) Spacer(modifier = Modifier.height(2.dp))
RichMarkdown(input = message.content) 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 { ListHeader {
Text( Text(
stringResource( stringResource(

View File

@ -1,6 +1,8 @@
package chat.revolt.sheets package chat.revolt.sheets
import androidx.compose.foundation.ExperimentalFoundationApi 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.Arrangement
import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column 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.size
import androidx.compose.foundation.layout.width import androidx.compose.foundation.layout.width
import androidx.compose.foundation.lazy.LazyColumn 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.HorizontalDivider
import androidx.compose.material3.LocalTextStyle import androidx.compose.material3.LocalTextStyle
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.ScrollableTabRow import androidx.compose.material3.ScrollableTabRow
import androidx.compose.material3.Tab import androidx.compose.material3.Tab
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableIntStateOf import androidx.compose.runtime.mutableIntStateOf
import androidx.compose.runtime.mutableStateListOf import androidx.compose.runtime.mutableStateListOf
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.PlatformTextStyle import androidx.compose.ui.text.PlatformTextStyle
import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.font.FontWeight
@ -37,15 +45,18 @@ import androidx.compose.ui.unit.sp
import chat.revolt.R import chat.revolt.R
import chat.revolt.api.REVOLT_FILES import chat.revolt.api.REVOLT_FILES
import chat.revolt.api.RevoltAPI import chat.revolt.api.RevoltAPI
import chat.revolt.internals.text.MessageProcessor
import chat.revolt.api.internals.isUlid import chat.revolt.api.internals.isUlid
import chat.revolt.api.routes.custom.fetchEmoji import chat.revolt.api.routes.custom.fetchEmoji
import chat.revolt.api.routes.user.fetchUser import chat.revolt.api.routes.user.fetchUser
import chat.revolt.api.schemas.Emoji import chat.revolt.api.schemas.Emoji
import chat.revolt.api.schemas.User import chat.revolt.api.schemas.User
import chat.revolt.api.settings.GlobalState
import chat.revolt.components.chat.MemberListItem import chat.revolt.components.chat.MemberListItem
import chat.revolt.components.generic.RemoteImage import chat.revolt.components.generic.RemoteImage
import chat.revolt.components.generic.SheetEnd import chat.revolt.components.generic.SheetEnd
import chat.revolt.internals.text.MessageProcessor
import chat.revolt.persistence.KVStorage
import kotlinx.coroutines.launch
@OptIn(ExperimentalFoundationApi::class) @OptIn(ExperimentalFoundationApi::class)
@Composable @Composable
@ -55,6 +66,9 @@ fun ReactionInfoSheet(messageId: String, emoji: String, onDismiss: () -> Unit) {
val reactions = message.reactions val reactions = message.reactions
val reactionEmoji = reactions?.keys?.toList() val reactionEmoji = reactions?.keys?.toList()
val context = LocalContext.current
val scope = rememberCoroutineScope()
val extendedEmojiInfo = remember(emoji) { mutableStateListOf<Emoji>() } val extendedEmojiInfo = remember(emoji) { mutableStateListOf<Emoji>() }
LaunchedEffect(reactionEmoji) { LaunchedEffect(reactionEmoji) {
@ -120,6 +134,71 @@ fun ReactionInfoSheet(messageId: String, emoji: String, onDismiss: () -> Unit) {
item("info") { item("info") {
val current = reactionEmoji[selectedReactionIndex] 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( Column(
verticalArrangement = Arrangement.spacedBy(16.dp), verticalArrangement = Arrangement.spacedBy(16.dp),
modifier = Modifier.padding( modifier = Modifier.padding(
@ -158,6 +237,12 @@ fun ReactionInfoSheet(messageId: String, emoji: String, onDismiss: () -> Unit) {
) )
), ),
modifier = Modifier modifier = Modifier
.clickable(
interactionSource = interactionSource,
indication = null,
) {
incrementTapCount()
}
.size(64.dp) .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>