feat(experiment): server identity options

Signed-off-by: Infi <infi@infi.sh>
This commit is contained in:
Infi 2025-07-02 23:34:03 +02:00
parent 62a9a6c022
commit 2d8e9c3804
6 changed files with 251 additions and 14 deletions

View File

@ -28,6 +28,7 @@ class ExperimentInstance(default: Boolean) {
object Experiments {
val useKotlinBasedMarkdownRenderer = ExperimentInstance(false)
val usePolar = ExperimentInstance(false)
val enableServerIdentityOptions = ExperimentInstance(false)
suspend fun hydrateWithKv() {
val kvStorage = KVStorage(RevoltApplication.instance)
@ -35,7 +36,7 @@ object Experiments {
if (BuildConfig.DEBUG) {
LoadedSettings.experimentsEnabled = true
} else {
LoadedSettings.experimentsEnabled = kvStorage.getBoolean("experimentsEnabled") ?: false
LoadedSettings.experimentsEnabled = kvStorage.getBoolean("experimentsEnabled") == true
}
useKotlinBasedMarkdownRenderer.setEnabled(
@ -44,5 +45,8 @@ object Experiments {
usePolar.setEnabled(
kvStorage.getBoolean("exp/usePolar") == true
)
enableServerIdentityOptions.setEnabled(
kvStorage.getBoolean("exp/enableServerIdentityOptions") == true
)
}
}

View File

@ -53,8 +53,11 @@ import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.SpanStyle
import androidx.compose.ui.text.buildAnnotatedString
import androidx.compose.ui.text.font.FontStyle
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextDecoration
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
@ -90,6 +93,7 @@ import chat.revolt.internals.text.MessageProcessor
import chat.revolt.markdown.jbm.JBM
import chat.revolt.markdown.jbm.JBMRenderer
import chat.revolt.markdown.jbm.LocalJBMarkdownTreeState
import chat.revolt.persistence.KVStorage
import kotlinx.coroutines.launch
import chat.revolt.api.schemas.Message as MessageSchema
@ -200,6 +204,21 @@ fun Message(
val context = LocalContext.current
val scope = rememberCoroutineScope()
var kv by remember { mutableStateOf<KVStorage?>(null) }
var showUsernameDiscriminator by remember { mutableStateOf(false) }
var ignoreServerAvatar by remember { mutableStateOf(false) }
LaunchedEffect(Unit) {
if (Experiments.enableServerIdentityOptions.isEnabled) {
val userId = author.id ?: return@LaunchedEffect
kv = KVStorage(context)
kv?.let {
showUsernameDiscriminator =
it.getBoolean("exp/serverIdentityOptions/$userId/showUsernameDiscriminator") == true
ignoreServerAvatar =
it.getBoolean("exp/serverIdentityOptions/$userId/ignoreServerAvatar") == true
}
}
}
val attachmentView = rememberLauncherForActivityResult(
contract = ActivityResultContracts.StartActivityForResult(),
@ -321,7 +340,7 @@ fun Message(
username = User.resolveDefaultName(author),
userId = author.id ?: message.id ?: ULID.makeSpecial(0),
avatar = author.avatar,
rawUrl = authorAvatarUrl(message),
rawUrl = if (ignoreServerAvatar) null else authorAvatarUrl(message),
onClick = onAvatarClick
)
}
@ -333,7 +352,24 @@ fun Message(
if (message.tail == false) {
Row(verticalAlignment = Alignment.CenterVertically) {
Text(
text = webhookName ?: authorName(message),
text = buildAnnotatedString {
if (showUsernameDiscriminator) {
pushStyle(
SpanStyle(
color = LocalContentColor.current.copy(
alpha = 0.5f
),
fontWeight = FontWeight.Bold,
textDecoration = TextDecoration.LineThrough
)
)
}
append(webhookName ?: authorName(message))
if (showUsernameDiscriminator) {
pop()
append(" ${author.username ?: "<?>"}#${author.discriminator ?: "0000"}")
}
},
style = LocalTextStyle.current.copy(
fontWeight = FontWeight.Bold,
brush = authorColour(message)

View File

@ -37,6 +37,8 @@ class ExperimentsSettingsScreenViewModel : ViewModel() {
viewModelScope.launch {
useKotlinMdRendererChecked.value = Experiments.useKotlinBasedMarkdownRenderer.isEnabled
usePolarChecked.value = Experiments.usePolar.isEnabled
enableServerIdentityOptionsChecked.value =
Experiments.enableServerIdentityOptions.isEnabled
}
}
@ -84,6 +86,16 @@ class ExperimentsSettingsScreenViewModel : ViewModel() {
usePolarChecked.value = value
}
}
val enableServerIdentityOptionsChecked = mutableStateOf(false)
fun setEnableServerIdentityOptionsChecked(value: Boolean) {
viewModelScope.launch {
kv.set("exp/enableServerIdentityOptions", value)
Experiments.enableServerIdentityOptions.setEnabled(value)
enableServerIdentityOptionsChecked.value = value
}
}
}
@Composable
@ -167,6 +179,22 @@ fun ExperimentsSettingsScreen(
modifier = Modifier.clickable { viewModel.setUsePolarChecked(!viewModel.usePolarChecked.value) }
)
ListItem(
headlineContent = {
Text("Server Identity Options")
},
supportingContent = {
Text("Enable options to control what parts of others' server identities you want to see.")
},
trailingContent = {
Switch(
checked = viewModel.enableServerIdentityOptionsChecked.value,
onCheckedChange = null
)
},
modifier = Modifier.clickable { viewModel.setEnableServerIdentityOptionsChecked(!viewModel.enableServerIdentityOptionsChecked.value) }
)
Subcategory(
title = {
Text("Disable experiments")

View File

@ -0,0 +1,131 @@
package chat.revolt.sheets
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.ListItem
import androidx.compose.material3.ListItemDefaults
import androidx.compose.material3.MaterialTheme
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.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import chat.revolt.api.RevoltAPI
import chat.revolt.persistence.KVStorage
import kotlinx.coroutines.launch
// Internal: Untranslated
@Composable
fun ServerIdentityOptionsSheet(userId: String) {
val user = RevoltAPI.userCache[userId]
if (user == null) {
Text("No such user")
return
}
val context = LocalContext.current
val scope = rememberCoroutineScope()
val kv = remember { KVStorage(context) }
var showUsernameDiscriminator by remember { mutableStateOf(false) }
var ignoreServerAvatar by remember { mutableStateOf(false) }
LaunchedEffect(Unit) {
showUsernameDiscriminator = kv.getBoolean(
"exp/serverIdentityOptions/$userId/showUsernameDiscriminator"
) == true
ignoreServerAvatar = kv.getBoolean(
"exp/serverIdentityOptions/$userId/ignoreServerAvatar"
) == true
}
Column(
verticalArrangement = Arrangement.spacedBy(16.dp),
modifier = Modifier.padding(16.dp),
horizontalAlignment = Alignment.CenterHorizontally,
) {
Column(
modifier = Modifier.fillMaxWidth(),
horizontalAlignment = Alignment.CenterHorizontally,
) {
Text(
text = "Identity Options",
style = MaterialTheme.typography.titleMedium,
textAlign = TextAlign.Center,
modifier = Modifier.fillMaxWidth()
)
Text(
text = "${user.username}#${user.discriminator}",
style = MaterialTheme.typography.titleMedium.copy(
fontWeight = FontWeight.Normal
),
textAlign = TextAlign.Center,
modifier = Modifier.fillMaxWidth()
)
}
Column {
ListItem(
headlineContent = {
Text("Show Username#Tag next to nickname or display name")
},
trailingContent = {
Switch(
checked = showUsernameDiscriminator,
onCheckedChange = null
)
},
modifier = Modifier.clickable {
scope.launch {
kv.set(
"exp/serverIdentityOptions/$userId/showUsernameDiscriminator",
!showUsernameDiscriminator
)
showUsernameDiscriminator = !showUsernameDiscriminator
}
},
colors = ListItemDefaults.colors().copy(
containerColor = Color.Transparent,
)
)
ListItem(
headlineContent = {
Text("Ignore server avatar")
},
trailingContent = {
Switch(
checked = ignoreServerAvatar,
onCheckedChange = null
)
},
modifier = Modifier.clickable {
scope.launch {
kv.set(
"exp/serverIdentityOptions/$userId/ignoreServerAvatar",
!ignoreServerAvatar
)
ignoreServerAvatar = !ignoreServerAvatar
}
},
colors = ListItemDefaults.colors().copy(
containerColor = Color.Transparent,
)
)
}
}
}

View File

@ -45,6 +45,7 @@ import chat.revolt.api.internals.ULID
import chat.revolt.api.internals.solidColor
import chat.revolt.api.routes.user.fetchUserProfile
import chat.revolt.api.schemas.Profile
import chat.revolt.api.settings.Experiments
import chat.revolt.api.settings.FeatureFlags
import chat.revolt.composables.chat.RoleListEntry
import chat.revolt.composables.chat.UserBadgeList
@ -120,6 +121,19 @@ fun UserInfoSheet(
}
}
var showServerIdentityOptions by remember { mutableStateOf(false) }
if (showServerIdentityOptions) {
val sheetState = rememberModalBottomSheetState(true)
ModalBottomSheet(
sheetState = sheetState,
onDismissRequest = { showServerIdentityOptions = false }
) {
ServerIdentityOptionsSheet(
userId = user.id!!
)
}
}
LazyVerticalStaggeredGrid(
columns = StaggeredGridCells.Fixed(2),
horizontalArrangement = Arrangement.spacedBy(16.dp),
@ -129,17 +143,31 @@ fun UserInfoSheet(
item(key = "overview", span = StaggeredGridItemSpan.FullLine) {
Box {
RawUserOverview(user, profile, internalPadding = false)
if (FeatureFlags.userCardsGranted) {
SmallFloatingActionButton(
onClick = { showUserCard = true },
modifier = Modifier
.align(Alignment.TopEnd)
.padding(top = 8.dp, end = 8.dp)
) {
Icon(
painter = painterResource(R.drawable.ic_badge_account_horizontal_24dp),
contentDescription = null
)
Row(
modifier = Modifier
.align(Alignment.TopEnd)
.padding(top = 8.dp, end = 8.dp)
) {
if (Experiments.enableServerIdentityOptions.isEnabled) {
SmallFloatingActionButton(
onClick = { showServerIdentityOptions = true },
) {
Icon(
painter = painterResource(R.drawable.icn_psychology_alt_24dp),
contentDescription = null
)
}
}
if (FeatureFlags.userCardsGranted) {
SmallFloatingActionButton(
onClick = { showUserCard = true },
) {
Icon(
painter = painterResource(R.drawable.ic_badge_account_horizontal_24dp),
contentDescription = null
)
}
}
}
}

View File

@ -0,0 +1,10 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="960"
android:viewportHeight="960"
android:tint="?attr/colorControlNormal">
<path
android:fillColor="@android:color/white"
android:pathData="M240,880L240,708Q183,656 151.5,586.5Q120,517 120,440Q120,290 225,185Q330,80 480,80Q605,80 701.5,153.5Q798,227 827,345L879,550Q884,569 872,584.5Q860,600 840,600L760,600L760,720Q760,753 736.5,776.5Q713,800 680,800L600,800L600,880L520,880L520,720L680,720Q680,720 680,720Q680,720 680,720L680,520L788,520L750,365Q727,274 652,217Q577,160 480,160Q364,160 282,241Q200,322 200,438Q200,498 224.5,552Q249,606 294,648L320,672L320,880L240,880ZM494,520L494,520L494,520Q494,520 494,520Q494,520 494,520Q494,520 494,520Q494,520 494,520Q494,520 494,520Q494,520 494,520L494,520L494,520L494,520Q494,520 494,520Q494,520 494,520L494,520L494,520ZM480,640Q497,640 508.5,628.5Q520,617 520,600Q520,583 508.5,571.5Q497,560 480,560Q463,560 451.5,571.5Q440,583 440,600Q440,617 451.5,628.5Q463,640 480,640ZM450,512L511,512Q511,487 517.5,471.5Q524,456 544,434Q562,414 579,393.5Q596,373 596,340Q596,298 563.5,269Q531,240 483,240Q443,240 410.5,263Q378,286 365,323L420,346Q427,324 444.5,310.5Q462,297 483,297Q505,297 519.5,309Q534,321 534,340Q534,361 521.5,377.5Q509,394 492,411Q472,432 461,453Q450,474 450,512Z"/>
</vector>