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