mirror of
https://github.com/signalapp/Signal-Android.git
synced 2026-04-02 00:17:41 +01:00
Add a new internal-only 'labs' setting screen.
This commit is contained in:
committed by
Michelle Tang
parent
a37680685f
commit
95149764eb
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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)) }
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
)
|
||||
@@ -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
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -16,9 +16,9 @@ import io.reactivex.rxjava3.kotlin.subscribeBy
|
||||
import org.signal.core.util.concurrent.LifecycleDisposable
|
||||
import org.signal.core.util.logging.Log
|
||||
import org.thoughtcrime.securesms.R
|
||||
import org.thoughtcrime.securesms.keyvalue.SignalStore
|
||||
import org.thoughtcrime.securesms.messagerequests.MessageRequestState
|
||||
import org.thoughtcrime.securesms.recipients.Recipient
|
||||
import org.thoughtcrime.securesms.util.RemoteConfig
|
||||
|
||||
/**
|
||||
* Delegate object for managing the conversation options menu
|
||||
@@ -164,8 +164,8 @@ internal object ConversationOptionsMenu {
|
||||
hideMenuItem(menu, R.id.menu_add_shortcut)
|
||||
}
|
||||
|
||||
if (RemoteConfig.internalUser) {
|
||||
menu.findItem(R.id.menu_export)?.title = menu.findItem(R.id.menu_export)?.title.toString() + " (Internal Only)"
|
||||
if (SignalStore.labs.individualChatPlaintextExport) {
|
||||
menu.findItem(R.id.menu_export)?.title = menu.findItem(R.id.menu_export)?.title.toString() + " (Labs)"
|
||||
} else {
|
||||
hideMenuItem(menu, R.id.menu_export)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
package org.thoughtcrime.securesms.keyvalue
|
||||
|
||||
import org.thoughtcrime.securesms.util.RemoteConfig
|
||||
|
||||
class LabsValues internal constructor(store: KeyValueStore) : SignalStoreValues(store) {
|
||||
companion object {
|
||||
const val INDIVIDUAL_CHAT_PLAINTEXT_EXPORT: String = "labs.individual_chat_plaintext_export"
|
||||
const val STORY_ARCHIVE: String = "labs.story_archive"
|
||||
}
|
||||
|
||||
public override fun onFirstEverAppLaunch() = Unit
|
||||
|
||||
public override fun getKeysToIncludeInBackup(): List<String> = emptyList()
|
||||
|
||||
var individualChatPlaintextExport by booleanValue(INDIVIDUAL_CHAT_PLAINTEXT_EXPORT, true).falseForExternalUsers()
|
||||
|
||||
var storyArchive by booleanValue(STORY_ARCHIVE, true).falseForExternalUsers()
|
||||
|
||||
private fun SignalStoreValueDelegate<Boolean>.falseForExternalUsers(): SignalStoreValueDelegate<Boolean> {
|
||||
return this.map { actualValue -> RemoteConfig.internalUser && actualValue }
|
||||
}
|
||||
}
|
||||
@@ -38,6 +38,7 @@ class SignalStore(context: Application, private val store: KeyValueStore) {
|
||||
val apkUpdateValues = ApkUpdateValues(store)
|
||||
val backupValues = BackupValues(store)
|
||||
val callQualityValues = CallQualityValues(store)
|
||||
val labsValues = LabsValues(store)
|
||||
|
||||
val plainTextValues = PlainTextSharedPrefsDataStore(context)
|
||||
|
||||
@@ -86,6 +87,7 @@ class SignalStore(context: Application, private val store: KeyValueStore) {
|
||||
apkUpdate.onFirstEverAppLaunch()
|
||||
backup.onFirstEverAppLaunch()
|
||||
callQuality.onFirstEverAppLaunch()
|
||||
labs.onFirstEverAppLaunch()
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
@@ -118,7 +120,8 @@ class SignalStore(context: Application, private val store: KeyValueStore) {
|
||||
story.keysToIncludeInBackup +
|
||||
apkUpdate.keysToIncludeInBackup +
|
||||
backup.keysToIncludeInBackup +
|
||||
callQuality.keysToIncludeInBackup
|
||||
callQuality.keysToIncludeInBackup +
|
||||
labs.keysToIncludeInBackup
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -274,6 +277,11 @@ class SignalStore(context: Application, private val store: KeyValueStore) {
|
||||
val callQuality: CallQualityValues
|
||||
get() = instance!!.callQualityValues
|
||||
|
||||
@JvmStatic
|
||||
@get:JvmName("labs")
|
||||
val labs: LabsValues
|
||||
get() = instance!!.labsValues
|
||||
|
||||
val groupsV2AciAuthorizationCache: GroupsV2AuthorizationSignalStoreCache
|
||||
get() = GroupsV2AuthorizationSignalStoreCache.createAciCache(instance!!.store)
|
||||
|
||||
|
||||
@@ -81,9 +81,9 @@ import org.thoughtcrime.securesms.calls.log.CallLogFilter
|
||||
import org.thoughtcrime.securesms.components.compose.ActionModeTopBar
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.BadgeImageSmall
|
||||
import org.thoughtcrime.securesms.conversationlist.model.ConversationFilter
|
||||
import org.thoughtcrime.securesms.keyvalue.SignalStore
|
||||
import org.thoughtcrime.securesms.recipients.Recipient
|
||||
import org.thoughtcrime.securesms.recipients.rememberRecipientField
|
||||
import org.thoughtcrime.securesms.util.RemoteConfig
|
||||
|
||||
interface MainToolbarCallback {
|
||||
fun onNewGroupClick()
|
||||
@@ -408,7 +408,7 @@ private fun PrimaryToolbar(
|
||||
NotificationProfileAction(state, callback)
|
||||
ProxyAction(state, callback)
|
||||
|
||||
if (state.destination == MainNavigationListLocation.STORIES && RemoteConfig.internalUser) {
|
||||
if (state.destination == MainNavigationListLocation.STORIES && SignalStore.labs.storyArchive) {
|
||||
IconButtons.IconButton(
|
||||
onClick = callback::onStoryArchiveClick
|
||||
) {
|
||||
|
||||
@@ -27,7 +27,6 @@ import org.thoughtcrime.securesms.stories.archive.StoryArchiveDuration
|
||||
import org.thoughtcrime.securesms.stories.dialogs.StoryDialogs
|
||||
import org.thoughtcrime.securesms.stories.settings.create.CreateStoryFlowDialogFragment
|
||||
import org.thoughtcrime.securesms.stories.settings.create.CreateStoryWithViewersFragment
|
||||
import org.thoughtcrime.securesms.util.RemoteConfig
|
||||
import org.thoughtcrime.securesms.util.adapter.mapping.MappingAdapter
|
||||
import org.thoughtcrime.securesms.util.adapter.mapping.PagingMappingAdapter
|
||||
import org.thoughtcrime.securesms.util.navigation.safeNavigate
|
||||
@@ -170,7 +169,7 @@ class StoriesPrivacySettingsFragment :
|
||||
}
|
||||
)
|
||||
|
||||
if (RemoteConfig.internalUser) {
|
||||
if (SignalStore.labs.storyArchive) {
|
||||
dividerPref()
|
||||
|
||||
sectionHeaderPref(R.string.StoryArchive__archive)
|
||||
|
||||
@@ -134,6 +134,13 @@
|
||||
app:exitAnim="@anim/fragment_open_exit"
|
||||
app:popEnterAnim="@anim/fragment_close_enter"
|
||||
app:popExitAnim="@anim/fragment_close_exit" />
|
||||
<action
|
||||
android:id="@+id/action_appSettingsFragment_to_labsSettingsFragment"
|
||||
app:destination="@id/labsSettingsFragment"
|
||||
app:enterAnim="@anim/fragment_open_enter"
|
||||
app:exitAnim="@anim/fragment_open_exit"
|
||||
app:popEnterAnim="@anim/fragment_close_enter"
|
||||
app:popExitAnim="@anim/fragment_close_exit" />
|
||||
<action
|
||||
android:id="@+id/action_appSettingsFragment_to_manageDonationsFragment"
|
||||
app:destination="@id/manageDonationsFragment"
|
||||
@@ -823,6 +830,13 @@
|
||||
|
||||
<!-- endregion -->
|
||||
|
||||
<!-- Labs Settings -->
|
||||
|
||||
<fragment
|
||||
android:id="@+id/labsSettingsFragment"
|
||||
android:name="org.thoughtcrime.securesms.components.settings.app.labs.LabsSettingsFragment"
|
||||
android:label="labs_settings_fragment" />
|
||||
|
||||
<!-- Internal Settings -->
|
||||
|
||||
<fragment
|
||||
|
||||
Reference in New Issue
Block a user