Add split pane UI for new conversation screen.

This commit is contained in:
jeffrey-signal
2025-10-08 14:47:48 -04:00
committed by Alex Hart
parent 0f35eb7f7b
commit 534756c833
15 changed files with 3975 additions and 38 deletions

View File

@@ -38,6 +38,7 @@ import androidx.compose.ui.layout.layout
import androidx.compose.ui.platform.LocalConfiguration
import androidx.compose.ui.platform.LocalInspectionMode
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import androidx.compose.ui.zIndex
import androidx.window.core.ExperimentalWindowCoreApi
@@ -86,6 +87,12 @@ enum class WindowSizeClass(
EXTENDED_PORTRAIT(Navigation.RAIL),
EXTENDED_LANDSCAPE(Navigation.RAIL);
val listPaneDefaultPreferredWidth: Dp
get() = if (isExtended()) 416.dp else 316.dp
val detailPaneMaxContentWidth: Dp = 624.dp
val horizontalPartitionDefaultSpacerSize: Dp = 12.dp
fun isCompact(): Boolean = this == COMPACT_PORTRAIT || this == COMPACT_LANDSCAPE
fun isMedium(): Boolean = this == MEDIUM_PORTRAIT || this == MEDIUM_LANDSCAPE
fun isExtended(): Boolean = this == EXTENDED_PORTRAIT || this == EXTENDED_LANDSCAPE
@@ -93,8 +100,11 @@ enum class WindowSizeClass(
fun isLandscape(): Boolean = this == COMPACT_LANDSCAPE || this == MEDIUM_LANDSCAPE || this == EXTENDED_LANDSCAPE
fun isPortrait(): Boolean = !isLandscape()
fun isSplitPane(): Boolean {
return if (isLargeScreenSupportEnabled() && SignalStore.internal.forceSplitPaneOnCompactLandscape) {
@JvmOverloads
fun isSplitPane(
forceSplitPaneOnCompactLandscape: Boolean = SignalStore.internal.forceSplitPaneOnCompactLandscape
): Boolean {
return if (isLargeScreenSupportEnabled() && forceSplitPaneOnCompactLandscape) {
this != COMPACT_PORTRAIT
} else {
this.navigation != Navigation.BAR

View File

@@ -19,7 +19,9 @@ import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.platform.LocalInspectionMode
import androidx.compose.ui.unit.Dp
import org.thoughtcrime.securesms.keyvalue.SignalStore
/**
* AppScaffoldNavigator wraps a delegate navigator (such as the value returned by [rememberThreePaneScaffoldNavigatorDelegate]
@@ -85,9 +87,12 @@ open class AppScaffoldNavigator<T> @RememberInComposition constructor(private va
@OptIn(ExperimentalMaterial3AdaptiveApi::class)
@Composable
fun rememberAppScaffoldNavigator(
isSplitPane: Boolean,
horizontalPartitionSpacerSize: Dp,
defaultPanePreferredWidth: Dp
windowSizeClass: WindowSizeClass = WindowSizeClass.rememberWindowSizeClass(),
isSplitPane: Boolean = windowSizeClass.isSplitPane(
forceSplitPaneOnCompactLandscape = if (LocalInspectionMode.current) false else SignalStore.internal.forceSplitPaneOnCompactLandscape
),
horizontalPartitionSpacerSize: Dp = windowSizeClass.horizontalPartitionDefaultSpacerSize,
defaultPanePreferredWidth: Dp = windowSizeClass.listPaneDefaultPreferredWidth
): AppScaffoldNavigator<Any> {
val delegate = rememberThreePaneScaffoldNavigatorDelegate(
isSplitPane,

View File

@@ -0,0 +1,142 @@
/*
* Copyright 2025 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.thoughtcrime.securesms.window
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveApi
import androidx.compose.material3.adaptive.layout.PaneExpansionState
import androidx.compose.material3.adaptive.layout.ThreePaneScaffoldScope
import androidx.compose.material3.adaptive.layout.rememberPaneExpansionState
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.platform.LocalInspectionMode
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.res.vectorResource
import androidx.compose.ui.text.style.TextAlign
import org.signal.core.ui.compose.AllDevicePreviews
import org.signal.core.ui.compose.Previews
import org.signal.core.ui.compose.Scaffolds
import org.thoughtcrime.securesms.R
import org.thoughtcrime.securesms.keyvalue.SignalStore
/**
* Wraps [AppScaffold], adding a top app bar that spans across both the list and detail panes.
*/
@OptIn(ExperimentalMaterial3AdaptiveApi::class)
@Composable
fun AppScaffoldWithTopBar(
navigator: AppScaffoldNavigator<Any> = rememberAppScaffoldNavigator(),
topBarContent: @Composable () -> Unit = {},
detailContent: @Composable () -> Unit = {},
navRailContent: @Composable () -> Unit = {},
bottomNavContent: @Composable () -> Unit = {},
paneExpansionState: PaneExpansionState = rememberPaneExpansionState(),
paneExpansionDragHandle: (@Composable ThreePaneScaffoldScope.(PaneExpansionState) -> Unit)? = null,
animatorFactory: AppScaffoldAnimationStateFactory = AppScaffoldAnimationStateFactory.Default,
listContent: @Composable () -> Unit
) {
val windowSizeClass = WindowSizeClass.rememberWindowSizeClass()
val isSplitPane = windowSizeClass.isSplitPane(
forceSplitPaneOnCompactLandscape = if (LocalInspectionMode.current) false else SignalStore.internal.forceSplitPaneOnCompactLandscape
)
if (isSplitPane) {
Column {
topBarContent()
AppScaffold(
navigator = navigator,
detailContent = detailContent,
navRailContent = navRailContent,
bottomNavContent = bottomNavContent,
paneExpansionState = paneExpansionState,
paneExpansionDragHandle = paneExpansionDragHandle,
animatorFactory = animatorFactory,
listContent = listContent
)
}
} else {
AppScaffold(
navigator = navigator,
detailContent = detailContent,
navRailContent = navRailContent,
bottomNavContent = bottomNavContent,
paneExpansionState = paneExpansionState,
paneExpansionDragHandle = paneExpansionDragHandle,
animatorFactory = animatorFactory,
listContent = {
Scaffold(topBar = topBarContent) { paddingValues ->
Box(modifier = Modifier.padding(paddingValues)) {
listContent()
}
}
}
)
}
}
@OptIn(ExperimentalMaterial3Api::class, ExperimentalMaterial3AdaptiveApi::class)
@AllDevicePreviews
@Composable
private fun AppScaffoldWithTopBarPreview() {
Previews.Preview {
val windowSizeClass = WindowSizeClass.rememberWindowSizeClass()
val isSplitPane = windowSizeClass.isSplitPane(forceSplitPaneOnCompactLandscape = false)
AppScaffoldWithTopBar(
navigator = rememberAppScaffoldNavigator(),
topBarContent = {
Scaffolds.DefaultTopAppBar(
title = if (!isSplitPane) stringResource(R.string.NewConversationActivity__new_message) else "",
titleContent = { _, title -> Text(text = title, style = MaterialTheme.typography.titleLarge) },
navigationIcon = ImageVector.vectorResource(R.drawable.symbol_arrow_start_24),
navigationContentDescription = "",
onNavigationClick = { }
)
},
listContent = {
Box(
contentAlignment = Alignment.Center,
modifier = Modifier
.fillMaxSize()
.background(color = Color.Red)
) {
Text(
text = "ListContent\n$windowSizeClass",
textAlign = TextAlign.Center
)
}
},
detailContent = {
Box(
contentAlignment = Alignment.Center,
modifier = Modifier
.fillMaxSize()
.background(color = Color.Blue)
) {
Text(
text = "DetailContent",
textAlign = TextAlign.Center
)
}
}
)
}
}