Bypass single-pane scaffold for RTL.

Co-authored-by: Greyson Parrelli <greyson@signal.org>
This commit is contained in:
Alex Hart
2026-05-07 16:42:26 -03:00
committed by GitHub
parent e518eca9a1
commit 413962a093
6 changed files with 61 additions and 15 deletions
@@ -207,12 +207,21 @@ class InternalSettingsFragment : DSLSettingsFragment(R.string.preferences__inter
switchPref(
title = DSLSettingsText.from("Force split pane UI on phones."),
isEnabled = !state.forceSinglePane,
isChecked = state.forceSplitPane,
onClick = {
viewModel.setForceSplitPane(!state.forceSplitPane)
}
)
switchPref(
title = DSLSettingsText.from("Force single-pane on newer devices."),
isChecked = state.forceSinglePane,
onClick = {
viewModel.setForceSinglePane(!state.forceSinglePane)
}
)
clickPref(
title = DSLSettingsText.from("Display enable permission sheet"),
onClick = {
@@ -31,6 +31,7 @@ data class InternalSettingsState(
val hasPendingOneTimeDonation: Boolean,
val hevcEncoding: Boolean,
val forceSplitPane: Boolean,
val forceSinglePane: Boolean,
val useNewMediaActivity: Boolean,
val disableInternalUser: Boolean
)
@@ -203,6 +203,7 @@ class InternalSettingsViewModel(private val repository: InternalSettingsReposito
hasPendingOneTimeDonation = SignalStore.inAppPayments.getPendingOneTimeDonation() != null,
hevcEncoding = SignalStore.internal.hevcEncoding,
forceSplitPane = SignalStore.internal.forceSplitPane,
forceSinglePane = SignalStore.internal.forceSinglePane,
useNewMediaActivity = SignalStore.internal.useNewMediaActivity,
disableInternalUser = RemoteConfig.internalUserDisabled
)
@@ -225,6 +226,11 @@ class InternalSettingsViewModel(private val repository: InternalSettingsReposito
refresh()
}
fun setForceSinglePane(forceSinglePane: Boolean) {
SignalStore.internal.forceSinglePane = forceSinglePane
refresh()
}
class Factory(private val repository: InternalSettingsRepository) : ViewModelProvider.Factory {
override fun <T : ViewModel> create(modelClass: Class<T>): T {
return requireNotNull(modelClass.cast(InternalSettingsViewModel(repository)))
@@ -32,6 +32,7 @@ class InternalValues internal constructor(store: KeyValueStore) : SignalStoreVal
const val WEB_SOCKET_SHADOWING_STATS: String = "internal.web_socket_shadowing_stats"
const val ENCODE_HEVC: String = "internal.hevc_encoding"
const val FORCE_SPLIT_PANE_ON_COMPACT_LANDSCAPE: String = "internal.force.split.pane.on.compact.landscape.ui"
const val FORCE_SINGLE_PANE_ON_ALL_DEVICES: String = "internal.force_single_pane_on_all_devices"
const val SHOW_ARCHIVE_STATE_HINT: String = "internal.show_archive_state_hint"
const val INCLUDE_DEBUGLOG_IN_BACKUP: String = "internal.include_debuglog_in_backup"
const val IMPORTED_BACKUP_DEBUG_INFO: String = "internal.imported_backup_debug_info"
@@ -48,6 +49,11 @@ class InternalValues internal constructor(store: KeyValueStore) : SignalStoreVal
*/
var forceSplitPane by booleanValue(FORCE_SPLIT_PANE_ON_COMPACT_LANDSCAPE, false).falseForExternalUsers()
/**
* Force single-pane on all devices
*/
var forceSinglePane by booleanValue(FORCE_SINGLE_PANE_ON_ALL_DEVICES, false).falseForExternalUsers()
var useNewMediaActivity by booleanValue(USE_NEW_MEDIA_ACTIVITY, false).falseForExternalUsers()
/**
@@ -49,9 +49,11 @@ import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.graphicsLayer
import androidx.compose.ui.layout.layout
import androidx.compose.ui.platform.LocalConfiguration
import androidx.compose.ui.platform.LocalInspectionMode
import androidx.compose.ui.platform.LocalLayoutDirection
import androidx.compose.ui.platform.LocalResources
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.LayoutDirection
import androidx.compose.ui.unit.dp
import androidx.compose.ui.zIndex
import kotlinx.coroutines.launch
@@ -61,6 +63,7 @@ import org.signal.core.ui.compose.Previews
import org.signal.core.ui.getWindowBreakpoint
import org.signal.core.ui.isWidthExpanded
import org.signal.core.ui.rememberIsSplitPane
import org.thoughtcrime.securesms.keyvalue.SignalStore
import org.thoughtcrime.securesms.main.MainFloatingActionButtonsCallback
import org.thoughtcrime.securesms.main.MainNavigationBar
import org.thoughtcrime.securesms.main.MainNavigationRail
@@ -132,8 +135,14 @@ fun AppScaffold(
contentWindowInsets: WindowInsets = WindowInsets.systemBars,
animatorFactory: AppScaffoldAnimationStateFactory = AppScaffoldAnimationStateFactory.Default
) {
val useSimpleScaffold = navigator.scaffoldDirective.maxHorizontalPartitions == 1 && Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU
if (useSimpleScaffold) {
val isForceSinglePane = if (LocalInspectionMode.current) {
false
} else {
SignalStore.internal.forceSinglePane
}
val useSimpleScaffold = isForceSinglePane || (navigator.scaffoldDirective.maxHorizontalPartitions == 1 && Build.VERSION.SDK_INT < 33)
if (useSimpleScaffold && LocalLayoutDirection.current != LayoutDirection.Rtl) {
SinglePaneAppScaffold(
navigator = navigator,
modifier = modifier,
@@ -142,7 +151,8 @@ fun AppScaffold(
secondaryContent = secondaryContent,
bottomNavContent = bottomNavContent,
snackbarHost = snackbarHost,
contentWindowInsets = contentWindowInsets
contentWindowInsets = contentWindowInsets,
animatorFactory = animatorFactory
)
} else {
AdaptiveAppScaffold(
@@ -307,10 +317,14 @@ private fun SinglePaneAppScaffold(
secondaryContent: @Composable () -> Unit,
bottomNavContent: @Composable () -> Unit = {},
snackbarHost: @Composable () -> Unit = {},
contentWindowInsets: WindowInsets = WindowInsets.systemBars
contentWindowInsets: WindowInsets = WindowInsets.systemBars,
animatorFactory: AppScaffoldAnimationStateFactory = AppScaffoldAnimationStateFactory.Default
) {
val showDetail = navigator.scaffoldValue.primary == PaneAdaptedValue.Expanded
val coroutineScope = rememberCoroutineScope()
val isRtl = LocalLayoutDirection.current == LayoutDirection.Rtl
val directionMultiplier = if (isRtl) -1 else 1
val skipSlide = AppScaffoldNavigator.NavigationState.ENTER !in animatorFactory.enabledStates
BackHandler(enabled = navigator.canNavigateBack()) {
coroutineScope.launch { navigator.navigateBack() }
@@ -326,12 +340,12 @@ private fun SinglePaneAppScaffold(
AnimatedContent(
targetState = showDetail,
transitionSpec = {
val transform = if (targetState) {
slideInHorizontally(animationSpec = AppScaffoldAnimationDefaults.tween()) { fullWidth -> fullWidth } togetherWith
slideOutHorizontally(animationSpec = AppScaffoldAnimationDefaults.tween()) { fullWidth -> -fullWidth }
} else {
slideInHorizontally(animationSpec = AppScaffoldAnimationDefaults.tween()) { fullWidth -> -fullWidth } togetherWith
slideOutHorizontally(animationSpec = AppScaffoldAnimationDefaults.tween()) { fullWidth -> fullWidth }
val transform = when {
skipSlide -> EnterTransition.None togetherWith ExitTransition.None
targetState -> slideInHorizontally(animationSpec = AppScaffoldAnimationDefaults.tween()) { fullWidth -> fullWidth * directionMultiplier } togetherWith
slideOutHorizontally(animationSpec = AppScaffoldAnimationDefaults.tween()) { fullWidth -> -fullWidth * directionMultiplier }
else -> slideInHorizontally(animationSpec = AppScaffoldAnimationDefaults.tween()) { fullWidth -> -fullWidth * directionMultiplier } togetherWith
slideOutHorizontally(animationSpec = AppScaffoldAnimationDefaults.tween()) { fullWidth -> fullWidth * directionMultiplier }
}
transform using SizeTransform(clip = false) { _, _ -> snap() }
},
@@ -19,7 +19,9 @@ import androidx.compose.runtime.State
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.ui.platform.LocalLayoutDirection
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.LayoutDirection
import androidx.compose.ui.unit.dp
private const val SEEK_DAMPING_RATIO = Spring.DampingRatioNoBouncy
@@ -76,9 +78,11 @@ fun ThreePaneScaffoldPaneScope.animateFloat(
@OptIn(ExperimentalMaterial3AdaptiveApi::class)
@Composable
fun ThreePaneScaffoldPaneScope.defaultListInitAnimationState(): AppScaffoldAnimationState {
val directionMultiplier = if (LocalLayoutDirection.current == LayoutDirection.Rtl) -1 else 1
val offset = animateDp(
targetWhenHiding = {
-AppScaffoldAnimationDefaults.InitAnimationOffset
-AppScaffoldAnimationDefaults.InitAnimationOffset * directionMultiplier
},
targetWhenShowing = {
0.dp
@@ -100,6 +104,8 @@ fun ThreePaneScaffoldPaneScope.defaultListInitAnimationState(): AppScaffoldAnima
@OptIn(ExperimentalMaterial3AdaptiveApi::class)
@Composable
fun ThreePaneScaffoldPaneScope.defaultListSeekAnimationState(): AppScaffoldAnimationState {
val directionMultiplier = if (LocalLayoutDirection.current == LayoutDirection.Rtl) -1 else 1
val scale = animateFloat(
transitionSpec = {
appScaffoldSeekSpring()
@@ -112,7 +118,7 @@ fun ThreePaneScaffoldPaneScope.defaultListSeekAnimationState(): AppScaffoldAnima
transitionSpec = {
appScaffoldSeekSpring()
},
targetWhenHiding = { -(88.dp) },
targetWhenHiding = { -(88.dp) * directionMultiplier },
targetWhenShowing = { 0.dp }
)
@@ -160,9 +166,11 @@ fun ThreePaneScaffoldPaneScope.defaultListReleaseAnimationState(from: AppScaffol
@OptIn(ExperimentalMaterial3AdaptiveApi::class)
@Composable
fun ThreePaneScaffoldPaneScope.defaultDetailInitAnimationState(): AppScaffoldAnimationState {
val directionMultiplier = if (LocalLayoutDirection.current == LayoutDirection.Rtl) -1 else 1
val offset = animateDp(
targetWhenHiding = {
AppScaffoldAnimationDefaults.InitAnimationOffset
AppScaffoldAnimationDefaults.InitAnimationOffset * directionMultiplier
},
targetWhenShowing = {
0.dp
@@ -184,6 +192,8 @@ fun ThreePaneScaffoldPaneScope.defaultDetailInitAnimationState(): AppScaffoldAni
@OptIn(ExperimentalMaterial3AdaptiveApi::class)
@Composable
fun ThreePaneScaffoldPaneScope.defaultDetailSeekAnimationState(): AppScaffoldAnimationState {
val directionMultiplier = if (LocalLayoutDirection.current == LayoutDirection.Rtl) -1 else 1
val scale = animateFloat(
transitionSpec = {
appScaffoldSeekSpring()
@@ -197,7 +207,7 @@ fun ThreePaneScaffoldPaneScope.defaultDetailSeekAnimationState(): AppScaffoldAni
appScaffoldSeekSpring()
},
targetWhenShowing = { 0.dp },
targetWhenHiding = { 88.dp }
targetWhenHiding = { 88.dp * directionMultiplier }
)
val roundedCorners = animateDp(