diff --git a/app/src/main/java/org/thoughtcrime/securesms/MainActivity.kt b/app/src/main/java/org/thoughtcrime/securesms/MainActivity.kt index 1b08796516..e1a4c5d94e 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/MainActivity.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/MainActivity.kt @@ -28,10 +28,12 @@ import androidx.compose.foundation.layout.BoxWithConstraintsScope import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.displayCutoutPadding import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.foundation.layout.padding 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.navigation.rememberListDetailPaneScaffoldNavigator import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect @@ -41,9 +43,6 @@ import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip -import androidx.compose.ui.graphics.RectangleShape -import androidx.compose.ui.graphics.Shape -import androidx.compose.ui.unit.dp import androidx.fragment.app.DialogFragment import androidx.fragment.compose.AndroidFragment import androidx.fragment.compose.rememberFragmentState @@ -83,6 +82,7 @@ import org.thoughtcrime.securesms.main.MainActivityListHostFragment import org.thoughtcrime.securesms.main.MainBottomChrome import org.thoughtcrime.securesms.main.MainBottomChromeCallback import org.thoughtcrime.securesms.main.MainBottomChromeState +import org.thoughtcrime.securesms.main.MainContentLayoutData import org.thoughtcrime.securesms.main.MainMegaphoneState import org.thoughtcrime.securesms.main.MainNavigationBar import org.thoughtcrime.securesms.main.MainNavigationDetailLocation @@ -231,28 +231,29 @@ class MainActivity : PassphraseRequiredActivity(), VoiceNoteMediaControllerOwner ) } - val scaffoldNavigator = rememberListDetailPaneScaffoldNavigator() val windowSizeClass = WindowSizeClass.rememberWindowSizeClass() - - val contentClip: Shape = remember(windowSizeClass) { - if (windowSizeClass.isExtended()) { - RoundedCornerShape(18.dp) - } else { - RectangleShape - } - } - - LaunchedEffect(detailLocation) { - if (detailLocation is MainNavigationDetailLocation.Conversation) { - if (SignalStore.internal.largeScreenUi) { - scaffoldNavigator.navigateTo(ThreePaneScaffoldRole.Primary, detailLocation) - } else { - startActivity((detailLocation as MainNavigationDetailLocation.Conversation).intent) - } - } - } + val contentLayoutData = MainContentLayoutData.rememberContentLayoutData() MainContainer { + val scaffoldNavigator = rememberListDetailPaneScaffoldNavigator( + scaffoldDirective = calculatePaneScaffoldDirectiveWithTwoPanesOnMediumWidth( + currentWindowAdaptiveInfo() + ).copy( + horizontalPartitionSpacerSize = contentLayoutData.partitionWidth, + defaultPanePreferredWidth = contentLayoutData.rememberDefaultPanePreferredWidth(maxWidth) + ) + ) + + LaunchedEffect(detailLocation) { + if (detailLocation is MainNavigationDetailLocation.Conversation) { + if (SignalStore.internal.largeScreenUi) { + scaffoldNavigator.navigateTo(ThreePaneScaffoldRole.Primary, detailLocation) + } else { + startActivity((detailLocation as MainNavigationDetailLocation.Conversation).intent) + } + } + } + AppScaffold( navigator = scaffoldNavigator, bottomNavContent = { @@ -280,9 +281,10 @@ class MainActivity : PassphraseRequiredActivity(), VoiceNoteMediaControllerOwner Column( modifier = Modifier + .padding(start = contentLayoutData.listPaddingStart) .fillMaxSize() .background(listContainerColor) - .clip(contentClip) + .clip(contentLayoutData.shape) ) { MainToolbar( state = mainToolbarState, @@ -316,13 +318,17 @@ class MainActivity : PassphraseRequiredActivity(), VoiceNoteMediaControllerOwner fragmentState = fragmentState, arguments = requireNotNull(destination.intent.extras) { "Handed null Conversation intent arguments." }, modifier = Modifier + .padding(end = contentLayoutData.detailPaddingEnd) + .clip(contentLayoutData.shape) .background(color = MaterialTheme.colorScheme.surface) .fillMaxSize() - .clip(contentClip) ) } } - } + }, + paneExpansionDragHandle = if (contentLayoutData.hasDragHandle()) { + { } + } else null ) } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/main/MainContentLayoutData.kt b/app/src/main/java/org/thoughtcrime/securesms/main/MainContentLayoutData.kt new file mode 100644 index 0000000000..60abaeaa46 --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/main/MainContentLayoutData.kt @@ -0,0 +1,93 @@ +/* + * Copyright 2025 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.thoughtcrime.securesms.main + +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.runtime.Composable +import androidx.compose.runtime.Immutable +import androidx.compose.runtime.remember +import androidx.compose.ui.graphics.RectangleShape +import androidx.compose.ui.graphics.Shape +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.dp +import org.thoughtcrime.securesms.window.WindowSizeClass + +/** + * 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 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 + */ +@Immutable +data class MainContentLayoutData( + val shape: Shape, + val partitionWidth: Dp, + val listPaddingStart: Dp, + val detailPaddingEnd: Dp +) { + private val extraPadding: Dp = partitionWidth + listPaddingStart + detailPaddingEnd + + /** + * Whether or not the WindowSizeClass supports drag handles. + */ + @Composable + fun hasDragHandle(): Boolean { + return WindowSizeClass.rememberWindowSizeClass().isExtended() + } + + /** + * Calculates the default preferred width + */ + @Composable + fun rememberDefaultPanePreferredWidth(maxWidth: Dp): Dp { + val windowSizeClass = WindowSizeClass.rememberWindowSizeClass() + + return remember(maxWidth, windowSizeClass) { + when { + windowSizeClass.isCompact() -> maxWidth + windowSizeClass.isMedium() -> (maxWidth - extraPadding) / 2f + else -> 416.dp + } + } + } + + companion object { + /** + * Uses the WindowSizeClass to build out a MainContentLayoutData. + */ + @Composable + fun rememberContentLayoutData(): MainContentLayoutData { + val windowSizeClass = WindowSizeClass.rememberWindowSizeClass() + + return remember(windowSizeClass) { + MainContentLayoutData( + shape = when { + windowSizeClass.isCompact() -> RectangleShape + windowSizeClass.isMedium() -> RoundedCornerShape(18.dp) + else -> RoundedCornerShape(14.dp) + }, + partitionWidth = when { + windowSizeClass.isCompact() -> 0.dp + windowSizeClass.isMedium() -> 13.dp + else -> 16.dp + }, + listPaddingStart = when { + windowSizeClass.isCompact() -> 0.dp + windowSizeClass.isMedium() -> 12.dp + else -> 16.dp + }, + detailPaddingEnd = when { + windowSizeClass.isCompact() -> 0.dp + windowSizeClass.isMedium() -> 12.dp + else -> 24.dp + } + ) + } + } + } +} 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 c20e688b9e..8d7ebffe9e 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/window/AppScaffold.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/window/AppScaffold.kt @@ -16,6 +16,10 @@ import androidx.compose.material3.Text import androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveApi 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.rememberPaneExpansionState import androidx.compose.material3.adaptive.navigation.NavigableListDetailPaneScaffold import androidx.compose.material3.adaptive.navigation.ThreePaneScaffoldNavigator import androidx.compose.material3.adaptive.navigation.rememberListDetailPaneScaffoldNavigator @@ -28,6 +32,7 @@ import androidx.compose.ui.platform.LocalConfiguration import androidx.compose.ui.platform.LocalInspectionMode import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp import androidx.window.core.ExperimentalWindowCoreApi import androidx.window.core.layout.WindowHeightSizeClass import androidx.window.core.layout.WindowWidthSizeClass @@ -57,7 +62,7 @@ enum class WindowSizeClass( ) { COMPACT_PORTRAIT(Navigation.BAR), COMPACT_LANDSCAPE(Navigation.BAR), - MEDIUM_PORTRAIT(Navigation.BAR), + MEDIUM_PORTRAIT(Navigation.RAIL), MEDIUM_LANDSCAPE(Navigation.RAIL), EXTENDED_PORTRAIT(Navigation.RAIL), EXTENDED_LANDSCAPE(Navigation.RAIL); @@ -160,6 +165,7 @@ fun AppScaffold( detailContent: @Composable () -> Unit = {}, navRailContent: @Composable () -> Unit = {}, bottomNavContent: @Composable () -> Unit = {}, + paneExpansionDragHandle: (@Composable ThreePaneScaffoldScope.(PaneExpansionState) -> Unit)? = null, listContent: @Composable () -> Unit ) { val isForcedCompact = WindowSizeClass.checkForcedCompact() @@ -176,9 +182,10 @@ fun AppScaffold( return } - if (windowSizeClass.isMedium()) { - Row { - Box(modifier = Modifier.weight(1f)) { + NavigableListDetailPaneScaffold( + navigator = navigator, + listPane = { + AnimatedPane { ListAndNavigation( listContent = listContent, navRailContent = navRailContent, @@ -186,31 +193,15 @@ fun AppScaffold( windowSizeClass = windowSizeClass ) } - - Box(modifier = Modifier.weight(1f)) { + }, + detailPane = { + AnimatedPane { detailContent() } - } - } else { - NavigableListDetailPaneScaffold( - navigator = navigator, - listPane = { - AnimatedPane { - ListAndNavigation( - listContent = listContent, - navRailContent = navRailContent, - bottomNavContent = bottomNavContent, - windowSizeClass = windowSizeClass - ) - } - }, - detailPane = { - AnimatedPane { - detailContent() - } - } - ) - } + }, + paneExpansionDragHandle = paneExpansionDragHandle, + paneExpansionState = rememberPaneExpansionState() + ) } @Composable @@ -248,6 +239,13 @@ private fun ListAndNavigation( private fun AppScaffoldPreview() { Previews.Preview { AppScaffold( + navigator = rememberListDetailPaneScaffoldNavigator( + scaffoldDirective = calculatePaneScaffoldDirectiveWithTwoPanesOnMediumWidth( + currentWindowAdaptiveInfo() + ).copy( + horizontalPartitionSpacerSize = 10.dp + ) + ), listContent = { Box( contentAlignment = Alignment.Center,