Add a new internal-only 'labs' setting screen.

This commit is contained in:
Greyson Parrelli
2026-03-17 10:45:49 -04:00
committed by Michelle Tang
parent a37680685f
commit 95149764eb
12 changed files with 241 additions and 8 deletions

View File

@@ -109,6 +109,7 @@ class AppSettingsFragment : ComposeFragment(), Callbacks {
is AppSettingsRoute.Payments -> findNavController().safeNavigate(R.id.action_appSettingsFragment_to_paymentsActivity)
is AppSettingsRoute.HelpRoute.Settings -> findNavController().safeNavigate(R.id.action_appSettingsFragment_to_helpSettingsFragment)
is AppSettingsRoute.Invite -> findNavController().safeNavigate(R.id.action_appSettingsFragment_to_inviteFragment)
is AppSettingsRoute.LabsRoute.Labs -> findNavController().safeNavigate(R.id.action_appSettingsFragment_to_labsSettingsFragment)
is AppSettingsRoute.InternalRoute.Internal -> findNavController().safeNavigate(R.id.action_appSettingsFragment_to_internalSettingsFragment)
is AppSettingsRoute.AccountRoute.ManageProfile -> findNavController().safeNavigate(R.id.action_appSettingsFragment_to_manageProfileActivity)
is AppSettingsRoute.UsernameLinkRoute.UsernameLink -> findNavController().safeNavigate(R.id.action_appSettingsFragment_to_usernameLinkSettingsFragment)
@@ -531,9 +532,20 @@ private fun AppSettingsContent(
Dividers.Default()
}
item {
Rows.TextRow(
text = "Labs",
icon = painterResource(R.drawable.symbol_flash_24),
onClick = {
callbacks.navigate(AppSettingsRoute.LabsRoute.Labs)
}
)
}
item {
Rows.TextRow(
text = stringResource(R.string.preferences__internal_preferences),
icon = painterResource(R.drawable.symbol_key_24),
onClick = {
callbacks.navigate(AppSettingsRoute.InternalRoute.Internal)
}

View File

@@ -0,0 +1,11 @@
/*
* Copyright 2025 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.thoughtcrime.securesms.components.settings.app.labs
sealed interface LabsSettingsEvents {
data class ToggleIndividualChatPlaintextExport(val enabled: Boolean) : LabsSettingsEvents
data class ToggleStoryArchive(val enabled: Boolean) : LabsSettingsEvents
}

View File

@@ -0,0 +1,111 @@
/*
* Copyright 2025 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.thoughtcrime.securesms.components.settings.app.labs
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.unit.dp
import androidx.fragment.app.viewModels
import androidx.navigation.fragment.findNavController
import org.signal.core.ui.compose.ComposeFragment
import org.signal.core.ui.compose.Rows
import org.signal.core.ui.compose.Scaffolds
import org.signal.core.ui.compose.SignalIcons
import org.signal.core.ui.compose.horizontalGutters
import org.signal.core.ui.compose.theme.SignalTheme
import org.thoughtcrime.securesms.R
class LabsSettingsFragment : ComposeFragment() {
private val viewModel: LabsSettingsViewModel by viewModels()
@Composable
override fun FragmentContent() {
val state by viewModel.state.collectAsState()
LabsSettingsContent(
state = state,
onEvent = viewModel::onEvent,
onNavigationClick = { findNavController().popBackStack() }
)
}
}
@Composable
private fun LabsSettingsContent(
state: LabsSettingsState,
onEvent: (LabsSettingsEvents) -> Unit,
onNavigationClick: () -> Unit
) {
Scaffolds.Settings(
title = "Labs",
navigationContentDescription = "Go back",
navigationIcon = SignalIcons.ArrowStart.imageVector,
onNavigationClick = onNavigationClick
) { contentPadding ->
LazyColumn(
contentPadding = contentPadding
) {
item {
Row(
modifier = Modifier
.fillMaxWidth()
.horizontalGutters()
.padding(vertical = 16.dp)
.background(
color = SignalTheme.colors.colorSurface2,
shape = RoundedCornerShape(12.dp)
)
.padding(16.dp)
) {
Icon(
painter = painterResource(R.drawable.symbol_info_fill_24),
contentDescription = null,
tint = MaterialTheme.colorScheme.onSurfaceVariant,
modifier = Modifier.size(20.dp)
)
Text(
text = "These are internal-only pre-release features. They are all in various unfinished states. They may need more polish, finalized designs, or cross-client compatibility before they can be released.",
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onSurfaceVariant,
modifier = Modifier.padding(start = 12.dp)
)
}
}
item {
Rows.ToggleRow(
checked = state.individualChatPlaintextExport,
text = "Individual Chat Plaintext Export",
label = "Enable exporting individual chats as a collection of human-readable plaintext files. New entry in the three-dot menu in the chat screen.",
onCheckChanged = { onEvent(LabsSettingsEvents.ToggleIndividualChatPlaintextExport(it)) }
)
}
item {
Rows.ToggleRow(
checked = state.storyArchive,
text = "Story Archive",
label = "Keep your own stories for longer and view them later. A new button on the toolbar on the stories tab.",
onCheckChanged = { onEvent(LabsSettingsEvents.ToggleStoryArchive(it)) }
)
}
}
}
}

View File

@@ -0,0 +1,14 @@
/*
* Copyright 2025 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.thoughtcrime.securesms.components.settings.app.labs
import androidx.compose.runtime.Immutable
@Immutable
data class LabsSettingsState(
val individualChatPlaintextExport: Boolean = false,
val storyArchive: Boolean = false
)

View File

@@ -0,0 +1,37 @@
/*
* Copyright 2025 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.thoughtcrime.securesms.components.settings.app.labs
import androidx.lifecycle.ViewModel
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import org.thoughtcrime.securesms.keyvalue.SignalStore
class LabsSettingsViewModel : ViewModel() {
private val _state = MutableStateFlow(loadState())
val state: StateFlow<LabsSettingsState> = _state
fun onEvent(event: LabsSettingsEvents) {
when (event) {
is LabsSettingsEvents.ToggleIndividualChatPlaintextExport -> {
SignalStore.labs.individualChatPlaintextExport = event.enabled
_state.value = _state.value.copy(individualChatPlaintextExport = event.enabled)
}
is LabsSettingsEvents.ToggleStoryArchive -> {
SignalStore.labs.storyArchive = event.enabled
_state.value = _state.value.copy(storyArchive = event.enabled)
}
}
}
private fun loadState(): LabsSettingsState {
return LabsSettingsState(
individualChatPlaintextExport = SignalStore.labs.individualChatPlaintextExport,
storyArchive = SignalStore.labs.storyArchive
)
}
}

View File

@@ -114,6 +114,11 @@ sealed interface AppSettingsRoute : Parcelable {
data object Featured : DonationsRoute
}
@Parcelize
sealed interface LabsRoute : AppSettingsRoute {
data object Labs : LabsRoute
}
@Parcelize
sealed interface InternalRoute : AppSettingsRoute {
data object Internal : InternalRoute