diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index 97ffa9d1e9..6043530cd1 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -1076,7 +1076,7 @@
diff --git a/app/src/main/java/org/thoughtcrime/securesms/MainActivity.kt b/app/src/main/java/org/thoughtcrime/securesms/MainActivity.kt
index 2449cb674b..57a85a1cdb 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/MainActivity.kt
+++ b/app/src/main/java/org/thoughtcrime/securesms/MainActivity.kt
@@ -29,11 +29,12 @@ import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.displayCutoutPadding
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.systemBarsPadding
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveApi
import androidx.compose.material3.adaptive.currentWindowAdaptiveInfo
import androidx.compose.material3.adaptive.layout.ThreePaneScaffoldRole
-import androidx.compose.material3.adaptive.layout.calculatePaneScaffoldDirectiveWithTwoPanesOnMediumWidth
+import androidx.compose.material3.adaptive.layout.calculatePaneScaffoldDirective
import androidx.compose.material3.adaptive.navigation.rememberListDetailPaneScaffoldNavigator
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
@@ -93,6 +94,7 @@ import org.thoughtcrime.securesms.main.MainToolbar
import org.thoughtcrime.securesms.main.MainToolbarCallback
import org.thoughtcrime.securesms.main.MainToolbarMode
import org.thoughtcrime.securesms.main.MainToolbarViewModel
+import org.thoughtcrime.securesms.main.NavigationBarSpacerCompat
import org.thoughtcrime.securesms.main.SnackbarState
import org.thoughtcrime.securesms.mediasend.camerax.CameraXUtil
import org.thoughtcrime.securesms.mediasend.v2.MediaSelectionActivity
@@ -236,9 +238,10 @@ class MainActivity : PassphraseRequiredActivity(), VoiceNoteMediaControllerOwner
MainContainer {
val scaffoldNavigator = rememberListDetailPaneScaffoldNavigator(
- scaffoldDirective = calculatePaneScaffoldDirectiveWithTwoPanesOnMediumWidth(
+ scaffoldDirective = calculatePaneScaffoldDirective(
currentWindowAdaptiveInfo()
).copy(
+ maxHorizontalPartitions = if (windowSizeClass.isSplitPane()) 2 else 1,
horizontalPartitionSpacerSize = contentLayoutData.partitionWidth,
defaultPanePreferredWidth = contentLayoutData.rememberDefaultPanePreferredWidth(maxWidth)
)
@@ -258,10 +261,20 @@ class MainActivity : PassphraseRequiredActivity(), VoiceNoteMediaControllerOwner
navigator = scaffoldNavigator,
bottomNavContent = {
if (isNavigationVisible) {
- MainNavigationBar(
- state = mainNavigationState,
- onDestinationSelected = mainNavigationCallback
- )
+ Column(
+ modifier = Modifier
+ .clip(contentLayoutData.navigationBarShape)
+ .background(color = SignalTheme.colors.colorSurface2)
+ ) {
+ MainNavigationBar(
+ state = mainNavigationState,
+ onDestinationSelected = mainNavigationCallback
+ )
+
+ if (!windowSizeClass.isSplitPane()) {
+ NavigationBarSpacerCompat()
+ }
+ }
}
},
navRailContent = {
@@ -360,11 +373,6 @@ class MainActivity : PassphraseRequiredActivity(), VoiceNoteMediaControllerOwner
@Composable
private fun MainContainer(content: @Composable BoxWithConstraintsScope.() -> Unit) {
val windowSizeClass = WindowSizeClass.rememberWindowSizeClass()
- val modifier = if (windowSizeClass.isLandscape()) {
- Modifier.displayCutoutPadding()
- } else {
- Modifier
- }
SignalTheme(isDarkMode = DynamicTheme.isDarkTheme(this)) {
val backgroundColor = if (windowSizeClass.isCompact()) {
@@ -373,6 +381,12 @@ class MainActivity : PassphraseRequiredActivity(), VoiceNoteMediaControllerOwner
SignalTheme.colors.colorSurface1
}
+ val modifier = if (windowSizeClass.isSplitPane()) {
+ Modifier.systemBarsPadding().displayCutoutPadding()
+ } else {
+ Modifier
+ }
+
BoxWithConstraints(
modifier = Modifier
.background(color = backgroundColor)
diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/internal/InternalSettingsFragment.kt b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/internal/InternalSettingsFragment.kt
index e340a3a283..0026792b58 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/internal/InternalSettingsFragment.kt
+++ b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/internal/InternalSettingsFragment.kt
@@ -174,6 +174,16 @@ class InternalSettingsFragment : DSLSettingsFragment(R.string.preferences__inter
}
)
+ switchPref(
+ isEnabled = state.largeScreenUi,
+ title = DSLSettingsText.from("Force split pane UI on landscape phones."),
+ summary = DSLSettingsText.from("This setting requires split pane UI to be enabled."),
+ isChecked = state.forceSplitPaneOnCompactLandscape,
+ onClick = {
+ viewModel.setForceSplitPaneOnCompactLandscape(!state.forceSplitPaneOnCompactLandscape)
+ }
+ )
+
sectionHeaderPref(DSLSettingsText.from("Playgrounds"))
clickPref(
diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/internal/InternalSettingsState.kt b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/internal/InternalSettingsState.kt
index c923829fa9..298f7e2cff 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/internal/InternalSettingsState.kt
+++ b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/internal/InternalSettingsState.kt
@@ -26,5 +26,6 @@ data class InternalSettingsState(
val hasPendingOneTimeDonation: Boolean,
val hevcEncoding: Boolean,
val newCallingUi: Boolean,
- val largeScreenUi: Boolean
+ val largeScreenUi: Boolean,
+ val forceSplitPaneOnCompactLandscape: Boolean
)
diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/internal/InternalSettingsViewModel.kt b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/internal/InternalSettingsViewModel.kt
index cc45321104..e8e32be73f 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/internal/InternalSettingsViewModel.kt
+++ b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/internal/InternalSettingsViewModel.kt
@@ -167,7 +167,8 @@ class InternalSettingsViewModel(private val repository: InternalSettingsReposito
hasPendingOneTimeDonation = SignalStore.inAppPayments.getPendingOneTimeDonation() != null,
hevcEncoding = SignalStore.internal.hevcEncoding,
newCallingUi = SignalStore.internal.newCallingUi,
- largeScreenUi = SignalStore.internal.largeScreenUi
+ largeScreenUi = SignalStore.internal.largeScreenUi,
+ forceSplitPaneOnCompactLandscape = SignalStore.internal.forceSplitPaneOnCompactLandscape
)
fun onClearOnboardingState() {
@@ -188,6 +189,11 @@ class InternalSettingsViewModel(private val repository: InternalSettingsReposito
refresh()
}
+ fun setForceSplitPaneOnCompactLandscape(forceSplitPaneOnCompactLandscape: Boolean) {
+ SignalStore.internal.forceSplitPaneOnCompactLandscape = forceSplitPaneOnCompactLandscape
+ refresh()
+ }
+
class Factory(private val repository: InternalSettingsRepository) : ViewModelProvider.Factory {
override fun create(modelClass: Class): T {
return requireNotNull(modelClass.cast(InternalSettingsViewModel(repository)))
diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationFragment.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationFragment.kt
index a02d03dfb7..83b81b825b 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationFragment.kt
+++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationFragment.kt
@@ -590,7 +590,7 @@ class ConversationFragment :
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
binding.toolbar.isBackInvokedCallbackEnabled = false
- binding.root.setUseWindowTypes(resources.getWindowSizeClass().isCompact())
+ binding.root.setUseWindowTypes(!resources.getWindowSizeClass().isSplitPane())
disposables.bindTo(viewLifecycleOwner)
@@ -1370,9 +1370,7 @@ class ConversationFragment :
}
private fun presentNavigationIconForNormal() {
- val windowSizeClass = resources.getWindowSizeClass()
-
- if (windowSizeClass.isCompact()) {
+ if (!resources.getWindowSizeClass().isSplitPane()) {
binding.toolbar.setNavigationIcon(R.drawable.ic_arrow_left_24)
binding.toolbar.setNavigationContentDescription(R.string.ConversationFragment__content_description_back_button)
binding.toolbar.setNavigationOnClickListener {
diff --git a/app/src/main/java/org/thoughtcrime/securesms/keyvalue/InternalValues.kt b/app/src/main/java/org/thoughtcrime/securesms/keyvalue/InternalValues.kt
index bd1d379a39..f3821d2979 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/keyvalue/InternalValues.kt
+++ b/app/src/main/java/org/thoughtcrime/securesms/keyvalue/InternalValues.kt
@@ -29,6 +29,7 @@ class InternalValues internal constructor(store: KeyValueStore) : SignalStoreVal
const val ENCODE_HEVC: String = "internal.hevc_encoding"
const val NEW_CALL_UI: String = "internal.new.call.ui"
const val LARGE_SCREEN_UI: String = "internal.large.screen.ui"
+ const val FORCE_SPLIT_PANE_ON_COMPACT_LANDSCAPE: String = "internal.force.split.pane.on.compact.landscape.ui"
}
public override fun onFirstEverAppLaunch() = Unit
@@ -40,6 +41,11 @@ class InternalValues internal constructor(store: KeyValueStore) : SignalStoreVal
*/
var largeScreenUi by booleanValue(LARGE_SCREEN_UI, false).defaultForExternalUsers()
+ /**
+ * Force split-pane mode on compact landscape
+ */
+ var forceSplitPaneOnCompactLandscape by booleanValue(FORCE_SPLIT_PANE_ON_COMPACT_LANDSCAPE, false).defaultForExternalUsers()
+
/**
* Members will not be added directly to a GV2 even if they could be.
*/
diff --git a/app/src/main/java/org/thoughtcrime/securesms/main/MainContentLayoutData.kt b/app/src/main/java/org/thoughtcrime/securesms/main/MainContentLayoutData.kt
index 60abaeaa46..82f5fe656a 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/main/MainContentLayoutData.kt
+++ b/app/src/main/java/org/thoughtcrime/securesms/main/MainContentLayoutData.kt
@@ -15,10 +15,14 @@ import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import org.thoughtcrime.securesms.window.WindowSizeClass
+private val MEDIUM_CONTENT_CORNERS = 18.dp
+private val EXTENDED_CONTENT_CORNERS = 14.dp
+
/**
* Describes metrics for the content layout (list and detail) of the main screen.
*
- * @param shape The shape of each of the list and detail fragments
+ * @param shape The clipping shape of each of the list and detail fragments
+ * @param navigationBarShape The clipping shape applied to the navigation bar, if present.
* @param partitionWidth The width of the divider between list and detail
* @param listPaddingStart The padding between the list pane and the navigation rail
* @param detailPaddingEnd The padding at the end of the detail pane
@@ -26,6 +30,7 @@ import org.thoughtcrime.securesms.window.WindowSizeClass
@Immutable
data class MainContentLayoutData(
val shape: Shape,
+ val navigationBarShape: Shape,
val partitionWidth: Dp,
val listPaddingStart: Dp,
val detailPaddingEnd: Dp
@@ -49,9 +54,9 @@ data class MainContentLayoutData(
return remember(maxWidth, windowSizeClass) {
when {
- windowSizeClass.isCompact() -> maxWidth
- windowSizeClass.isMedium() -> (maxWidth - extraPadding) / 2f
- else -> 416.dp
+ !windowSizeClass.isSplitPane() -> maxWidth
+ windowSizeClass.isExtended() -> 416.dp
+ else -> (maxWidth - extraPadding) / 2f
}
}
}
@@ -67,24 +72,29 @@ data class MainContentLayoutData(
return remember(windowSizeClass) {
MainContentLayoutData(
shape = when {
- windowSizeClass.isCompact() -> RectangleShape
- windowSizeClass.isMedium() -> RoundedCornerShape(18.dp)
- else -> RoundedCornerShape(14.dp)
+ !windowSizeClass.isSplitPane() -> RectangleShape
+ windowSizeClass.isExtended() -> RoundedCornerShape(EXTENDED_CONTENT_CORNERS)
+ else -> RoundedCornerShape(MEDIUM_CONTENT_CORNERS)
+ },
+ navigationBarShape = when {
+ !windowSizeClass.isSplitPane() -> RectangleShape
+ windowSizeClass.isExtended() -> RoundedCornerShape(0.dp, 0.dp, EXTENDED_CONTENT_CORNERS, EXTENDED_CONTENT_CORNERS)
+ else -> RoundedCornerShape(0.dp, 0.dp, MEDIUM_CONTENT_CORNERS, MEDIUM_CONTENT_CORNERS)
},
partitionWidth = when {
- windowSizeClass.isCompact() -> 0.dp
- windowSizeClass.isMedium() -> 13.dp
- else -> 16.dp
+ !windowSizeClass.isSplitPane() -> 0.dp
+ windowSizeClass.isExtended() -> 16.dp
+ else -> 13.dp
},
listPaddingStart = when {
- windowSizeClass.isCompact() -> 0.dp
- windowSizeClass.isMedium() -> 12.dp
- else -> 16.dp
+ !windowSizeClass.isSplitPane() -> 0.dp
+ windowSizeClass.isExtended() -> 16.dp
+ else -> 12.dp
},
detailPaddingEnd = when {
- windowSizeClass.isCompact() -> 0.dp
- windowSizeClass.isMedium() -> 12.dp
- else -> 24.dp
+ !windowSizeClass.isSplitPane() -> 0.dp
+ windowSizeClass.isExtended() -> 24.dp
+ else -> 12.dp
}
)
}
diff --git a/app/src/main/java/org/thoughtcrime/securesms/main/MainNavigation.kt b/app/src/main/java/org/thoughtcrime/securesms/main/MainNavigation.kt
index d8c4b789f1..e762fb58e4 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/main/MainNavigation.kt
+++ b/app/src/main/java/org/thoughtcrime/securesms/main/MainNavigation.kt
@@ -12,7 +12,6 @@ import androidx.compose.animation.core.tween
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.BoxScope
-import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.defaultMinSize
import androidx.compose.foundation.layout.height
@@ -97,50 +96,46 @@ fun MainNavigationBar(
state: MainNavigationState,
onDestinationSelected: (MainNavigationListLocation) -> Unit
) {
- Column(modifier = Modifier.background(color = SignalTheme.colors.colorSurface2)) {
- NavigationBar(
- containerColor = SignalTheme.colors.colorSurface2,
- contentColor = MaterialTheme.colorScheme.onSurface,
- modifier = Modifier.height(if (state.compact) 48.dp else 80.dp),
- windowInsets = WindowInsets(0, 0, 0, 0)
- ) {
- val entries = remember(state.isStoriesFeatureEnabled) {
- if (state.isStoriesFeatureEnabled) {
- MainNavigationListLocation.entries
- } else {
- MainNavigationListLocation.entries.filterNot { it == MainNavigationListLocation.STORIES }
- }
- }
-
- entries.forEach { destination ->
-
- val badgeCount = when (destination) {
- MainNavigationListLocation.CHATS -> state.chatsCount
- MainNavigationListLocation.CALLS -> state.callsCount
- MainNavigationListLocation.STORIES -> state.storiesCount
- }
-
- val selected = state.selectedDestination == destination
- NavigationBarItem(
- selected = selected,
- icon = {
- NavigationDestinationIcon(
- destination = destination,
- selected = selected
- )
- },
- label = if (state.compact) null else {
- { NavigationDestinationLabel(destination) }
- },
- onClick = {
- onDestinationSelected(destination)
- },
- modifier = Modifier.drawNavigationBarBadge(count = badgeCount, compact = state.compact)
- )
+ NavigationBar(
+ containerColor = SignalTheme.colors.colorSurface2,
+ contentColor = MaterialTheme.colorScheme.onSurface,
+ modifier = Modifier.height(if (state.compact) 48.dp else 80.dp),
+ windowInsets = WindowInsets(0, 0, 0, 0)
+ ) {
+ val entries = remember(state.isStoriesFeatureEnabled) {
+ if (state.isStoriesFeatureEnabled) {
+ MainNavigationListLocation.entries
+ } else {
+ MainNavigationListLocation.entries.filterNot { it == MainNavigationListLocation.STORIES }
}
}
- NavigationBarSpacerCompat()
+ entries.forEach { destination ->
+
+ val badgeCount = when (destination) {
+ MainNavigationListLocation.CHATS -> state.chatsCount
+ MainNavigationListLocation.CALLS -> state.callsCount
+ MainNavigationListLocation.STORIES -> state.storiesCount
+ }
+
+ val selected = state.selectedDestination == destination
+ NavigationBarItem(
+ selected = selected,
+ icon = {
+ NavigationDestinationIcon(
+ destination = destination,
+ selected = selected
+ )
+ },
+ label = if (state.compact) null else {
+ { NavigationDestinationLabel(destination) }
+ },
+ onClick = {
+ onDestinationSelected(destination)
+ },
+ modifier = Modifier.drawNavigationBarBadge(count = badgeCount, compact = state.compact)
+ )
+ }
}
}
diff --git a/app/src/main/java/org/thoughtcrime/securesms/window/AppScaffold.kt b/app/src/main/java/org/thoughtcrime/securesms/window/AppScaffold.kt
index 55f6a60bbd..23990cbaa9 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/window/AppScaffold.kt
+++ b/app/src/main/java/org/thoughtcrime/securesms/window/AppScaffold.kt
@@ -11,6 +11,7 @@ import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.displayCutoutPadding
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.material3.Text
import androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveApi
@@ -18,7 +19,7 @@ import androidx.compose.material3.adaptive.currentWindowAdaptiveInfo
import androidx.compose.material3.adaptive.layout.AnimatedPane
import androidx.compose.material3.adaptive.layout.PaneExpansionState
import androidx.compose.material3.adaptive.layout.ThreePaneScaffoldScope
-import androidx.compose.material3.adaptive.layout.calculatePaneScaffoldDirectiveWithTwoPanesOnMediumWidth
+import androidx.compose.material3.adaptive.layout.calculatePaneScaffoldDirective
import androidx.compose.material3.adaptive.layout.rememberPaneExpansionState
import androidx.compose.material3.adaptive.navigation.NavigableListDetailPaneScaffold
import androidx.compose.material3.adaptive.navigation.ThreePaneScaffoldNavigator
@@ -83,6 +84,14 @@ enum class WindowSizeClass(
fun isLandscape(): Boolean = this == COMPACT_LANDSCAPE || this == MEDIUM_LANDSCAPE || this == EXTENDED_LANDSCAPE
+ fun isSplitPane(): Boolean {
+ return if (SignalStore.internal.largeScreenUi && SignalStore.internal.forceSplitPaneOnCompactLandscape) {
+ this != COMPACT_PORTRAIT
+ } else {
+ this.navigation != Navigation.BAR
+ }
+ }
+
companion object {
@OptIn(ExperimentalWindowCoreApi::class)
fun Resources.getWindowSizeClass(): WindowSizeClass {
@@ -221,7 +230,11 @@ private fun ListAndNavigation(
bottomNavContent: @Composable () -> Unit,
windowSizeClass: WindowSizeClass
) {
- Row {
+ Row(
+ modifier = if (windowSizeClass.isLandscape()) {
+ Modifier.displayCutoutPadding()
+ } else Modifier
+ ) {
if (windowSizeClass.navigation == Navigation.RAIL) {
navRailContent()
}
@@ -250,7 +263,7 @@ private fun AppScaffoldPreview() {
Previews.Preview {
AppScaffold(
navigator = rememberListDetailPaneScaffoldNavigator(
- scaffoldDirective = calculatePaneScaffoldDirectiveWithTwoPanesOnMediumWidth(
+ scaffoldDirective = calculatePaneScaffoldDirective(
currentWindowAdaptiveInfo()
).copy(
horizontalPartitionSpacerSize = 10.dp